ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

文件上传漏洞:绕过原理、Webshell攻击链与安全加固指南

文件上传漏洞:绕过原理、Webshell攻击链与安全加固指南

文件上传漏洞大概是所有Web漏洞里最“出圈”的一个,报错率常年排在最前面,可偏偏很多开发团队在它身上反复栽跟头。文件上传功能本身不复杂,风险出在它把用户可控的文件直接引入了服务器;一旦处理不严谨,攻击者就能上传可执行脚本,顺着这条路径拿到网站控制权,甚至横向移动进内网。本文结合我在安全测试和应急响应里的实操经历,把常见文件上传漏洞的原理、攻击链路、代码修复、服务端加固和事后处置一次讲透,适合刚入门的安全工程师,也适合想补安全短板的开发与运维。

1. 文件上传这个功能,为什么总是沦为攻击入口

1.1 上传功能天然就是把“用户输入”写到服务器磁盘

很多业务都有上传:头像、身份证照片、附件、编辑器插图、Excel导入。从安全角度看,上传功能是Web请求链条中少有的“直接向服务器磁盘写文件”的入口。其他漏洞比如SQL注入、XSS往往是在数据处理过程中被间接利用,而文件上传一旦绕过校验,攻击者直接获得一个落地的可执行文件。这就相当于你把门卫、人脸识别全绕过了,对方直接拿钥匙进了机房。

文件上传的处理流程通常包括:接收临时文件、校验类型、重命名/保存、返回访问URL。问题出在每个环节都可能被拆解绕过,尤其很多团队把“校验”寄托在客户端,前端弹出个“请上传jpg”就以为安全了,服务端只拿Content-Type碰运气。真实攻击里,攻击者的思路永远是抓包、改写请求,把所有限制拆成纸糊的墙。必须换一个假设来写代码:所有上传请求都可能是恶意构造的,所有来自文件内容的名字、类型、路径字段都不能直接信任。

1.2 业务越复杂,攻击面越容易被忽略

上传功能通常不是独立存在的,它会和用户系统、内容管理系统、导出系统、定时任务绑定。业务迭代多了,可能出现:A模块校验很严格,B模块为了兼容老客户端顺手放宽;这套系统里上传完文件,另一套系统负责展示,路径拼接不对导致目录穿越。“上传”经过多次版本迭代,校验逻辑往往被改得七零八落,最后没人能说清到底支持哪些扩展名、哪些MIME。

我见过一个后台系统,为了导入Excel开发了5套上传接口,其中一套完全没做任何校验,任何文件都能上传,最后被人传了脚本,站点被改成“灰产平台”。这不是个例。只要团队没有把上传安全抽成公共组件,每个接口各自实现一次,就总会有人写出一个“能用但不安全”的版本。攻击者并不会只盯着主上传入口,他会顺藤摸瓜翻所有带file参数的接口,任何一个漏网的都能变成突破口。

1.3 攻击者拿到上传漏洞意味着什么

从风险等级看,可执行脚本上传属于最高那一档。攻击者一旦把PHP/JSP/ASP这类脚本传到服务器并能执行,就直接拿到了和Web服务同权限的命令执行能力。后面的事几乎水到渠成:读取数据库配置、翻源代码、下载备份文件、创建反弹连接、横向尝试内网其他主机。

即使脚本执行权限被禁,仅靠上传能力配合中间件的解析差异或含恶意内容的图片文件,也能在业务端触发存储型XSS或逻辑异常。所以安全圈常把“能否上传脚本文件”作为一条重要防线,完全有道理。评估一个上传漏洞的严重性时,不要只看“能不能传文件”,要看“文件落地之后还能发生什么”。

2. 最常见的上传漏洞类型,以及它们的绕过逻辑

2.1 前端校验绕过:改一改请求就进去了

很多上传功能只在前端加了accept="image/jpeg"或JS文件大小判断。这类限制只能提升正常用户的体验,防不住任何稍微懂点抓包工具的人。原理很简单:浏览器上传文件时,最终提交的是multipart/form-data请求,filename、Content-Type都是请求里可修改的字段。前端限制看起来像锁,其实只是门上的贴纸。

