题目很有趣,但是也没有做出来几道

Custom Web Server (1)

这个题目是一道服务器漏洞的题目

给了附件我们打开看一下

这是一个用c语言写的服务器,跟据这个代码和前面的页面的测试发现,这个服务器只能请求

if (!ends_with(filename, ".html") && !ends_with(filename, ".png") && !ends_with(filename, ".css") && !ends_with(filename, ".js")) return NULL;

这些后缀的页面,根据dockerfile我们可以知道我们的那个flag的名字是flag.txt

这里的化我们经过测试发现这里面会有路径穿越

这里有路径穿越但是这里面限制了文件后缀,我们无法穿越到根目录去读取这个flag.txt

我们知道,穿越目录到根目录之后再次穿越还是根目录

这里我们看一下打开文件的逻辑

这里的缓冲区的大小做了限制,这里我们就可以利用缓冲区溢出来截断文件后缀

我们可以看到这里是先做了判断再次对这个文件名进行检查,判断是否出现了溢出

这里限制的字节大小是1024,加上前面的public/,前面的七个字节,我们要填充1017个字节后面文件名后缀的进行截断

/../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../flag.txt.js

比赛的时候想到了这个缓冲区溢出,但是没有想到还能和截断联系在一起

Custom Web Server (2)

这两道题的服务器一摸一样,但是与(1)不同的是,这个服务器加载了nginx的代理,加上了代理之后,我们就无法进行路径穿越来读取文件了

但是两个服务器可能会发生请求走私来访问到我们想要的东西

这个没有弄明白

Mystiz’s Mini CTF (1)

这个是一个ctf的平台让我们找出来这个平台的漏洞

这个是要我们找到这个Hack this site!这个web挑战的flag

我们先注册一个账号,登进去看一下

这里就是那个挑战,有一个解

给了源码我们看一下源码

在web\migrations\versions这个目录下面发现了一个sql语句,我们可以观察一下,这个数据库里面有什么

我们可以看到这里面有三个表,分别是challenge,user,attempt,这里的话,这三个表分别记录的是我们的挑战,用户和提交记录

这是向这三个表里面插入数据,用户表中有两个用户分别是admin,player

这个challenge里面有挑战,这个攻击表里面也有数据是这个player提交的flag,我们的目的就是要获取这个player用户提交的flag

这里面我们看一下这个minnctf的大致框架

在这里面我们可以看到这个应该是文件的入口点,我们也可以在Dockerfile文件中看到,是先运行这个app.py

我们先看一下这个服务的整体框架然后再去分析这个入口文件

这里面有个migrations和app,这个app就是我们的主要的框架内容,下面的migrantions是我们的数据库迁移数据

migrations/
│
├── alembic.ini           # Alembic 配置文件
├── env.py                # 迁移环境配置
├── script.py.mako        # 迁移脚本模板
└── versions/             # 迁移脚本目录
    ├── 1234567890ab_add_field_to_user.py  # 迁移脚本
    └── 9876543210bc_remove_old_table.py  # 迁移脚本

这个就是这个目录,接下来看一下这个app这个目录

这里我们可以看到这个框架的入口,这里面有

app/
│
├── models/
├── static/
├── templates/
├── views
├── app.py
└── ...

app.py就是框架的入口文件,其他的models/目录下面是框架的核心模板,static/目录下都是一些静态的资源,templates/下面是一些模板渲染的,views/是视图目录

我们先把文件的入口点分析一下

这里面是初始化了各个功能,数据库,视图,数据库迁移,登入,限制请求

这个初始化了数据库

接下来是这个初始化了这个视图模块

这个模块是定义了一些api,这个api我们后面有大用,到后面我们再分析这个

接下就是这个command.py

这个模块,是进行的数据库迁移操作

接下来初始化用于管理 Flask 应用中的用户会话

最后一个就是初始化了限制

这里面限制了我们的请求次数

入口文件分析完成之后,接下来我们通过功能点来分析这个代码的具体作用

这个models,

大概有三个功能点,攻击,挑战,用户

这些都是一些类,用于向数据库中添加攻击,挑战,用户

最后我们可以看到这个密码储存到数据库中是密文储存的

