某天下午,任意安全工程师老周收到一条告警:内网某台业务服务器,文件有人通过一个下载接口,下载修复把`/etc/shadow`文件给拉走了。漏洞起初他以为是任意误报,查了下访问日志,文件那个请求长这样:`download?下载修复filename=../../../../etc/shadow`。路径穿越,漏洞干净利落。任意老周说,文件那一刻他后背发凉——这个下载功能上线三年了,下载修复没有任何人检查过它的漏洞参数。
任意文件下载漏洞,任意说白了就是文件程序把用户传入的文件路径直接拼进读取操作里,没做任何校验。下载修复攻击者用`../`往上一层层跳出目录,就能读到服务器上任意文件。轻则泄露配置文件里的数据库密码,重则把`/root/.ssh/id_rsa`私钥搞到手,直接登机。更狠的还能读`/proc/self/environ`拿环境变量,或者配合日志注入写个webshell。
修复这事儿,第一反应是过滤`../`。但别急着高兴,很多团队就是这么被绕过的。攻击者可以URL编码一次,`%2e%2e%2f`,或者二次编码,`%252e%252e%252f`,甚至用绝对路径`/etc/passwd`直接绕过相对路径检查。你拦了`..`,他改走`....//`,解析器一归一化,照样穿墙。所以,黑名单思路基本是给自己挖坑。
靠谱的做法是白名单。比如,下载功能本来只服务于某个特定目录下的报告文件,那就硬性规定:用户传入的只是一个文件名,而不是路径。程序内部固定拼接一个绝对路径前缀,比如`/var/data/reports/`,然后用`path.Join`或者`filepath.Clean`处理一下,再校验最终结果是否仍然以这个前缀开头。这一步很关键——光拼接不校验,等于没修。老周后来改代码,加了三行判断,测试组拿各种畸形payload打了一整天,一个都没穿过去。
另一个坑是符号链接。你以为校验了路径前缀就安全了,但服务器上某个目录里可能藏着一个软链接,指向`/etc`。攻击者先下载那个软链接,再顺着它读文件。所以,严谨点的做法是用`filepath.EvalSymlinks`把最终路径解析成真实路径,再跟前缀比对。有些系统还建议直接禁用下载目录里的符号链接,省得麻烦。
最后说个真实案例。某电商平台搞活动,运营上传了一批商品图片,技术图省事,用同一个下载接口做图片预览。结果测试人员发现,把文件名改成`../../../config/database.yml`,直接返回了数据库账号密码。修复时他们只加了`str_replace`过滤`../`,第二天就被双重编码绕过。后来改成白名单校验加真实路径比对,才消停。其实这类漏洞,根因不是技术复杂,而是开发者默认用户不会乱传参数——可互联网上什么人都有。
修完别急着收工。把下载接口的访问日志打开,看看有没有异常请求模式,比如大量`../`变体、超长路径、或者连续扫描不同文件名的行为。有条件的,加个文件类型白名单,再限制单IP单位时间内的下载次数。毕竟,漏洞修了是底线,防住后续的探测和利用,才算真的踏实。老周后来在团队周会上说了一句话:你以为你写的是下载功能,其实你开了一扇门,门后面有什么,你自己得先看清楚。