打开题目,可以进行注册和登录.注册的时候,想起之前V&N的招新赛,感觉这个注册的地方是可以搞事的,事实也证明确实可以搞事.

总之先随意了注册一个号,f12之后发现给出了源码下载地址,下载源码之后开始分析.

先来查看routes.py:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response
from flask_login import logout_user, LoginManager, current_user, login_user
from app import app, db
from config import Config
from app.models import User
from forms import RegisterForm, LoginForm, NewpasswordForm
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
from io import BytesIO
from code import get_verify_code

@app.route('/code')
def get_code():
    image, code = get_verify_code()
    # 图片以二进制形式写入
    buf = BytesIO()
    image.save(buf, 'jpeg')
    buf_str = buf.getvalue()
    # 把buf_str作为response返回前端,并设置首部字段
    response = make_response(buf_str)
    response.headers['Content-Type'] = 'image/gif'
    # 将验证码字符串储存在session中
    session['image'] = code
    return response

@app.route('/')
@app.route('/index')
def index():
    return render_template('index.html', title = 'hctf')

@app.route('/register', methods = ['GET', 'POST'])
def register():

    if current_user.is_authenticated:
        return redirect(url_for('index'))

    form = RegisterForm()
    if request.method == 'POST':
        name = strlower(form.username.data)
        if session.get('image').lower() != form.verify_code.data.lower():
            flash('Wrong verify code.')
            return render_template('register.html', title = 'register', form=form)
        if User.query.filter_by(username = name).first():
            flash('The username has been registered')
            return redirect(url_for('register'))
        user = User(username=name)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('register successful')
        return redirect(url_for('login'))
    return render_template('register.html', title = 'register', form = form)

@app.route('/login', methods = ['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))

    form = LoginForm()
    if request.method == 'POST':
        name = strlower(form.username.data)
        session['name'] = name
        user = User.query.filter_by(username=name).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title = 'login', form = form)

@app.route('/logout')
def logout():
    logout_user()
    return redirect('/index')

@app.route('/change', methods = ['GET', 'POST'])
def change():
    if not current_user.is_authenticated:
        return redirect(url_for('login'))
    form = NewpasswordForm()
    if request.method == 'POST':
        name = strlower(session['name'])
        user = User.query.filter_by(username=name).first()
        user.set_password(form.newpassword.data)
        db.session.commit()
        flash('change successful')
        return redirect(url_for('index'))
    return render_template('change.html', title = 'change', form = form)

@app.route('/edit', methods = ['GET', 'POST'])
def edit():
    if request.method == 'POST':
        
        flash('post successful')
        return redirect(url_for('index'))
    return render_template('edit.html', title = 'edit')

@app.errorhandler(404)
def page_not_found(error):
    title = unicode(error)
    message = error.description
    return render_template('errors.html', title=title, message=message)

def strlower(username):
    username = nodeprep.prepare(username)
    return username

功能就那么几个,但其中还有一个改密码的功能,之前联想到的V&N的招新赛题也是需要改密码的,那看来注册那里肯定是有事可搞的了.翻了下源码,看见index.html中有这么一段:

{% if current_user.is_authenticated and session['name'] == 'admin' %}
<h1 class="nav">hctf{xxxxxxxxx}</h1>

当用admin这个账号登录的时候,我们就可以获得flag了.admin的密码我们是不知道的,但由于存在一个改密码操作,所以可以考虑通过某种方法注册一个用户名在注册时不会被判为重复且改密码时用户名能够被处理为”admin”的用户.为了达成目的,继续分析源码.先聚焦到改密码操作的代码,发现在修改密码时调用了strlower函数,而这个函数在注册时也被调用了,于是去看该函数的实现,发现用到了nodeprep.prepare.继续跟发现:from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep.在下载源码的链接中有一个requirements.txt,其中的Twisted的版本为10.2.0,和最新版本差了很多,那应该是有漏洞了.而此处的漏洞为Unicode欺骗.’ᴬ’在经过了一次nodeprep.prepare 处理后会转成大写即’A’,此时再处理一次则会变为小写即’a’.而用户名在注册时会被处理一次,改密码时还要被处理一次,那么注册用户名为’ᴬᴰᴹᴵᴺ‘的用户,登陆后进行改密码的操作,就能够修改admin用户的密码了(ᴬᴰᴹᴵᴺ,注册时处理->ADMIN,改密码时再次处理->admin).用户名和密码都知道了,登出现在的账号后用admin账号登录,即可获得flag.

具体的编码:https://unicode-table.com/en/search/?q=small+capitalnodeprep.prepare

本题除了这种做法还有另外两种做法:session伪造和条件竞争.详见sky师傅的文章:2018 HCTF Web Writeup

Categories:

Tags:

No responses yet

发表评论

电子邮件地址不会被公开。 必填项已用*标注

闽ICP备19027300号