他是调用的了这个compute_hash()方法

这个加密的是salt.sha256(密码)

这个加密我们到后面也有大用

这里的话这个静态资源我们就不分析了

接下来我们重点分析一下这个views模块

这里我们看到这个确实是比较复杂我们一层一层的分析

首先是这个__init__.py

from flask import Blueprint, request, jsonify
from flask.views import MethodView
import collections

from app.views import pages
from app.views.api import users
from app.views.api import challenges
from app.views.api.admin import challenges as admin_challenges
from app.models.user import User
from app.models.challenge import Challenge
from app.models.attempt import Attempt


class GroupAPI(MethodView):
    init_every_request = False

    def __init__(self, model):
        self.model = model

        self.name_singular = self.model.__tablename__
        self.name_plural = f'{self.model.__tablename__}s'
    
    def get(self):
        # the users are only able to list the entries related to them
        items = self.model.query_view.all()

        group = request.args.get('group')

        if group is not None and not group.startswith('_') and group in dir(self.model):
            grouped_items = collections.defaultdict(list)
            for item in items:
                id = str(item.__getattribute__(group))
                grouped_items[id].append(item.marshal())
            return jsonify({self.name_plural: grouped_items}), 200

        return jsonify({self.name_plural: [item.marshal() for item in items]}), 200


def register_api(app, model, name):
    group = GroupAPI.as_view(f'{name}_group', model)
    app.add_url_rule(f'/api/{name}/', view_func=group)
    

def init_app(app):
    # Views
    app.register_blueprint(pages.route, url_prefix='/')

    # API
    app.register_blueprint(users.route, url_prefix='/api/users')
    app.register_blueprint(challenges.route, url_prefix='/api/challenges')
    app.register_blueprint(admin_challenges.route, url_prefix='/api/admin/challenges')

    register_api(app, User, 'users')
    register_api(app, Challenge, 'challenges')
    register_api(app, Attempt, 'attempts')

这里面就是注册了几个这个api路由我们结合这着前面的功能点进行分析

我们访问这个api/challenges

发现了这个返回的json数据包

这里面我们看到这个有个这个group参数,分组查询,看不懂代码什么意思

我们抓个包看一下

这个历史记录里面有请求这个group这个参数,我们试着访问一下

这里好像是要分组查询这个东西

我们也可以按照其他的分组进行查询

这个功能我们搞明白了之后,我们看一下下面的这个pages.py这个

from flask import Blueprint, request, jsonify, render_template, flash, url_for, redirect
from http import HTTPStatus, HTTPMethod
from wtforms_sqlalchemy.orm import model_form
from flask_login import login_user, logout_user, current_user

from app.db import db
from app.limiter import limiter
from app.models.user import User

route = Blueprint('pages', __name__)


@route.route('/', methods=[HTTPMethod.GET])
def homepage():
    return render_template('homepage.html', current_user=current_user), HTTPStatus.OK

@route.route('/register/', methods=[HTTPMethod.GET])
def register():
    return render_template('register.html'), HTTPStatus.OK

@route.route('/register/', methods=[HTTPMethod.POST])
def register_submit():
    user = User()
    UserForm = model_form(User)

    form = UserForm(request.form, obj=user)

    if not form.validate():
        flash('Invalid input', 'warning')
        return redirect(url_for('pages.register'))

    form.populate_obj(user)

    user_with_same_username = User.query_view.filter_by(username=user.username).first()
    if user_with_same_username is not None:
        flash('User with the same username exists.', 'warning')
        return redirect(url_for('pages.register'))

    db.session.add(user)
    db.session.commit()

    login_user(user)
    return redirect(url_for('pages.homepage'))

@route.route('/login/', methods=[HTTPMethod.GET])
def login():
    return render_template('login.html'), HTTPStatus.OK

@route.route('/login/', methods=[HTTPMethod.POST])
@limiter.limit("2/minute")
def login_submit():
    username = request.form.get('username')
    password = request.form.get('password')

    user = User.query_view.filter_by(username=username).first()
    if user is None:
        flash('User not found.', 'warning')
        return redirect(url_for('pages.login'))

    if not user.check_password(password):
        flash('Invalid password.', 'warning')
        return redirect(url_for('pages.login'))

    login_user(user)
    return redirect(url_for('pages.homepage'))

