ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Tornado cookie_secret 泄露利用与防御:从签名机制到安全加固

2026/9/16 23:40:07 拓冰建站 浏览量
Tornado cookie_secret 泄露利用与防御:从签名机制到安全加固 做安全也好做后端也好只要项目用到 Tornado就一定绕不开一个配置项cookie_secret。我在授权测试里碰到 Tornado 站点时相当一部分情况最终都能从源码、配置文件或者日志里把它翻出来。而一旦拿到这个值离拿到管理员登录态往往就只差一个脚本的距离。身边不少开发同学对它的理解还停留在一个随机字符串加上去就能用 set_secure_cookie但实际它牵扯到的面比想象中大得多。这篇文章准备把 Tornado 的 secure cookie 机制、cookie_secret泄露后如何被一步步利用、以及最后怎么防一次讲透。适合三类人看一是做渗透测试和红队的朋友二是写 Python 后端的开发三是负责应用上线的运维。内容不算深但都是实战里真正会用到的。1. Cookie_secret 在 Tornado 里的真实地位1.1 从一次最典型的越权漏洞说起先说一个我在授权测试里遇到过的典型案例。目标是一个内部管理系统登录之后用户 ID 被写进 cookie接口返回数据时直接用这个 ID 去数据库查用户信息。看起来问题不大但问题是它用的是普通set_cookie不是set_secure_cookie。我随手把 cookie 里的user_id从10086改成1刷新页面直接变成了管理员账号。整个过程中我没有破解任何密码也没触发任何业务告警就是改了一个 cookie 值。这类漏洞在自研系统里非常多所以 Tornado 官方提供了set_secure_cookie和get_secure_cookie这对组合目的就是解决cookie 能不能被客户端随意篡改的问题。而这个方案的核心密钥就是cookie_secret。很多人以为这只是给 cookie 加了一个签名头实际上它牵动的安全机制有三层后面我会逐个拆开。1.2 Secure Cookie 不是加密 Cookie先纠正一个最常见的误解Tornado 的set_secure_cookie并不是加密 cookie它只是对 cookie 值做了签名防止篡改。签名和加密是两码事签名保证的是这个值没有被改过加密保证的是这个值别人看不懂。Tornado 对 value 的处理是先用 base64 编码再拼上时间戳最后用cookie_secret计算 HMAC 签名。base64 只是一个编码不是加密任何人拿到 cookie 都可以解码看到原始内容。所以你在get_secure_cookie里存的如果是{username: admin, role: admin}这种 JSON用户把 cookie 复制出来用工具 base64 解一下内容一目了然。真正防止他改的是后面那串签名。理解这一点对后续利用很重要如果cookie_secret泄露攻击者不仅能篡改 cookie还能直接解码看到业务逻辑里到底往 cookie 里塞了什么数据伪造起来会更有针对性。1.3 Cookie_secret 实际管着三件套我见过不少人对cookie_secret的理解只停留在给 set_secure_cookie 用但它在 Tornado 里至少管着三样东西set_secure_cookie/get_secure_cookie所有安全 cookie 的签名与校验。_xsrf防护Tornado 的 XSRF token 本质也是一个签名 cookie底层同样依赖cookie_secret。基于 secure cookie 自研的 session 方案很多项目会自己封装 session但底层存储或票据校验仍然回到这个密钥上。换句话说cookie_secret一旦泄露攻击者可以做的不只是伪造一个user_id。他可以伪造整个会话体系里的关键票据包括绕过 XSRF 防护伪造登录态甚至在某些业务自定义的权限标识里直接把自己变成管理员。这也是为什么这篇文章把它定义成安全机制里的总钥匙。2. 拆开签名看机制Tornado 是怎么用 secret 的2.1 一段 signed cookie 的内部结构想要把利用做扎实得先看懂 Tornado 生成的 signed cookie 长什么样。我这里用 Tornado 6.x 举例在代码里调用from tornado.web import create_signed_value secret my_secret_key signed create_signed_value(secret, user_id, 1024) print(signed)输出大致是下面这种格式b2|1711123456|MTAyNA|\x9a\x88\xb2\xf4...用|分隔后一共四段第一段2签名版本号6.x 默认是 2早期版本可能是 1。第二段1711123456生成 cookie 时的时间戳。第三段MTAyNA原始 value1024的 base64 编码。第四段HMAC 签名值原始二进制长度取决于哈希算法v2 用 SHA256 所以是 32 字节。整个值在 HTTP 传输过程中因为包含不可见字符会经过 cookie 规则的 URL 编码处理但你用 DevTools 或抓包软件查看时核心结构还是这四段。2.2 签名与校验的完整逻辑Tornado 的签名不是简单地把 secret 和 value 拼起来算一个 MD5而是使用标准的 HMAC 算法。v2 版本的处理逻辑大致是取当前时间戳int(time.time())。把 value 做标准 base64 编码。构造一个字符串包含 cookie 名称、key_version、base64 后的 value、时间戳用|连接。用cookie_secret作为 HMAC 的 key对上面这个字符串计算 HMAC-SHA256。把版本号、时间戳、base64 value、签名按顺序用|拼起来返回。校验时流程刚好反过来先把 cookie 值按|拆成四段。根据版本号重新计算期望签名。用hmac.compare_digest做恒定时间比对防止时序攻击。签名通过后再看时间戳和当前时间差是否超过max_age_days。这里面有一个非常关键的点签名输入覆盖了 cookie 名称。也就是说user_id的 signed cookie 不能直接拿来冒充admin_id的 cookie因为两者 name 不同重新计算的签名自然对不上。这看起来是个细节但实际写利用脚本时如果忽略了它伪造出来的 cookie 怎么验都不通过。2.3 版本差异与密钥轮换为什么要用 dictTornado 早期版本的签名算法和现在不太一样最老的一版使用 HMAC-SHA1而且签名输入不包含 name 字段后来为了防 cookie 名称替换攻击才补齐。所以遇到老项目时离线爆破脚本要兼容两套签名规则。另一个很多人没注意的功能是cookie_secret可以配置成字典settings { cookie_secret: { 1: old_secret, 2: current_secret, } }配置成字典之后创建 cookie 时默认用最大的 key 对应的 secret 来签名校验时会按 key 逐个尝试。这样做的好处是密钥轮换期间旧 cookie 不会被立刻全部踢下线。这个机制本身设计得不错但也意味着如果你在配置里看到这种结构爆破时要分别用两个 secret 都试一遍不要只试最后一个。3. 漏洞面分析Cookie_secret 是怎么被泄露的3.1 代码仓库硬编码占比最高我在项目里排查这种问题第一步永远不是看服务器配置而是直接翻代码仓库。很多 Tornado 项目的settings.py或config.py里会有一行cookie_secret 8fd9c3f2a4b6...这行看起来没问题但一旦代码被推到公开仓库、内部仓库权限没管好、或者某个员工把项目当样例发到了公开技术社区整个安全体系就等于白纸一张。比这更常见的是.git历史记录里留着旧版本配置哪怕后来改成从环境变量读取历史提交里的密钥依然还能用。我自己的排查习惯是克隆仓库后直接跑一遍代码历史搜索关键词就是cookie_secret、secret_key、settings。实践下来命中率非常高很多时候项目作者会觉得我后来删掉了就没事但 git 历史不会撒谎。3.2 配置文件、日志与调试接口泄露即使代码里没有硬编码配置文件的管控不到位也会出问题。常见场景包括docker-compose.yml、K8s 的 ConfigMap、.env文件直接写明文并且跟随镜像或部署包分发。应用启动或报错时把完整配置打印到日志里日志系统又接入了 Elasticsearch、Sentry 这类平台搜索权限没有严格控制。测试环境通过调试接口暴露配置比如 Flask-Debug 类似的调试页面在 Tornado 项目里有些人会自己加一个/config之类的内部接口忘了鉴权就上线了。还有一种是环境复用测试环境、预发环境和生产环境共用同一个cookie_secret。攻击者如果拿下了抵抗力最弱的测试环境就能拿着同一个密钥去伪造生产环境的登录态。这在大型公司里出现频率不低因为测试环境往往没有严格的安全防护但配置却和生产保持一致。3.3 弱 secret 与离线爆破就算代码和配置都没有泄露还有一个容易翻车的地方cookie_secret本身太弱。签名算法是公开的cookie 样例是公开的每个访客都能拿到自己的 cookie所以攻击者完全可以离线爆破。爆破思路很简单拿一条已知的 signed cookie从常见密钥字典里逐个尝试看哪个密钥能算出相同的签名。整个过程不需要向目标服务器发任何请求目标不会有任何访问日志WAF 也拦不到因为攻击流量根本没有到达对方服务器。有人可能觉得我的密钥是随机字符串不在字典里但实战中我见过不少cookie_secret secret、123456、qwerty、项目名加年份这种组合。只要字典够大、规则覆盖够广弱 secret 基本就是白送。这里顺带说一句最近几年圈子里讨论比较多的 Java 反序列化工具、Redis 未授权访问利用、SSRF 利用的本质很多都是先拿到一台机器的文件读取或命令执行能力然后把配置里的各种密钥一锅端。Tornado 的cookie_secret经常就是这口锅里最值钱的那块肉。4. 授权环境下的利用实战4.1 拿到一条 signed cookie离线爆破 secret先强调一遍下面的操作只建议在你自己搭建的靶场、或者已经获得书面授权的目标上做。未授权测试是违法行为这不是场面话是做这行必须守的底线。利用流程的第一步是获取一条已知的 signed cookie。访问目标任意页面从浏览器 DevTools 的 Application 面板里就能看到_xsrf或者其他 secure cookie 的值复制下来。然后在本地写一个爆破脚本from tornado.web import decode_signed_value cookie_name _xsrf signed_cookie 2|1711123456|MTAyNA|... wordlist [secret, 123456, qwerty, admin, tornado, test] for guess in wordlist: result decode_signed_value(guess, cookie_name, signed_cookie, max_age_days999) if result is not None: print([] Found cookie_secret:, guess) break else: print([-] Not in dictionary, try bigger wordlist or fuzzing rules)这段代码的关键是利用 Tornado 自己的校验函数decode_signed_value来判断密钥是否正确。如果猜的 secret 不对验签失败返回None如果正确就能解出原始 value。字典爆破时可以先用常见弱密钥规则生成一批候选再用rockyou这类大字典跑一轮速度依然很快。4.2 确定 secret 后伪造任意登录态拿到cookie_secret之后伪造任意 secure cookie 就是水到渠成的事。先看目标业务代码从哪里取用户身份比如class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): user self.get_secure_cookie(user_id) if user: return json.loads(user) return None这种写法很常见登录成功后把用户信息 JSON 序列化后塞进 secure cookie。攻击者现在可以用同样的 secret 伪造import json from tornado.web import create_signed_value secret found_secret_here payload {user_id: 1, username: admin, role: admin} signed create_signed_value(secret, user_id, json.dumps(payload)) print(signed)如果你不清楚具体字段名也不难办。把你自己的正常 cookie 拿过来 base64 解码一下就能看到业务往里面存了什么结构照葫芦画瓢改字段就行。这就是我前面强调secure cookie 不等于加密的原因攻击者能看到明文内容伪造时就特别有针对性。4.3 同步伪造 _xsrf 完成 POST 操作很多人尝试到这个步骤会踩一个坑伪造完 cookie刷新页面发现 GET 请求确实能以管理员身份访问了但一提交 POST 表单服务器返回 403。原因就是 Tornado 开启了 XSRF 防护POST 请求必须同时带上合法的_xsrftoken。由于_xsrf本身也是用cookie_secret签名的攻击者可以顺手再生成一个from tornado.web import create_signed_value secret found_secret_here fake_xsrf create_signed_value(secret, _xsrf, attacker_generated_token) print(fake_xsrf)攻击者需要把这个值同时设置到 cookie 和请求参数里。有两种做法一是手动把生成的值写入浏览器 DevTools 的 Cookie 面板然后在页面表单里带上对应的_xsrf参数二是用脚本发送请求时把 cookie 和X-XSRFToken头一起设置。这样整个 POST 链路就能完整跑通。这一步往往被忽略但在真实业务里非常关键因为真正能改变数据、创建账号、修改权限的接口基本都是 POST。4.4 利用链条总结与授权边界提醒把完整利用链条串起来看第一步从源码、配置、日志或弱密钥爆破中获取cookie_secret。第二步从浏览器复制一条 signed cookie确认当前版本和签名规则。第三步用已知 secret 调用create_signed_value伪造身份相关 cookie。第四步同步伪造_xsrfcookie绕过 POST 防护执行管理员操作。整个链条的核心就一个词cookie_secret。它一旦失守后续所有步骤都只是脚本活。再提醒一次这套方法请严格用在本地靶场和授权测试环境中。安全测试的目的是帮助自己或客户发现问题、修复问题而不是制造问题。如果你发现某个系统存在这类风险正确的做法是走正规漏洞报告流程而不是进一步深入。5. 常见问题与排查技巧实录5.1 签名拼接总是不对问题出在哪自己写脚本模拟 Tornado 签名的同学十有八九会卡在签名拼接上。常见问题有三个忘了把 cookie 名称参与签名导致生成的 cookie 验签失败。顺序拼错HMAC 输入里 name、value、timestamp 的顺序是有讲究的调换顺序就是完全不同的签名。时间戳精度问题Tornado 用的是秒级时间戳不是毫秒别传成time.time()的浮点原值。最省事的做法就是直接用from tornado.web import create_signed_value和decode_signed_value不要去重复造轮子。除非你在做深入的算法分析否则用官方公开函数既可靠又省时间。5.2 浏览器手动改 cookie 不生效利用脚本里生成的 signed cookie 是 bytes包含不可见字符直接复制粘贴到浏览器 Cookie 面板往往会失效。这是因为 HTTP Cookie 在传输时有一套编码规则而 DevTools 里手动添加时不会自动帮你处理。遇到这种情况用 Python 对 cookie 值做一次 URL 编码再写入from urllib.parse import quote signed create_signed_value(secret, user_id, 1024) encoded quote(signed.decode(latin-1)) print(encoded)把encoded的值写进浏览器 Cookie 面板一般就能生效。另外还要确保写入时 path 和 domain 和原 cookie 一致否则浏览器可能不会把它发给目标接口。5.3 怎么判断线上 secret 是不是已经泄露如果你负责的系统已经上了线想判断cookie_secret是否泄露有几个可操作的排查手段。第一搜代码仓库全部历史不只是当前代码重点看.git里有没有出现过明文密钥。如果出现过无论现在删没删都视为已泄露处理。第二检查日志平台和错误上报系统的访问权限搜关键词cookie_secret、settings看看有没有完整配置被打进日志的记录。第三观察异常登录。如果日志里出现多个不同 IP 使用同一个 cookie 访问账号或者存在异常时间段的管理员操作要立刻提高警惕。还有一个进阶技巧可以故意把一个蜜罐密钥部署到预发环境同时保留旧密钥的校验能力然后观察日志里有没有使用旧密钥签名的请求。真实用户的 cookie 不可能用旧版本密钥签出来一旦出现就说明有人在离线伪造请求这是个提前发现泄露的好办法。5.4 常见问题速查表现象可能原因解决思路get_secure_cookie 返回 Nonesecret 不一致或签名版本不兼容检查配置是否从环境变量正确加载确认 Tornado 版本伪造 cookie 浏览器不生效值未做 URL 编码path/domain 不对使用 quote 编码核对 cookie 作用范围POST 请求 403缺少合法 _xsrf token同步生成并设置 _xsrf cookie 和请求参数线上用户被踢下线修改了 secret 且未做版本化轮换使用 dict 形式的 cookie_secret 保留旧 key 过渡新老版本 cookie 都验不过项目升级后签名算法变更检查版本号兼容 v1/v2 两套签名逻辑6. 防御加固与上线自检清单6.1 密钥生成与管理别再用 secret 当密钥cookie_secret的生成不能靠随手敲键盘建议用密码学安全的随机源openssl rand -hex 32或者用 Pythonimport secrets print(secrets.token_urlsafe(64))生成之后不要写进代码仓库。放在环境变量、K8s Secret、Vault 这类专门的密钥管理服务里。至少要做到代码仓库只保留从环境变量读取密钥的代码逻辑不允许出现任何明文密钥。如果系统已经在线上运行且怀疑密钥泄露不要直接替换先用字典形式做平滑轮换cookie_secret: { 1: old_secret, 2: new_secret, }等旧 cookie 自然过期之后再把旧 key 从配置里移除。这个流程可以让线上用户无感过渡不会出现大规模强制下线。6.2 应用侧与基础设施侧的加固动作应用代码层面有几个点值得自查所有使用get_secure_cookie解出来的值仍然要当作不可信输入处理。签名只保证它没被改过不保证它一定合理业务层还要做权限校验。不要在日志里打印完整配置或 cookie 原始值可以在日志脱敏组件里把 secret 类字段统一过滤掉。测试环境和生产环境严格隔离禁止共享cookie_secret。测试环境防护弱一旦被拿到密钥生产环境也就跟着失守。基础设施层面建议把源码仓库的权限、日志平台的权限、配置中心的权限都纳入定期审计范围。很多cookie_secret泄露不是 Web 漏洞本身导致的而是开发机中毒、代码仓库权限过大、日志平台匿名可查这类外围问题。6.3 上线前 10 分钟自检清单我每次负责的项目上线前都会过一次这个清单代码仓库里搜索cookie_secret、secret_key、password关键词确认没有硬编码。检查.git历史确保历史版本里也没有泄露过密钥记录。确认cookie_secret长度大于 32 字节并且是用随机源生成的。确认生产环境和测试环境使用不同的密钥。确认错误日志、异常上报、调试接口都不会输出密钥相关的配置。如果用到了字典形式的 secret确认新旧 key 的过渡时间和清理节点已经规划好。这个清单花不了十分钟但能挡住大多数因为配置交付不规范导致的事故。我个人这几年做安全排查最深的体会是Tornado 本身的签名机制设计得相当稳绝大多数出问题的场景都不是框架漏洞而是密钥管理上的松懈。cookie_secret这种全局密钥一旦泄露影响面往往超出预期不只是登录态还包括 XSRF 防护和整个自建安全体系。所以别嫌麻烦密钥该轮换就轮换代码该扫描就扫描权限该收就收。最后再分享一个小技巧定期在代码仓库历史里搜一遍cookie_secret关键词看起来很简单但命中过时密钥的概率比你想象中高得多每次搜完我都能从这个动作里发现一两个本该早就处理掉的风险点。