我在授权测试里做过一次演示:一个上传头像功能前端只让选图片,用抓包工具拦截上传请求,把filename改成一个.php文件,内容改成测试脚本,直接绕过。后端连问都没问就把文件存下来了。所以判断一处上传校验靠不靠谱,第一步就是去掉前端、直接发原始请求试一次。如果发现后端返回的URL里带着我们传进去的文件名,说明原始文件名被直接采用了,这往往意味着后面还有更多可操作空间。

2.2 Content-Type与MIME校验:只信Content-Type等于裸奔

有的团队知道要校验类型,但只检查了Content-Type,比如要求必须是image/jpeg。攻击者的回应是改一个字段:把Content-Type: application/x-php改成image/png,文件内容照样是PHP代码。这种校验属于典型的“表面安全”,因为Content-Type是客户端说了算,没有任何加密或签名,服务端不应当把它当信任凭据。

正确做法是服务端读取文件字节内容做MIME探测。但只做MIME探测也远远不够,因为很多类型可以伪造。比如一个文件的内容前几百字节是合法的PNG图片头,后面却跟着PHP代码,finfo依然可能把它识别为image/png。MIME探测只是校验链条的一环,不是终点。

2.3 扩展名绕过:黑名单思路的经典翻车现场

如果后端知道要校验扩展名,常见的实现是维护一个黑名单,比如.php、.asp、.jsp。黑名单思路先天不足,因为可执行扩展名太多了。拿PHP环境来说,不仅有.php,还有.phtml、.php3、.php4、.php5、.pht,甚至一个精心构造的.phar也能在某些场景触发包含执行。

黑名单之外还有各种变形技巧:大小写混合(.pHp)、双写(.pphphp)、加空格(.php)、加点(.php.)、末尾换行;如果路径拼接时使用了文件名字符串,还可能配合%00截断字符(在旧版本环境中有效)。黑名单防御就像赌对方不知道冷门组合,而攻击者有大量时间遍历这些组合。所以安全社区早就不推荐纯黑名单,应该用白名单。白名单只需要列允许的类型,比如jpg、png、gif、pdf,其他一切后缀直接拒绝,没有模糊空间。

2.4 文件内容伪造与图片马:骗过了闻味道的程序

校验如果做到了服务端,很多人以为已经无敌了,但实际仍可绕过。程序员喜欢检查“文件头魔数”,以为读前几个字节足够了,但攻击者完全可以把一段脚本塞进一张合法图片的结尾,生成所谓的图片马。例如用测试工具构造一个文件:前几百字节是PNG头,中间是图片数据,文件尾部追加PHP脚本。如果服务器只会校验扩展名和MIME,甚至使用getimagesize()验证图片,它依然可能返回“合法图片”,但某些中间件或包含逻辑会执行尾部的脚本内容。

更彻底的防御是对上传后的图片做一次“二次渲染”,用图像处理库重新生成图片,彻底去掉附加数据。但二次渲染也有兼容性问题,需要针对不同图片格式测试。比如PNG的透明通道、GIF的动帧,处理不当会把正常图片搞坏。上线前一定要拉一批业务真实图片跑一遍回归,别让安全修复变成业务事故。

2.5 路径拼接漏洞:上传成了写马到任意目录

除了“能不能传”的问题,还有“传到哪”的问题。很多系统喜欢把文件名直接拼进路径,比如$upload_dir . $_FILES['file']['name']。一旦文件名中包含../,就可能发生目录穿越,把文件写到上级目录,甚至是Web根目录的可执行目录。这类漏洞有时候比扩展名校验更致命,因为即便系统设置了扩展名白名单,如果允许攻击者控制最终路径,仍可能覆盖已有文件或把恶意文件写到其他模块的解析范围。

我在漏洞审计时几乎把这些拼接逻辑当默认风险点,见到basename()用得不对、路径没有经过规范化,基本可以直接判定为高危。凡是需要把用户输入放到文件系统路径里的场景,都要先做路径标准化,确保解析后的绝对路径仍在允许的目录前缀之下。

3. 从一个授权测试场景,看文件上传漏洞的完整利用链

3.1 环境与约定

为了把前面说的技术点串起来,我用一个基于授权测试的虚构场景来还原利用链路。某后台系统有一个“公告附件上传”功能,只对管理员开放,目标站点为测试环境。需要明确的是:所有操作都在甲方书面授权的范围内进行,并严格控制影响,用完即还原。本节的目标不是教你攻击,而是让你理解漏洞形成的原因,以及后续修复手段能影响链路的哪些节点。

3.2 第一步:探测上传入口的校验逻辑