@route.route('/logout/', methods=[HTTPMethod.GET])
def logout():
    logout_user()
    return redirect(url_for('pages.homepage'))

@route.route('/challenges/', methods=[HTTPMethod.GET])
def list_challenges():
    return render_template('challenges.html'), HTTPStatus.OK

@route.route('/scoreboard/', methods=[HTTPMethod.GET])
def scoreboard():
    return render_template('scoreboard.html'), HTTPStatus.OK

@route.route('/admin/challenges/', methods=[HTTPMethod.GET])
def manage_challenges():
    if not current_user.is_authenticated:
        flash('You are not logged in.', 'warning')
        return redirect(url_for('pages.login'))

    if not current_user.is_admin:
        return render_template('403.html'), HTTPStatus.FORBIDDEN

    return render_template('admin/challenges.html'), HTTPStatus.OK

这里面都是各个路由的具体实现

下面我们看一下这个views这个模块的其他的

这个api这个路由下面是是一些管理员的管理的一些操作,

一些验证flag正确的操作

现在我们对这个框架已经分析完了,现在我们知道的是,我们有一个用户叫做player提交了flag

就只有他提交了flag,我们要获得这个flag,但是我们没有密码,通过数据库我们知道了,他的密码就是六位数字

爆破肯定是不行的,这里面限制了请求次数

这里面有几个api我们可以试着访问一下,这里面可能会暴露敏感信息

这里面是没有这个字段的,所以默认按user进行分组,我们试一下用这个password进行分组

确实是用泄露了密码,但是这个密码是加密过后的,我们暴露破解一下(前面分析过这个密码是六位)

import os
import sys
import string
import hashlib

PLAYER = "973fbe24.2e029c76eade955c007327d37168122318dfecb5cbf99b8f708cf940828de820"

SALT = PLAYER.split('.')[0]


for a in string.hexdigits:
    for b in string.hexdigits:
        for c in string.hexdigits:
            for d in string.hexdigits:
                for e in string.hexdigits:
                    for f in string.hexdigits:
                        result = SALT + '.' + hashlib.sha256(f'{SALT}/{a}{b}{c}{d}{e}{f}'.encode()).hexdigest()

                        if result == PLAYER:

                            print(f"User: player, password: {a}{b}{c}{d}{e}{f}")
                            sys.exit()

成功爆出来了这个密码

我们登入一下

这里面我们看到了这个web题目已经被攻破,这里面,我们用同样的方法来泄露这个flag

至此我们的挑战一完成

理一下思路

就是我们通过查看数据库,我们知道了这个挑战被player用户攻破了

我们经过代码审计发现,无法爆破这个用户名和密码

但是这里面我们可以通过这个api来泄露这个用户的哈希值

我们爆破这个哈希值登入进去

然后就用同样的方式来泄露这个flag

Mystiz’s Mini CTF (2)

这个挑战2和挑战1一样是同一个网站,但是题目的描述变了,这里面要我们获得的是A placeholder challenge的这个flag

这个挑战我们看数据库知道这个挑战是一年之后才发布的

 db.session.add(Challenge(id=1337, title='A placeholder challenge', description=f'Many players complained that the CTF is too guessy. We heard you. As an apology, we will give you a free flag. Enjoy - <code>{FLAG_2}</code>.', category=Category.MISC, flag=FLAG_2, score=500, solves=0, released_at=RELEASE_TIME_BACKUP))

这肯定要我们登入管理员的后台了

但是我们肯定不能像方法一一样,用来爆破这个admin的密码

这个密码有66位,我们肯定爆破不了

我们看一下他验证这个admin身份的逻辑

这个是通过这个is_admin,来验证这个是否是admin用户

再这个注册功能里面,我们可以看到这里面是直接获得我们传入的参数,这个参数我们抓个包看一下

这里面的参数我们可以加一下这个is_admin=true

这里就污染了原来的参数is_admin=true

注册成功之后,我们看到这个用户已经是admin用户了

我们看一下这个/api/admin/challenges

至此这两个挑战都已经完成