HackThisSite - Extended Basic Mission 07

Challenge

修正一个 PHP 页面里同时带有 bug 和漏洞的那一行,提交修正后的整行。

关卡页给出的是一段数据录入代码,要求:There is only one line that has a vuln, correct it. The output does not have to be valid XHTML and assume that a mysql connection has been made already. There is a bug as well as a vuln. You MUST fix both.

1
2
3
4
5
6
7
8
9
10
11
<?php
if(!empty($_POST['data']))
{
$data = mysql_real_escape_string($_POST['data']);
mysql_query("INSERT INTO tbl_data (data) VALUES ('$data')");
}
?>
<form name="grezvahfvfnjuvavatovgpu" action="<?=$_SERVER['PHP_SELF']?>" method="get">
<input type="text" name="data" />
<input type="submit" />
</form>

Solution

题面限定只有一行有漏洞,并额外提示 bug 和 vuln 都要修。

  • 前面四行是标准的先转义再拼接写法:mysql_real_escape_string 负责把 $_POST['data'] 里的引号等字符转义;题面明确说明数据库连接已经建立(该函数需要一个活动连接才能工作),所以这段拼接没有缺陷。
  • 真正承载输入 → 输出的只有末尾那一个 <form ...> 标签:它一边把 $_SERVER['PHP_SELF'] 输出进 HTML,一边决定表单用什么方法回传。两个缺陷正好都落在这同一行,对应题面的一行有漏洞加上 bug/vuln 都要修。

$_SERVER['PHP_SELF'] 是当前脚本的路径,会把用户可控的 PATH_INFO 一并包含进来。攻击者可以请求:

1
/missions/playit/extbasic/7/index.php/"><script>alert(1)</script>

此时 PHP_SELF 的值就是 /missions/playit/extbasic/7/index.php/"><script>alert(1)</script>,被原样拼进 action="..." 后会提前闭合属性并注入脚本标签,形成反射型 XSS:

1
<form name="grezvahfvfnjuvavatovgpu" action="/missions/playit/extbasic/7/index.php/"><script>alert(1)</script>" method="get">

修法是用 htmlspecialchars()< > " ' 等字符转成 HTML 实体再放进属性;闭合用的引号被转义后无法逃逸:

1
action="<?=htmlspecialchars($_SERVER['PHP_SELF'])?>"

这里 PHP_SELF 只出现在输出上下文里,按输出位置转义即可,它并没有参与 SQL 拼接。

处理逻辑读的是 $_POST['data']

1
if(!empty($_POST['data']))

但表单写的是 method="get",会把 data 作为查询参数写入 URL,$_POST['data'] 永远为空,if 分支进不去,INSERT 从不执行。这是功能层面的 bug。把表单方法改成 method="post" 即可:

1
method="post"

把两处修复合回原行,注意保留页面原有的 <?=$_SERVER['PHP_SELF']?> 写法:

1
<form name="grezvahfvfnjuvavatovgpu" action="<?=htmlspecialchars($_SERVER['PHP_SELF'])?>" method="post">

playit 类别统一提交到 POST /missions/extbasic/template.php,字段为 formkey(每次加载关卡页都会变)、lvlpass。提交必须带上 Referer: .../playit/extbasic/7/,否则服务端返回 Invalid Referer 且不计完成;formkey 是一次性的,抓页面后立刻提交要在同一流程里完成。

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
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python3
"""HackThisSite Extended Basic 7 (playit) live solver.

The level prints a tiny PHP page and asks for the one line that carries both a
bug and a vulnerability:

<form name="grezvahfvfnjuvavatovgpu" action="<?=$_SERVER['PHP_SELF']?>" method="get">

* vuln: ``$_SERVER['PHP_SELF']`` is echoed raw into the ``action`` attribute, so
a path like ``/x.php/"><script>alert(1)</script>`` is reflected as markup and
becomes XSS. ``htmlspecialchars()`` fixes it.
* bug: the form submits with ``method="get"`` while the handler only reads
``$_POST['data']``, so the INSERT never runs. ``method="post"`` fixes it.

The corrected line is posted to ``/missions/extbasic/template.php`` together with
the per-load ``formkey`` and ``lvl``. A ``Referer`` pointing at the level page is
mandatory, otherwise the endpoint answers ``Invalid Referer`` and the attempt does
not count. The session cookie comes from ``HTS_COOKIE`` and is never persisted.
"""
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/7/"
TEMPLATE = BASE + "/missions/extbasic/template.php"
CK = os.environ["HTS_COOKIE"]

ANSWER = (
'<form name="grezvahfvfnjuvavatovgpu" '
'action="<?=htmlspecialchars($_SERVER[\'PHP_SELF\'])?>" method="post">'
)

def get(url):
req = urllib.request.Request(url, headers={"Cookie": CK, "User-Agent": UA})
return urllib.request.urlopen(req, timeout=30).read().decode("utf-8", "replace")

def main():
page = get(LEVEL)
formkey = re.search(r'name="formkey" value="([^"]+)"', page).group(1)
data = urllib.parse.urlencode(
{"formkey": formkey, "lvl": "7", "pass": ANSWER}).encode()
req = urllib.request.Request(
TEMPLATE, data=data,
headers={"Cookie": CK, "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("go-on link to level 8:", "/missions/playit/extbasic/8" in resp)

if __name__ == "__main__":
main()

运行输出:

1
2
$ cd <hts-workspace> && uv run python challenges/hts-playit/extbasic-7/solve.py
go-on link to level 8: True

被接受的提交会在响应里给出指向第 8 关的 GoOn.gif 链接;profile 的 Extbasic: 一行由 (1) (2) (3) (4) (5) (6) 变为 (1) (2) (3) (4) (5) (6) (7),类别总览里第 7 关也从 <font class="incomplete"> 换成完成样式。

一个易错点是把标签写成 action="<?= htmlspecialchars($_SERVER['PHP_SELF']) ?>"(在 PHP 标签内侧留空格)会被判错。页面里的原始写法是 <?=$_SERVER['PHP_SELF']?>,提交端按这一规范形态比对,修正应原地包裹,且不引入额外空格。

Key points

  • 输出转义要按上下文选函数:拼进 HTML 属性用 htmlspecialchars,拼进 SQL 才用 mysql_real_escape_string,两者不能互相替代。
  • $_SERVER['PHP_SELF'] 含有用户可控的 PATH_INFO,直接回显就是反射型 XSS;现代写法是用 action="" 让表单提交回当前 URL,确需显示路径时再转义。
  • 表单 method 必须和处理端读取的超全局数组一致:get$_GETpost$_POST,错配会让处理分支永远进不去。
  • 同一行可以同时承载多个缺陷:本例的 <form ...> 既输出了未转义数据(vuln),又选错了请求方法(bug)。
  • playit 提交按原样区分空格与大小写,修正片段应与页面给出的原始写法保持一致,不得重排格式。
method=post>