HackThisSite - Extended Basic Mission 13

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> 块里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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`。

此时 $resultfalse,脚本却直接读 $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 与字符串比较时按空串处理,于是:

1
2
3
4
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,脚本回 1email 换成 0x0e0 都不行:isset($_GET['email']) 对空串仍为 trueemail= 不会被前一个 isset 分支挡掉。

PHP 8 已经移除 mysql_* 扩展,于是把三个 DB 调用按关卡语义打桩:mysql_query() 返回空结果集(表中不存在所发送的 name),mysql_fetch_assoc() 对空集返回 false,与线上行为一致。

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
38
39
40
41
42
43
44
45
<?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 [];
}

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:

1
2
3
4
5
6
7
8
9
10
$ php replica_vrfy.php 'name=&email=' 2>/dev/null
1
$ php replica_vrfy.php 'name=nobody&email=' 2>/dev/null
1
$ php replica_vrfy.php 'name=nobody&email=0' 2>/dev/null
0
$ php replica_vrfy.php 'name=nobody&email=x' 2>/dev/null
0
$ php replica_vrfy.php 'name=nobody' 2>/dev/null
0

emailname 匹配不到 → 1;非空 email0;只给 name 不给 emailisset 为假)→ 0。与上面的推导一致。(2>/dev/null 只是滤掉 PHP 8 那条布尔下标警告;线上 PHP 5 不产生警告,返回同样是 null。)

Key points

  • namemysql_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=