HackThisSite - Extended Basic Mission 06

Challenge

This site is run by a new sysadmin who does not know much about web configuration. The script is located at http://moo.com/moo.php Attempt to make the script think you are authed by entering the correct URI. 进入正确的 URI,让脚本以为你已经通过认证。

关卡页给出脚本 me.php 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$user = $_GET['user'];
$pass = $_GET['pass'];
if (isAuthed($user,$pass))
{
$passed = TRUE;
}
if ($passed == TRUE)
{
echo 'you win';
}
?>
<form action="me.php" method="get">
<input type="text" name="user" />
<input type="password" name="pass" />
</form>
<?php
function isAuthed($a,$b)
{
return FALSE;
}
?>

页面底部还有一行全局提醒:All missions are case sensitive. I tried to keep them lowercase however.。答案大小写敏感。

Solution

isAuthed($a,$b) 的返回值写死为 FALSE,所以走正常表单提交 user/passif (isAuthed($user,$pass)) 永远不成立,$passed = TRUE 这一行根本不会执行。想从凭据这条路进去是没有出口的。

脚本只从 $_GET 里取了 userpass$passed 在整个文件里没有任何赋值默认值的语句:

1
2
3
4
$user = $_GET['user'];
$pass = $_GET['pass'];
if (isAuthed($user,$pass)) { $passed = TRUE; }
if ($passed == TRUE) { echo 'you win'; }

如果 $passed 只是一个普通的未定义局部变量,第二个 if 恒为假,这题无从下手。题面第一句正是钥匙:new sysadmin who does not know much about web configuration,一个不懂 Web 配置的管理员,对应的就是这个时代最典型的一项 PHP 配置错误:register_globals = On

register_globals 打开时,PHP 会在脚本运行前把查询字符串里的每个键自动导入成同名全局变量。于是 URL 里的 passed 会直接落到脚本的 $passed 上,完全绕过 isAuthed() 这条分支。

要让 $passed == TRUE 成立,只需在 URL 上挂一个 passed 参数:

1
http://moo.com/moo.php?passed=TRUE

PHP 的松散比较 == 在两侧类型不同时会做类型转换:字符串和布尔值比较时,字符串被转成布尔值,任何非空且非 "0" 的字符串都为真。因此 "TRUE" == TRUE 求值为 true,脚本进入 if 分支并输出 you win

值按源码里惯用的写法给 TRUE

formkey 每次加载关卡页都会变,取页面和提交必须在同一次运行里完成;cookie 只从环境变量读入,不落盘。

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
import os
import re
import urllib.parse
import urllib.request

UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36")
BASE = "https://www.hackthissite.org"
LEVEL = BASE + "/missions/playit/extbasic/6/"
TEMPLATE = BASE + "/missions/extbasic/template.php"
COOKIE = os.environ["HTS_COOKIE"]
ANSWER = "http://moo.com/moo.php?passed=TRUE"

def fetch(url, referer=None):
headers = {"Cookie": COOKIE, "User-Agent": UA}
if referer:
headers["Referer"] = referer
req = urllib.request.Request(url, headers=headers)
return urllib.request.urlopen(req, timeout=30).read().decode("utf-8", "replace")

def main():
page = fetch(LEVEL)
formkey = re.search(r'name="formkey"\s+value="([^"]+)"', page).group(1)
data = urllib.parse.urlencode(
{"formkey": formkey, "lvl": "6", "pass": ANSWER}).encode()
req = urllib.request.Request(
TEMPLATE, data=data,
headers={"Cookie": COOKIE, "User-Agent": UA,
"Content-Type": "application/x-www-form-urlencoded",
"Referer": LEVEL})
print(urllib.request.urlopen(req, timeout=30).read().decode("utf-8", "replace"))

if __name__ == "__main__":
main()

关卡以 You have already done this mission. 与个人资料的 Extbasic: 徽章 (6) 两处同时作为完成判据。

Key points

  • register_globals 会把查询字符串的键自动提升为全局变量,攻击者可以覆盖脚本本应自己掌控的标志位(如 $passed
  • 判断是否登录/是否通过的标志必须由服务端在可信逻辑里赋值,不能依赖任何用户可控的名字
  • 认证分支不可达时,漏洞往往在缺失的初始化宽松比较上,而不是在比较内容上
  • == 的类型转换让 "TRUE" == TRUE 为真;权限判断应使用 === 并显式校验类型
  • register_globals 早已在 PHP 5.4 中被移除,现代 PHP 不再存在这条路径
http://moo.com/moo.php?passed=TRUE