什么都不改,先传一个正常的图片,观察返回结果:保存的URL是什么?是否有独立域名?文件名是否被重命名?然后传一个纯文本文件,看看服务端报什么错。接着抓包,把文件名改成一个.php测试脚本,内容用任意字符串,观察返回是否成功。如果返回成功,说明后端根本没过滤扩展名。如果返回“类型不允许”,再逐步试探是否只校验了Content-Type——把它改成image/png再试一次。

通过这类小步递进,很快就能画出校验逻辑的边界。很多测试者上来就注入大Payload,反而容易触发WAF,什么都摸不清。建议先做逻辑探测,再做内容构造。这个阶段的信息很关键:它决定了后续是直接改名上传,还是需要构造复合文件绕过内容检测。

3.3 第二步:构造出能落地的Webshell文件

在授权测试环境下,我们构造一个轻量级探测样本,比如一段不包含任何破坏性命令的脚本:

<?php echo "upload-test-" . md5("probe");

这个脚本一旦被执行,页面会输出一串特殊的md5值,用于确认脚本落地并且被解析执行。文件名可以用test.php或通过各种变形绕过扩展名过滤,比如test.php5、test.phtml。在抓包工具里修改filename字段,同时把文件内容替换成上面的代码。如果服务端有黑名单过滤但没过滤.php5,这里就可能直接命中。如果目标环境的Web服务器存在历史解析漏洞,那么把文件名构造成test.jpg.php或test.php.jpg,可能会把文件按PHP解析,也能绕过常见的“仅图片”白名单校验。

3.4 第三步:利用路径与URL规则完成访问与执行

文件上传成功不等于漏洞可利用,关键要看返回的访问URL能不能触发执行。访问刚才上传文件的地址,如果在响应里看到了我们定义的md5标记,说明脚本已执行。如果只是下载或显示源代码,说明上传目录禁止脚本执行,另找路子。

测试中还有一种常见现象:文件存在但URL规则把.php以外的东西当静态文件返回,导致无法执行,这时就需要结合中间件解析差异、包含功能、或尝试通过目录穿越写到更外层目录。这一步能直观感受到“上传成功”和“代码执行”之间的差距,也解释了为什么防御方只要把执行环境断开,攻击面就会缩水一大半。

3.5 第四步:根据会话状态判断漏洞严重性

最后要看漏洞能走多远。如果目前登录的是管理员,而上传接口没有做单独的CSRF防护,就有可能被其他页面触发跨站请求伪造,诱导管理员上传恶意文件。如果上传目录与Web根目录同属一个虚拟主机,脚本执行权限和数据访问权限通常相同,风险立即升级为“拿下一台Web服务器”。

我在评估漏洞级别时,会重点关注三点:是否可访问执行、是否落在Web根目录、当前会话是否具备高权限。三者叠加,基本就是最高危状态。反过来,如果上传目录独立、禁止脚本解析,就算文件传成功了,攻击者也很难直接获得RCE,威胁等级会明显下降。这也是后文服务端加固的重要依据。

4. 代码层封堵:一套能挡住大多数攻击的上传实现

4.1 校验顺序:先做什么后做什么

安全的文件上传实现不是一句“用白名单”那么简单,需要明确校验顺序。我的建议是:先做请求级校验(大小、数量、会话权限),再做文件内容校验(扩展名、MIME、魔数、图片结构),最后才是文件落盘与响应。每一步校验失败都要直接终止,不能“警告但不阻断”。

顺序也很重要:先做成本最低的检查,避免后面昂贵的处理被无效请求打满。但安全策略上,最终有效性取决于最严格的那一步,而不是最先执行的那一步。所以哪怕前端已经限制过了,服务端也必须重新校验一遍,而且不能信任前端传过来的任何校验结果。

4.2 扩展名白名单与随机重命名

防御核心是扩展名白名单。业务允许什么扩展名,就只认什么扩展名,其他一律拒绝。常见的安全做法是:从文件原始名称中取出扩展名,小写化后与白名单比对;然后重新生成一个完全随机的文件名,丢弃原始文件名。这样既能防黑名单绕过,又能避免目录穿越——因为新文件名里只有字母数字。

随机重命名还有额外好处:攻击者无法预测上传文件的最终URL,降低了二次利用的可能。下面的示例函数演示了这种思路:

