从CTF题剖析Tornado框架SSTI漏洞与cookie_secret窃取攻击链

发布时间:2026/8/2 8:20:32
从CTF题剖析Tornado框架SSTI漏洞与cookie_secret窃取攻击链
1. 从一道CTF题看Tornado框架的安全边界最近在复盘一些经典的Web安全挑战题发现2018年护网杯的easy_tornado这道题虽然过去几年了但其中涉及到的Tornado框架特性、模板注入SSTI的利用思路以及如何从框架内部配置信息泄露到最终获取cookie_secret的完整链条至今仍然非常有教学价值。这道题之所以经典是因为它没有停留在简单的“发现注入点-执行命令”的层面而是要求攻击者必须理解Tornado这个特定Web框架的运作机制并利用其内置对象和配置来完成攻击。很多刚接触SSTI的朋友可能知道{{7*7}}能返回49就是存在漏洞但面对一个真实的、带有防护的框架时往往就无从下手了。今天我们就以这道题为蓝本彻底拆解其中的两种核心解法并深入探讨Tornado框架在安全设计上的一些“边界”情况。题目通常提供一个简单的Web界面有三个链接比如/file?filename/flag.txtsignaturexxx、/file?filename/welcome.txtsignaturexxx和一个提示/error?msgError。它的核心目标是获取存储在服务器上的/flag.txt文件内容。而解题的关键就在于理解这些URL中signature参数的生成规则并最终拿到用于签名验证的cookie_secret。这个cookie_secret是Tornado框架用于安全签名Cookies和其他令牌的核心密钥一旦泄露就意味着整个应用的安全校验机制形同虚设。我们接下来的所有操作都将围绕如何“撬开”这个秘密展开。2. 信息搜集与漏洞入口点分析面对任何Web挑战第一步永远是信息搜集。访问题目给出的几个链接我们会发现一些规律。访问/file?filename/flag.txt可能会直接返回一个错误提示signature不正确。而访问/file?filename/welcome.txtsignaturexxxx签名正确时则会返回文件内容。同时/error?msgError这个页面其内容似乎会直接渲染我们传入的msg参数。这里就出现了第一个可疑点/error路由的处理逻辑。在Tornado框架中路由和请求处理器RequestHandler是绑定的。一个常见的、存在风险的操作是开发者直接将用户输入传递给了模板渲染函数比如self.render(‘error.html’, msguser_input)。如果这个user_input在这里就是msg参数没有被恰当地过滤或转义那么它就会被当作模板语法的一部分进行解析和执行这就导致了服务器端模板注入SSTI。为了验证这一点我们可以尝试一个最简单的SSTI探测载荷。访问/error?msg{{7*7}}。如果页面上显示的不是字符串“{{7*7}}”而是数字49那么几乎可以确定存在模板注入漏洞。因为模板引擎这里是Tornado自带的模板引擎执行了7*7这个表达式并输出了结果。这是我们的突破口。但是仅仅能执行7*7是远远不够的。我们的目标是cookie_secret它是一个存储在服务器内存中的配置项通常通过应用设置Application.settings来访问。在Tornado的模板上下文中有一个特殊的对象handler它指向当前处理请求的RequestHandler实例。而这个handler对象有一个settings属性这是一个字典包含了Tornado应用初始化时传入的所有配置。其中cookie_secret就存放在handler.settings中。所以接下来的攻击思路就很清晰了通过SSTI访问handler.settings来获取cookie_secret。我们可以构造这样的Payload/error?msg{{handler.settings}}。如果漏洞存在且上下文中有handler对象那么服务器返回的页面里就可能会直接打印出settings字典的全部内容其中就包含我们梦寐以求的cookie_secret。注意并非所有Tornado模板注入场景下handler对象都可用。这取决于模板是如何被渲染的。如果开发者使用了某些方式限制了模板的上下文handler可能不存在。但这道题的设计是经典的“教学场景”handler是可用的。在实际渗透测试中如果handler不可用就需要寻找其他内置对象或方法比如利用__import__、__builtins__等Python内置函数来逐步构造利用链那会复杂得多。3. 核心攻击链利用SSTI窃取cookie_secret当我们通过{{handler.settings}}成功获取到配置字典后接下来的步骤就是从中提取cookie_secret。返回的内容可能是一大串字典的字符串表示类似于{‘debug’: False, ‘cookie_secret’: ‘b’y\x85\xb8…\x9e\x1d\xd3’’, ‘login_url’: ‘/login’, …}。我们需要找到cookie_secret对应的那个长字符串。这个字符串通常是字节类型bytes经过repr()函数转换后的形式里面包含了很多十六进制或转义字符。拿到这个cookie_secret的原始字节串表示后我们需要理解它在题目签名机制中的作用。观察/file这个接口它需要两个参数filename和signature。其签名算法很可能是这样的signature md5(cookie_secret md5(filename))。这里存在两种常见的拼接方式一种是md5( cookie_secret md5(filename) )另一种是md5( md5(filename) cookie_secret )。具体是哪一种需要根据题目提示或通过测试来判断。在easy_tornado这道题中常见的实现是md5(cookie_secret md5(filename))。因此攻击流程如下获取cookie_secret的原始值通过SSTI得到handler.settings[‘cookie_secret’]假设其值为字节串b’\x12\x34…\xab’。计算目标文件的MD5计算/flag.txt的MD5哈希值得到一个32位的十六进制字符串例如‘md5_of_flag’。构造签名将cookie_secret的原始字节与步骤2得到的MD5字符串的字节形式进行拼接然后计算这个拼接后字节串的MD5值。这里有一个关键细节md5(filename)的结果是十六进制字符串在参与计算时需要将其转换为字节串。在Python中如果直接使用字符串需要调用.encode()方法。而cookie_secret本身已经是字节串。所以计算过程是import hashlib; signature hashlib.md5(cookie_secret_raw ‘md5_of_flag’.encode()).hexdigest()。发起请求使用计算出的signature访问/file?filename/flag.txtsignature计算出的签名。如果算法和拼接顺序正确服务器端的验证就会通过从而返回/flag.txt的文件内容也就是本题的Flag。这个链条清晰地展示了从前端输入点msg参数到后端核心配置cookie_secret的完整利用过程。它不仅仅是一个SSTI更是一次对应用内部状态和配置的“内部侦察”。下面我们用一段模拟的Python代码来演示这个过程的核心部分import hashlib import urllib.parse # 假设通过SSTI获取到的cookie_secret这里是示例值实际是bytes # 从handler.settings获取到的可能是 repr(b‘...‘) 的字符串形式需要安全转换 cookie_secret_repr “b’\\xf2\\x9e\\x0b…\\xe6’” # 警告在实际利用中直接eval(repr_string)是极度危险的仅用于CTF环境演示。 # 应使用 ast.literal_eval 或根据格式手动解析。 cookie_secret_raw eval(cookie_secret_repr) # 得到 bytes 对象 # 计算 filename 的 MD5 filename “/flag.txt” md5_filename hashlib.md5(filename.encode()).hexdigest() # 得到 hex str # 构造签名 md5(cookie_secret md5(filename)) signature hashlib.md5(cookie_secret_raw md5_filename.encode()).hexdigest() # 构造最终请求URL base_url “http://target.com/file” params {‘filename’: filename, ‘signature’: signature} final_url f“{base_url}?{urllib.parse.urlencode(params)}” print(f“请求URL: {final_url}”)4. 深度利用绕过直接读取的FileReader解法上面介绍的是最常见、最直观的解法。但easy_tornado这道题之所以有两种解法是因为它还存在另一个有趣的利用点。除了通过handler.settings直接读取配置我们还可以利用Tornado模板的另一个特性来读取文件包括可能包含cookie_secret的配置文件。在Tornado的模板语法中可以通过{% module … %}语句加载模块也可以通过{% include … %}包含模板文件。但更通用的是Python的SSTI允许我们调用对象的属性和方法。我们之前用了handler它本身有很多属性。其中handler是RequestHandler的子类实例而RequestHandler有一个initialize方法在初始化时被调用。但这里我们关注另一个点文件读取。如果handler.settings被过滤或无法直接访问我们是否可以找到其他路径来读取服务器上的文件呢比如/flag.txt我们读不到因为需要签名。但是服务器上很可能存在一个配置文件比如config.py或settings.py里面明文写着cookie_secret ‘…’。如果我们能读到这个文件问题同样解决了。在Python的SSTI中我们可以利用内置函数open来读取文件。但open函数在模板的沙箱环境中可能被禁用。这时我们需要找到已经在当前上下文中可用的、能够读取文件的对象。Tornado的RequestHandler有一个static_url方法用于生成静态文件URL这不直接用于读文件。但是我们可以寻找更底层的对象。一种思路是利用Python的__import__函数导入os模块然后执行命令。但很多CTF环境会限制命令执行。另一种更贴合本题的解法是利用tornado.template模块本身的Template类或者直接利用handler的_template_loaders属性。不过这里介绍一种更直接、在特定版本下可能生效的方法利用handler的__init__方法链追溯到其application属性再找到settings。但我们已经知道handler.settings是可行的。所谓的“第二种解法”其核心可能不在于技术原理完全不同而在于触发漏洞的入口点或利用的链式调用有所不同。例如题目可能对error页面的msg参数做了部分过滤阻止了{{handler.settings}}这样的直接访问。那么攻击者就需要构造一个更迂回的Payload。假设过滤了settings这个单词我们可以尝试用Python的属性访问链来绕过。比如handler对象有一个application属性指向当前的Application对象而Application对象也有settings属性。所以Payload可以变为{{handler.application.settings}}。这本质上访问的是同一个字典。再进一步如果我们想文件读取可以尝试{{open(‘/etc/passwd’).read()}}但这通常会被禁止。更隐蔽的方式是利用模板的{% include %}指令进行文件包含如果服务端错误地将用户输入拼接进了include的文件路径就可能造成任意文件读取。例如构造/error?msg{% include “/proc/self/environ” %}尝试读取环境变量看其中是否包含密钥信息。但这需要include功能存在且路径可控。实际上在easy_tornado最常见的第二种解法描述中往往是通过SSTI调用os.popen或subprocess来执行系统命令直接查找或读取包含cookie_secret的文件。例如{{__import__(“os”).popen(“cat /app/config.py”).read()}}或者如果知道cookie_secret通常定义在application.py或settings.py中{{__import__(“os”).popen(“grep -r ‘cookie_secret’ /app”).read()}}这种解法跳过了对Tornado框架内部对象handler的依赖转而利用更通用的Python代码执行能力。它适用于handler对象被从模板上下文移除但模板引擎本身执行限制不严的情况。两种解法的对比在于解法一主流利用框架特性handler.settings更精准、更直接依赖特定框架上下文。解法二通用利用Python代码执行__import__(‘os’).popen更通用、更暴力但更容易被部署的安全措施如禁用危险模块、沙箱拦截。在实际解题时如果第一种方法失效例如返回了空的settings或报错就应该立即尝试第二种更通用的代码执行路径。5. 签名算法的逆向与构造无论通过哪种方式拿到了cookie_secret最终都要回到签名验证这一步。我们之前假设了签名算法是md5(cookie_secret md5(filename))。但在实战中我们需要验证这个算法是否正确。题目通常会给出一个有效的例子比如访问/file?filename/welcome.txtsignature已知的正确签名能成功返回内容。我们可以利用这个例子进行逆向推导。已知量filename/welcome.txtsignature正确的签名已知。未知量cookie_secret我们现在已经拿到了以及算法细节拼接顺序、是否加盐、哈希次数等。验证方法用我们获取到的cookie_secret按照猜测的算法比如md5(secret md5(filename))计算/welcome.txt的签名看结果是否与题目给出的正确签名一致。如果不一致就需要调整算法假设。常见的变体包括md5(md5(filename) secret)md5(secret filename)不对filename先做md5hmac相关算法双重MD5md5(md5(secret filename))在Python中我们可以快速编写一个测试脚本import hashlib def calc_signature_v1(secret_bytes, filename): # 算法1: md5(secret md5(filename)) md5_f hashlib.md5(filename.encode()).hexdigest() data secret_bytes md5_f.encode() return hashlib.md5(data).hexdigest() def calc_signature_v2(secret_bytes, filename): # 算法2: md5(md5(filename) secret) md5_f hashlib.md5(filename.encode()).hexdigest() data md5_f.encode() secret_bytes return hashlib.md5(data).hexdigest() def calc_signature_v3(secret_bytes, filename): # 算法3: md5(secret filename) data secret_bytes filename.encode() return hashlib.md5(data).hexdigest() # 假设我们获取到的secret和已知的正确签名 cookie_secret_raw b‘...‘ # 你的secret字节串 known_filename “/welcome.txt” known_correct_signature “已知的正确签名” # 测试每种算法 for i, func in enumerate([calc_signature_v1, calc_signature_v2, calc_signature_v3], start1): sig func(cookie_secret_raw, known_filename) print(f“算法{i}计算结果: {sig}”) if sig known_correct_signature: print(f“ 匹配成功使用算法{i}”) break一旦确定了算法用它来计算/flag.txt的签名就水到渠成了。这个过程体现了渗透测试中的“已知明文攻击”思想利用一个已知的输入输出对来推导出系统使用的加密或哈希算法的具体细节。6. 实战中的细节、踩坑与防御思考在真正动手操作这道题或者类似漏洞时会遇到不少细节问题。这里分享几个常见的“坑”和注意事项坑1cookie_secret的格式处理通过SSTI{{handler.settings}}获取到的输出cookie_secret值很可能是一个Python字节串bytes的repr()形式比如b’\xca\xc7\xb2…’。你不能直接把这个字符串拿去和MD5结果拼接。必须将其转换回原始的字节对象。在CTF的隔离环境中有时可以冒险使用eval()但绝对不适用于任何正式或未知环境。更安全的做法是写一个小脚本解析这个字符串去除开头的b’和结尾的’然后对里面的每个\xXX序列进行字节解码。例如import ast secret_repr “b’\\xca\\xc7\\xb2…’” # 使用ast.literal_eval安全地计算字符串表示的Python字面量 secret_bytes ast.literal_eval(secret_repr)坑2MD5计算时的编码问题在Python中hashlib.md5()函数接受一个字节串bytes作为输入。当你计算一个字符串的MD5时需要调用.encode()方法如hashlib.md5(“/flag.txt”.encode()).hexdigest()。而在拼接cookie_secret字节串和md5(filename)十六进制字符串时必须确保两者都是字节串后再拼接。一个常见的错误是secret_bytes md5_f这里md5_f是字符串会导致类型错误。正确的做法是secret_bytes md5_f.encode()。坑3算法猜测与顺序正如前面提到的签名算法的拼接顺序至关重要。如果通过/welcome.txt验证失败不要轻易放弃尝试另一种顺序。有时题目还会在签名后附加一个号或者进行Base64编码需要仔细观察响应或题目描述。坑4SSTI的过滤与绕过在更接近实战的场景或难度更高的CTF题中{{和}}可能会被过滤。这时就需要尝试模板引擎的其他语法。Tornado模板也支持{% … %}语句和{# … #}注释。对于语句可以尝试{% print(handler.settings) %}。如果某些关键词被过滤可以考虑使用字符串拼接、编码、特性如__class__、__mro__等高级技巧来绕过这属于更深入的SSTI利用范畴。从防御角度思考 这道题几乎是一个完美的反面教材展示了几个关键的安全失误永远不要信任用户输入/error路由直接将msg参数传入模板渲染是SSTI的根本原因。应对所有渲染到模板的用户输入进行严格的过滤或转义。Tornado默认会对{{ }}中的变量进行HTML转义但如果你使用{% raw … %}或者直接嵌入了表达式危险就产生了。敏感配置不应泄露cookie_secret这样的密钥绝对不应该出现在任何可能被用户访问到的上下文里。通过handler.settings暴露全部配置是极其危险的。在生产环境中应避免将敏感配置传入模板上下文或者至少进行严格的过滤。签名算法需要足够强健题目中使用的md5(cookie_secret md5(filename))算法在已知cookie_secret和算法细节的情况下可以被轻易伪造。更安全的做法是使用HMAC算法如hmac.new(secret, msg, hashlib.sha256).hexdigest()或者至少使用更安全的哈希函数如SHA256并加入随机盐nonce防止重放攻击。最小权限原则即使存在SSTI如果模板执行环境被严格沙箱化禁止访问__import__、open、os等危险模块和函数攻击者的利用难度也会大大增加。Tornado模板本身不是沙箱需要开发者自行控制或使用更安全的模板引擎。通过这样一道题我们不仅学会了一种漏洞利用技巧更重要的是理解了漏洞产生的根源和整套安全链条是如何被打破的。在开发中时刻保持对用户输入的高度警惕对敏感信息的严格保护是构建安全Web应用的基石。