Nginx 缓存投毒:一个 Set-Cookie 引发的越权
L3HCTF2025 nginx缓存攻击
Nginx配置分析
从配置片段可以看出,Nginx对静态资源设置了缓存:
location ~.*\.(gif|jpg|jpeg|png|bmp|swf)$ {
proxy_ignore_headers Cache-Control Expires Vary Set-Cookie;
proxy_pass http://127.0.0.1:5000;
proxy_cache static;
proxy_cache_valid 200 302 30d;
}
这几个配置选项的致命组合是:
proxy_ignore_headers ... Set-Cookie:这是最关键的一条。它指示Nginx在缓存时忽略后端返回的Set-Cookie头。通常,一个需要登录的页面会包含Set-Cookie,而Nginx默认不会缓存带有Set-Cookie的响应。忽略它之后,Nginx就敢缓存这些本该私有的页面了。proxy_cache_valid 200 302 30d:告诉Nginx对于200和302状态码的响应缓存长达30天。location ~.\*\.(png)$:任何以.png结尾的URL请求都会匹配到这个规则。
攻击逻辑:如果一个需要认证才能访问的页面(如 /get_last_ip/z3r4y.png)的URL看起来像静态资源,那么第一个访问它的已认证用户的请求内容和响应,会被Nginx缓存下来。后续所有用户(包括未登录用户)再次访问这个完全相同的URL时,Nginx会直接返回缓存的副本,而不会再向后端Flask应用请求,从而绕过了Flask的登录检查。
🚀 完整做题流程
假设目标网站是 http://target.com
第一步:注册特殊用户名
- 动作:注册一个用户,用户名为
z3r4y.png。 - 目的:让用户信息页面的URL (
/get_last_ip/z3r4y.png) 匹配到Nginx的静态资源缓存规则。这是整个攻击的前提。
第二步:污染 last_ip 字段
- 动作:使用你刚注册的
z3r4y.png账号登录。然后访问网站任意页面,并在请求头中带上恶意的X-Forwarded-For,例如:
X-Forwarded-For: {{ 7*7 }}
- 原理:Flask应用的
@app.after_request钩子函数会读取request.remote_addr(在很多配置下,这个值来自X-Forwarded-For),并将其写入当前登录用户的last_ip字段。这样,你就将SSTI的载荷植入了数据库。
第三步:触发Nginx缓存
- 动作:在登录状态下,访问
http://target.com/get_last_ip/z3r4y.png。 - 原理:
- 由于你已登录,Flask会正常返回你的IP信息页面,内容中包含
{{ 7*7 }}。 - 由于这个URL以
.png结尾,Nginx会将这个响应(包含你的敏感会话数据和SSTI载荷)缓存到磁盘上。 - 此时,Nginx缓存了一个“已认证”的会话状态。
第四步:通过缓存绕过认证并执行SSTI
- 动作:
1. 退出登录(或换一个未登录的浏览器)。
2. 直接访问http://target.com/ip_detail/z3r4y.png。 - 原理:
/ip_detail路由会先去请求http://127.0.0.1/get_last_ip/z3r4y.png。- 这个请求匹配了Nginx的
.png缓存规则。 - Nginx发现缓存中已经有这个URL的响应了,于是直接返回缓存的、带有SSTI载荷的页面,而不会去检查请求者是否登录。
/ip_detail路由拿到了这个包含{{ 7*7 }}的响应,并将其放入render_template_string中渲染,从而执行了模板代码,最终页面上会显示出49。
第五步:利用SSTI获取权限
-
重复流程:因为缓存一旦建立就很难清除(有效期30天),所以你需要重新注册一个新用户(如
payload.png)来注入更复杂的Payload。 -
最终Payload:在第二步中,使用更强大的SSTI载荷来污染
last_ip,例如:
X-Forwarded-For: {{lipsum.__globals__['os'].popen('id').read()}}
- 然后重复第三、四步:用新用户名触发缓存并访问
/ip_detail,从而读取命令执行的结果。
总结
这道题的流程精髓在于:利用Nginx对“伪静态”资源的缓存机制,将动态、需认证的页面“冻结”成一个公开可访问的静态文件,从而绕过认证,并利用其中被污染的数据触发SSTI漏洞。