function store_upload(array $file, string $destDir): string { // 1. 扩展名白名单 $allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf']; $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, $allowedExts, true)) { throw new RuntimeException('不支持的扩展名'); } // 2. 随机重命名,仅保留白名单扩展名 $newName = bin2hex(random_bytes(16)) . '.' . $ext; // 3. 保存前再次确认 if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) { throw new RuntimeException('非法上传'); } if (!move_uploaded_file($file['tmp_name'], $destDir . DIRECTORY_SEPARATOR . $newName)) { throw new RuntimeException('文件保存失败'); } return $newName; }

这段代码本身并不能防住所有情况,但把“原始文件名不可信”落在了代码里。实际项目通常还要把$destDir固定在配置目录,并确保用户不能控制。

4.3 MIME、文件头与图片有效性检查

扩展名白名单之外,还要做内容层校验。这里推荐组合拳:先通过文件信息库(如PHP的finfo)读取真实MIME,再对图片类文件调用getimagesize()校验图片结构。对严格场景,还要用图像库对图片进行二次渲染,输出一张全新图片,丢弃原始文件中的所有附加字节。

二次渲染的取舍是安全性最高,但可能导致GIF动图失帧、PNG透明信息丢失,所以上线前需要跑一批业务图片回归。如果业务允许上传PDF、Office文档,建议用专门的文件解析库验证文档结构,至少不要只信任扩展名。我见过一个项目为了支持SVG而放松了校验,结果攻击者上传SVG文件,里面直接内嵌脚本,在前端执行了存储型XSS。所以凡是支持的可能解析脚本的格式,都要谨慎纳入。

4.4 存储与响应环节的隔离设计

代码层还有一个容易被忽略的点:上传文件的返回URL和存储目录要隔离。最理想的方案是使用独立对象存储,上传凭证用临时签名URL,Web服务器不直接保存文件。如果必须本地存储,至少要做到三点:上传目录放在Web根目录之外或独立站点;响应文件时不直接拼接路径,而是交给静态服务只读返回;所有文件通过一个受控的下载接口读取,并禁止目录列表访问。

这样即使某个扩展名漏进系统,也无法在Web根目录被当作脚本运行。隔离设计的核心思路是“可上传不等于可执行,可访问不等于可解析”,把文件存储能力和代码执行能力彻底拆开。

4.5 代码示例

把上面的要素合并成一个更完整的入口函数:

class UploadManager { private array $allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf']; private array $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf']; public function save(array $file, string $destDir): string { $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, $this->allowedExts, true)) { throw new RuntimeException('扩展名不允许'); } $mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']); if (!in_array($mime, $this->allowedMimes, true)) { throw new RuntimeException('文件类型不允许'); } if (str_starts_with($mime, 'image/')) { $info = @getimagesize($file['tmp_name']); if ($info === false) { throw new RuntimeException('不是有效图片'); } // 二次渲染:用 GD/Imagick 重新生成图片,去掉附加载荷 $this->reRenderImage($file['tmp_name'], $mime); } if (!is_dir($destDir)) { if (!mkdir($destDir, 0755, true) && !is_dir($destDir)) { throw new RuntimeException('目录不可写'); } } $newName = bin2hex(random_bytes(16)) . '.' . $ext; move_uploaded_file($file['tmp_name'], $destDir . DIRECTORY_SEPARATOR . $newName); return $newName; } }

这段代码可以作为基线,不同语言和框架只需翻译逻辑,不需要照搬顺序。重要的是每一层都独立发挥作用,暴露给用户的只有随机文件名。注意,reRenderImage需要根据实际情况实现,对不支持的业务图片类型要给出清晰的报错提示,而不是默默放行。

5. 服务端加固:就算业务代码有漏洞,后防线也要能兜底

5.1 上传目录禁止执行脚本

纵深防御的关键一招是让“上传目录”和“脚本执行”绝缘。以常见的Nginx配置为例,如果上传目录是/data/www/site/uploads/,可以在该路径下关闭脚本解析,或者只允许静态扩展名访问:

location ^~ /uploads/ { location ~ \.(php|phtml|php5|phar)$ { return 404; } try_files $uri =404; }

配置写成这样有个前提:uploads下的文件确实不需要执行PHP。如果不是独立目录,而是和入口文件混在一起,就需要在Web框架路由层做“静态文件直接返回”的规则,并用正则排除可执行后缀。很多开发者不理解为什么“明明做了白名单还要禁止解析”,其实这是两手准备:万一白名单绕过,第二道防线还能挡住。

5.2 对象存储与独立域名部署

有条件的话,把上传文件挪到对象存储,Web服务器不再落盘。对象存储天然不解析脚本,静态返回文件,配合访问鉴权还能精细化控制。注意上传过程要用“带有效期临时凭证”的方式,而不是把存储桶的永久密钥写在后端代码里。

即便必须使用本地磁盘,也建议把上传文件放在独立域名或独立端口下,与主业务域隔离。这样做在浏览器侧也能减少Cookie共享面,降低上传文件中的恶意脚本读取业务Cookie的可能。从攻击面来看,上传URL越独立,数据流越干净,后续审计日志时也更容易定位问题。

5.3 权限、大小与并发限制

服务器配置不要忽视资源类限制。上传目录的属主是Web运行用户,目录权限给到755,文件权限给到644,去掉不必要的可执行位。虽然Linux的noexec不能直接阻止PHP被解析,但去掉执行位可以降低部分二进制类后门的直接落地。业务上还要限制单文件大小、单次请求大小、上传频率、并发连接数,防止用超大文件打满磁盘或用并发请求拖垮应用。

我见过被上传漏洞打垮的站,不是被RCE,而是攻击者反复提交大附件,把磁盘和带宽全吃光。资源限制也属于“处理”安全的一部分。前端限制最大10MB,服务端就要同样限制,而且要更严格,不能只依赖前端。

5.4 日志与监控:给事后处置留线索

上传日志要记全:时间、来源IP、原始文件名、最终保存名、大小、用户标识、结果状态。但注意不要记录文件内容,避免日志膨胀和敏感数据泄漏。监控方面,对上传后访问可执行文件的行为做告警,一旦发现.php文件被当作静态资源请求、且文件出现在上传目录,立即生成高危事件。

合理的日志和监控,能把应急排查从“翻遍服务器找马”变成“按图索骥看日志”,价值非常大。如果没有日志,一次入侵的响应时间可能要拉长数倍,攻击者早就在内网站稳脚跟了。

6. 已经中招后的应急处理:止损、排查、修复

6.1 第一步:先断外联、隔离服务器

发现上传漏洞被利用时,第一件事不是急着删文件,而是阻止攻击者继续操作。先判断服务器是否存在对外连接,是否有异常的反弹连接或计划任务;如果情况不明,直接把受影响主机从业务流量中摘除,保留现场。摘除方式可以采用临时修改防火墙策略,不要在摘除前就重启进程,否则内存里的恶意进程可能被清除,但也不利于取证。

隔离后的机器还要评估:这台机器是否能访问数据库、内网其他主机?如果有,优先切断这些链路,防止横向扩散。具体操作上,除了网络隔离,还要检查攻击者是否创建了新的SSH隧道或代理进程,如果有,立刻停止相关进程并记录命令行、父进程关系。

6.2 第二步:快速定位Webshell样本

隔离之后开始找样本。Web后门文件的特点是最近修改时间、非常规文件名、非业务功能新增。用类似于下面的查找命令可以快速圈定可疑文件:

find /data/www/ -type f \( -name "*.php" -o -name "*.php5" -o -name "*.phtml" \) -mtime -30 -print

配合按时间排序,把最近30天变动的脚本文件拉出来,逐一人工检查。还要检查上传目录下有没有比业务文件更“新”的图片文件,因为图片马往往会被伪装成正常图片名。更加高效的做法是直接把整个网站源码和文件清单拉出来,和发布基线对比。如果没有基线,就先按扩展名、时间去筛,再检查高可疑文件内容。

搜到恶意文件后不要急着删,先备份一份并记录文件属性、哈希值。这些信息在后面做溯源和痕迹保留时非常重要,删了可能就没了。清理后门不是一删了之,还要确认是否还有其他变种或隐藏在非标准位置的文件。

6.3 第三步:翻日志还原攻击链

样本有了,下一步要还原攻击者是怎么进来的。查看Web访问日志,找到第一次上传该恶意文件的请求,重点搜索upload、attach、filename=等关键字,并关联同一个来源IP。日志里可能有大量正常请求,建议先按返回码过滤,把上传成功(如200、302)的请求拉出来;随后看该IP在目标时间前后的访问记录,判断是否还上传了其他文件、访问了哪些URL、是否调用了系统命令。

有些攻击者会清除日志,所以最好能配合安全设备流量日志、系统登录日志一起看。这一步的目标是把“上传样本”和“攻击者动作”对应上,避免只处理了一个文件,而实际上他还留了其他后门。日志分析要细致,不能只看当天记录,还要向前翻一周甚至一个月,因为攻击者可能早在正式利用前就做了多次尝试。

6.4 第四步:修复漏洞并清理持久化

确认根因后,先按第4章和第5章的方法把上传逻辑加固。如果环境已经不可信,不要只是删马之后继续用,建议重新部署应用目录,并重置数据库账号密码、后台管理员密码,撤销所有可能泄露的Token。接下来检查系统层面持久化:计划任务、systemd服务、登录脚本、SSH授权密钥、启动目录,任何一个都可能被攻击者用来维持权限。

清理持久化需要对比系统变更,手动确认每一项,而不是盲目删除。比如某个陌生定时任务被注释了,如果不是你手动加的,就要查清楚它的来源。我遇到过几次“删马后第二天又复活”的情况,最后都是因为漏了一个计划任务或启动脚本,攻击者靠它把文件重新拉下来。

6.5 一个可复用的应急检查单

下面是一张在多个项目里用过的检查单模板,按顺序执行,基本能覆盖大多数上传漏洞入侵场景:

顺序检查内容常用方法预期结果
1阻断受影响主机对外连接防火墙/云策略隔离无法外传数据
2定位WebShell样本按时间、扩展名搜索+基线对比找出全部可疑脚本
3恢复访问日志检查Web日志中上传入口找到首传时间与来源IP
4溯源攻击入口关联IP、UA、Payload确认漏洞点
5清理恶意文件与持久化手工核查计划任务、启动项、密钥系统恢复干净
6加固上传逻辑白名单+随机名+内容检测+禁止执行无法再次上传可执行文件
7全站密码/Token轮换管理员、数据库、API Key防Session劫持延续

这张表只是一个起点。实际处置中经常遇到不止一个入口,所以清理完关键样本后,至少要再观察一个完整的请求周期,确认没有新的反弹连接。如果条件允许,建议对受影响目录做一段时间的文件变更监控,确保没有第二波写入。

7. 我踩过的那些坑与最后想说的几条原则

7.1 三个让我印象深刻的教训

第一个教训:只校验Content-Type的接口,上线两周就被打穿。当时某个后台导入模块为了配合老前端,故意放了一个“宽松校验”的口子,结果被自动化扫描直接上传了可执行脚本,前后不到一天。后来我坚持所有上传接口必须走同一个安全组件,不允许为某个模块单独写绕过逻辑。这个问题在团队协作里很容易被忽视,一个“临时兼容”往往就成了永久隐患。

第二个教训:黑名单过滤得再长也能漏。我有一次把几十种脚本后缀都列进黑名单,自以为天衣无缝,结果攻击者上传了一个.html文件,配合目标环境的解析规则,直接在浏览器里执行了脚本逻辑,造成了一次存储型XSS风波。从那以后我再也不给黑名单加戏,一律白名单。黑名单的维护成本太高,而且总有人比你更懂冷门后缀。

第三个教训:清理木马时漏掉了同目录下的“图片马”。当时只删了页面里出现的PHP文件,完全没注意上传目录里多了一张看起来“正常”的头像图。后面复查日志才发现,攻击者早就通过这张图片马拿回了办法。现在我做应急的时候,凡是上传目录里近期的图片类文件,都要从头到尾扫描一遍字符串,并且和业务原始图片哈希比对。那次之后我对“完整清理”四个字有了完全不同的理解。

7.2 一条主线:让每一层都独立生效

文件上传漏洞的防范,本质上不是加一个函数、配一条规则的事,而是一条防御链路。用户输入永不信任、白名单取代黑名单、随机命名取代用户文件、目录与执行隔离、日志与监控闭环、应急有预案。如果只做其中一两步,也许能挡住入门脚本,但挡不住针对性测试。

我个人的原则是:把每一次防御加固都当作“如果这层无效,下一层依然能兜住”来设计。做到这个程度,再有人拿着常见绕过手法来测试,你会发现他和正常用户一样,什么东西都传不进去。安全不是某一次更新完就结束,而是每次面对新绕过手法后,把这条链路重新审视一遍。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进