1. 题目背景与核心思路解析“BUUCTF[CISCN2019 华东北赛区]Web2”这道题在CTF圈子里算是一个挺经典的案例它完美地展示了如何将多个看似独立的Web漏洞串联起来形成一条完整的攻击链。很多新手在初次接触时可能会被题目中复杂的交互和层层递进的关系搞懵觉得无从下手。但只要你静下心来跟着攻击者的思路走一遍就会发现其中的逻辑非常清晰每一步都踩在了开发者意想不到的“坑”上。这道题的核心其实是一个“组合拳”。它不像一些简单的SQL注入或者文件上传那样给你一个明显的入口点。你需要先通过一个接口获取到关键信息然后利用这个信息去构造另一个请求最终才能拿到目标。整个过程涉及到对API逻辑的逆向、对加密方式的猜测与破解以及对服务器端会话机制的深入理解。我当年打比赛第一次碰到这种题时也是卡了很久后来复盘才发现关键在于要跳出“单点突破”的思维建立起“链路思维”——把整个应用当成一个黑盒去梳理数据从哪里来经过哪些处理最终到哪里去以及我们能在哪个环节施加影响。从题目编号“CISCN2019 华东北赛区”来看这属于国家级网络安全竞赛的赛题难度属于中等偏上。它考察的不仅仅是某个漏洞的利用更是选手的综合渗透测试能力包括信息收集、代码审计哪怕是黑盒、逻辑推理和漏洞串联。接下来我们就一步步拆解这道题我会结合我多次复现和教学的经验把其中容易踩坑的点和关键的技巧都讲清楚。1.1 环境初探与信息收集拿到任何Web题目第一步永远是信息收集。对于这道题我们首先访问目标地址。通常题目页面可能看起来非常简单甚至只有一个登录框或者一个简单的信息展示页面。但我们需要用“黑客”的眼光去看待它。首先使用浏览器开发者工具F12是基本操作。我们需要检查网页源代码查看是否有注释掉的敏感信息、隐藏的链接、或者前端JavaScript代码中泄露的逻辑。有时关键的API路径、参数名甚至加解密函数就明晃晃地写在注释里。网络请求Network这是重中之重。刷新页面或进行任何操作后观察浏览器发送了哪些请求。重点关注XHR/Fetch请求现代Web应用大量使用Ajax核心逻辑往往通过API接口完成。题目可能通过一个/api/xxx的接口获取数据而页面上只是一个展示壳子。请求参数与响应仔细查看每个请求的URL、参数特别是POST的Body、以及服务器的返回内容。响应头Headers有时也包含提示比如特殊的Cookie、服务器信息等。Session与Cookie注意Cookie的变化特别是那些看起来像加密或编码过的值如sessiontoken。以这道题为例在初步访问时我们可能发现页面只有一个简单的功能比如查询某个信息。但通过抓包我们很可能发现一个关键的API例如/api/getinfo。这个API的响应可能就是整个题目的突破口。响应内容可能是一段加密的字符串或者一个包含特殊字段的JSON对象。注意在CTF题目中前端的一切都可能是“烟雾弹”或“提示”。不要轻易相信前端显示的内容和进行的验证如输入长度限制、格式校验真正的逻辑都在服务器端。我们的攻击面也集中在与服务器的直接通信上。1.2 核心漏洞点逻辑缺陷与加密绕过在信息收集阶段我们假设发现了一个关键接口例如/api/decode。它的功能是接收客户端发送的加密数据解密后执行某些操作。这本身就是一种危险的设计模式因为它将解密逻辑暴露给了不可信的客户端。漏洞往往出现在这里服务器相信了客户端提供的“加密”数据而没有验证其完整性和真实性。具体来说可能存在以下问题弱加密算法或硬编码密钥服务器使用的加密算法如AES、DES可能强度不足或者加密密钥直接硬编码在源代码中。如果我们能通过某种方式如源码泄露、逆向前端JS获取或推算出密钥就能自己加密任意数据发送给服务器。加密模式与填充的缺陷例如使用ECB模式会导致相同的明文块产生相同的密文块可能引发攻击。或者Padding Oracle攻击通过服务器对填充错误的不同响应来逐字节解密或加密数据。逻辑混淆服务器可能并未真正进行复杂的加密解密而是使用了一种可逆的编码如Base64或简单的替换如自定义的字符串映射并称之为“加密”。如果我们能分析出编码/替换规则就能伪造数据。在这道“Web2”题目中经过分析很可能是服务器端使用了一种可预测的、或密钥可控的加密方式。我们的任务就是破解这个方式从而能够构造一个特殊的加密数据当服务器解密后会将其解释为一个能让我们提升权限例如成为管理员或读取敏感文件如/flag的指令。例如服务器期望的明文结构可能是{user: guest, role: user}经过加密后客户端发送密文。如果我们能构造明文为{user: admin, role: admin}并将其正确加密那么服务器解密后就会认为我们是管理员。那么如何获取加密密钥或算法呢常见的方法有源代码审计如果题目提供了源码或部分源码直接查找加密相关函数。前端JavaScript分析加密可能在前端进行虽然不安全但在CTF中常见。查看混淆或未混淆的JS代码寻找CryptoJS、encrypt、decrypt、密钥等关键字。旁路攻击如果加密过程涉及时间戳等变量可能通过时间差来推断信息。但在这类题目中更常见的是利用服务器另一个接口的“解密”功能。这正是本题的巧妙之处题目可能提供了一个/api/encode或/api/decode接口。/api/encode用于将我们输入的数据加密/api/decode用于解密数据并可能执行它。如果我们能控制输入/api/encode的数据并得到其密文我们就能观察“明文-密文对”。如果算法不安全如流加密的异或可能通过多次查询来推断出密钥流。2. 攻击链构建与详细利用过程理解了核心漏洞点我们就可以开始动手构建攻击链了。这个过程就像解谜每一步的输入都是下一步的钥匙。2.1 第一步利用信息泄露接口获取初始密文假设我们通过抓包发现了如下接口GET /api/info返回当前用户的信息但信息是加密的。响应可能是{data: U2FsdGVkX1/...很长一串密文...}。POST /api/decode接收一个cipher参数返回解密后的明文。但可能对解密内容有过滤或者只返回部分结果。我们的第一个目标是获取一个“样本密文”。我们访问/api/info拿到那串密文。这个密文对应的明文很可能就是类似{user: your_ip, role: guest}这样的结构。它为我们后续的分析和攻击提供了基础材料。接下来我们尝试向/api/decode发送这个密文。看看服务器返回什么。如果返回了完整的明文那太好了我们直接看到了数据格式。如果返回错误或者返回了部分信息例如只显示了user字段我们需要记录下错误信息这可能用于Padding Oracle攻击。实操心得务必使用Burp Suite、Postman或Python的requests库来操作。浏览器地址栏只适合初步访问。复杂的参数构造和序列化攻击在浏览器里很难完成。我会习惯用Python写脚本方便调试和批量尝试。2.2 第二步分析加密模式与尝试破解拿到至少一对明文密文后开始分析。首先看密文长度和特征。如果密文长度是16、32、48等16的倍数很可能是AESCBC/ECB模式。如果密文包含U2FsdGVkX1开头这是OpenSSL的salted格式的标识常用于DES或AES加密但CryptoJS也常用此格式。如果密文看起来是Hex编码或Base64编码先解码看看原始字节。情况一算法可逆或密钥硬编码如果运气好通过前端JS或别的途径发现加密是简单的AES.encrypt(plaintext, “a_weak_key”)。那么我们可以直接用这个密钥在本地使用相同的库如Python的pycryptodome来加密我们想要的恶意明文。情况二利用编解码功能进行“选择明文攻击”这是本题更可能的情况。我们有一个/api/encode接口或者/api/decode接口在某种条件下会返回完整明文。 攻击思路如下我们向/api/encode发送一个我们精心构造的、极短的明文P1得到密文C1。我们再发送另一个明文P2得到密文C2。通过比较P1、P2与C1、C2的关系推测算法。异或流加密如果C1和C2长度相同且C1 XOR C2的结果等于P1 XOR P2那么很可能是流加密且密钥流复用。我们可以利用这一点通过已知的(P1, C1)对计算出密钥流KeyStream P1 XOR C1。然后对于任何我们想要的明文P_target可以计算出对应的密文C_target P_target XOR KeyStream。ECB模式如果我们构造的明文能对齐块边界比如AES是16字节一块。我们构造P1 “A”*16得到C1。那么C1就是“A”*16加密后的块。如果我们想让服务器解密后得到“B”*16我们只需要找到“B”*16加密后的块C_B。如何找如果我们能控制/api/encode的输入我们可以直接输入“B”*16得到C_B。然后在最终的攻击载荷中我们用C_B替换掉C1中对应的块即可。情况三Padding Oracle攻击如果适用如果/api/decode接口对于填充错误例如PKCS#7填充错误和格式错误例如JSON解析错误返回不同的错误信息那么就可能存在Padding Oracle漏洞。利用这个漏洞我们可以在不知道密钥的情况下解密任意密文或者加密任意明文。但这通常需要发送大量请求每个字节约256次必须编写脚本自动化完成。由于这道题是比赛环境为了兼顾难度和可解性采用情况二异或或ECB块替换的可能性最大。我们需要通过观察和测试来确定。2.3 第三步构造恶意载荷与权限提升一旦我们掌握了加密或伪造密文的方法下一步就是构造能达成目标的明文。根据第一步从/api/info解密或推测出的数据结构我们知道服务器期望的格式。假设是JSON{“user”: “xxx”, “role”: “xxx”, “other”: “…”}。 我们的目标是提升权限将role字段改为admin。读取文件可能需要增加一个字段如“cmd”: “readfile”, “path”: “/flag”。这取决于服务器端解密后如何处理数据。如果服务器只是简单地反序列化JSON并检查role那么改role就够了。如果服务器会将解密后的数据作为命令执行危险那么我们的构造就需要更像一个命令。构造示例假设为JSON原始明文推测{“user”: “172.17.0.1”, “role”: “guest”}目标明文{“user”: “172.17.0.1”, “role”: “admin”}如何加密目标明文如果已获取密钥直接用密钥和相同IV如果需要加密即可。如果是异或流加密计算原始明文 XOR 原始密文 密钥流。然后目标明文 XOR 密钥流 目标密文。这里有一个关键点明文和密文必须是字节形式并且长度必须严格相等。如果目标明文长度不同需要填充到相同长度。通常可以填充空格或利用JSON格式的无关空白。如果是ECB块替换我们需要分别加密“role”: “guest”这个块和“role”: “admin”这个块注意要包含引号和冒号空格并确保长度刚好16字节可能需要调整。然后用后者的密文块替换前者在完整密文中的位置。2.4 第四步发送攻击请求获取Flag构造好最终的攻击密文后我们将其发送到哪个接口通常是那个执行操作的接口。可能是直接替换/api/info请求中的密文参数如果它是靠这个参数判断身份的。发送到/api/decode但期望服务器解密后不仅返回结果还会根据结果执行高权限操作。发送到另一个我们尚未发现的接口如/api/admin。我们需要根据题目上下文判断。一个常见的方法是在提升权限后再次访问/api/info或类似接口此时返回的信息中可能就包含了flag。或者可能会直接返回一个包含flag路径的响应。在整个过程中编码问题是一个大坑。服务器处理的是字节我们操作时可能是字符串。要确保在加密、异或等操作前字符串都使用正确的编码如utf-8转换为字节串bytes。同样接收到的密文Base64格式也需要先解码为字节串再进行操作。3. 完整利用脚本与逐行解析理论讲完了我们来点实际的。下面我以一个基于异或流加密猜测的典型利用场景为例用Python编写一个完整的攻击脚本。我会假设一些具体的接口和响应你可以根据实际题目情况进行调整。import requests import base64 import json # 目标URL根据实际题目修改 BASE_URL http://your_target_ip:port # 第一步获取初始的密文样本例如从 /api/info def get_initial_cipher(): url f{BASE_URL}/api/info resp requests.get(url) # 假设返回的是JSON且密文在data字段 data resp.json() cipher_b64 data[data] # 将Base64密文解码为字节串 cipher_bytes base64.b64decode(cipher_b64) print(f[] 获取到初始密文 (Base64): {cipher_b64}) print(f[] 密文字节长度: {len(cipher_bytes)}) return cipher_bytes # 第二步利用 /api/encode 接口进行选择明文攻击推测密钥流 # 假设 /api/encode 接收一个明文返回加密后的Base64密文 def encode_plaintext(plaintext): url f{BASE_URL}/api/encode # 假设以JSON格式发送字段名为 msg payload {msg: plaintext} resp requests.post(url, jsonpayload) data resp.json() cipher_b64 data[cipher] cipher_bytes base64.b64decode(cipher_b64) return cipher_bytes def guess_xor_keystream(): # 我们构造一个已知的、长度合适的明文P1 # 为了简化我们构造一个与推测的原始明文结构相同、但内容已知的字符串 # 例如原始明文可能是 {user:test,role:user}长度假设为35字节 # 但我们不知道具体长度所以先获取一个密文根据其长度来构造等长的明文 sample_cipher get_initial_cipher() target_len len(sample_cipher) # 构造一个全为A的明文长度与密文相同 P1 bA * target_len # 注意这里P1是字节串 print(f[] 构造测试明文P1 (长度 {target_len}): {P1}) # 获取P1对应的密文C1 C1 encode_plaintext(P1.decode(utf-8)) # 接口可能接收字符串所以先解码 print(f[] 获取到对应密文C1: {C1.hex()}) # 计算密钥流 KeyStream P1 XOR C1 # 这里需要将P1也转为字节串它已经是了并进行逐字节异或 keystream bytes([a ^ b for a, b in zip(P1, C1)]) print(f[] 计算得到密钥流 (前16字节): {keystream[:16].hex()}) return keystream # 第三步构造目标明文并加密 def create_malicious_cipher(keystream): # 推测的原始明文格式根据信息泄露或猜测 # 假设我们从某个地方得知了原始明文例如通过解码一个错误响应得到部分信息 # 假设原始明文是 original_plain b{user:192.168.1.1,role:guest} # 但长度需要与keystream匹配。我们需要先确定原始明文。 # 方法用密钥流解密初始密文得到原始明文 initial_cipher get_initial_cipher() original_plain bytes([a ^ b for a, b in zip(initial_cipher, keystream)]) print(f[] 解密初始密文得到原始明文: {original_plain.decode(utf-8, errorsignore)}) # 构造目标明文将 role 字段改为 admin # 注意这里需要精确替换字节不能改变长度。所以我们要找到 guest 的位置。 original_str original_plain.decode(utf-8) # 确保是有效的JSON便于处理 try: original_json json.loads(original_str) original_json[role] admin # 修改角色 target_plain_str json.dumps(original_json, separators(,, :)) # 紧凑格式避免空格变化 except: # 如果不是标准JSON进行字符串替换风险较高需要精确匹配 print([-] 原始明文不是标准JSON尝试字符串替换...) target_plain_str original_str.replace(guest, admin) target_plain target_plain_str.encode(utf-8) # 检查长度是否一致 if len(target_plain) ! len(keystream): print(f[-] 警告目标明文长度({len(target_plain)})与密钥流长度({len(keystream)})不符) # 可能需要填充或截断这里简单填充空格到相同长度需根据实际情况调整 if len(target_plain) len(keystream): target_plain b * (len(keystream) - len(target_plain)) else: target_plain target_plain[:len(keystream)] print(f[] 调整后目标明文: {target_plain}) # 加密 C_target P_target XOR Keystream malicious_cipher bytes([a ^ b for a, b in zip(target_plain, keystream)]) malicious_cipher_b64 base64.b64encode(malicious_cipher).decode(utf-8) print(f[] 构造的恶意密文 (Base64): {malicious_cipher_b64}) return malicious_cipher_b64 # 第四步发送攻击请求 def send_exploit(cipher_b64): # 假设将恶意密文发送到 /api/decode 或 /api/admin 来触发高权限操作 # 这里需要根据题目实际接口调整 url f{BASE_URL}/api/decode payload {cipher: cipher_b64} resp requests.post(url, jsonpayload) print(f[] 发送攻击请求到 {url}) print(f[] 响应状态码: {resp.status_code}) print(f[] 响应内容: {resp.text}) # 也可能需要将恶意密文作为Cookie或另一个参数发送 # 例如如果身份信息在Cookie中 # cookies {session: cipher_b64} # resp requests.get(f{BASE_URL}/admin, cookiescookies) # print(resp.text) if __name__ __main__: print([*] 开始利用CISCN2019 Web2漏洞链...) try: # 1. 推测密钥流 keystream guess_xor_keystream() # 2. 构造恶意密文 malicious_cipher_b64 create_malicious_cipher(keystream) # 3. 发送攻击 send_exploit(malicious_cipher_b64) except Exception as e: print(f[-] 利用过程出错: {e}) import traceback traceback.print_exc()脚本关键点解析长度对齐异或操作要求明文和密钥流长度严格相等。脚本中通过构造与初始密文等长的测试明文P1来确保这一点。在构造目标明文时也进行了长度检查和填充。编码处理网络传输和服务器处理通常涉及Base64和UTF-8编码。脚本中在接口调用时对字符串进行.encode()/.decode()对密文进行base64.b64encode()/.b64decode()这是最容易出错的地方。JSON处理使用json.loads()和json.dumps()可以更安全地修改结构化数据。separators(‘,’ ‘:’)参数用于生成紧凑的JSON避免因空格差异导致长度变化。错误处理加入了try…except块以应对原始明文不是标准JSON的情况CTF中有时会使用自定义格式。灵活性send_exploit函数中的目标URL和参数格式需要你根据实际题目的抓包结果进行修改。可能不是/api/decode也可能是修改Cookie中的某个值。4. 常见问题排查与实战技巧在实际操作中几乎不可能一帆风顺。下面我总结了一些在解这类题目时最常见的“坑”和解决技巧。4.1 密文长度对不齐怎么办这是异或攻击中最常见的问题。我们的前提是len(P1) len(C1) len(Keystream)。如果服务器在加密前对明文进行了填充如PKCS#7那么len(C1)可能会大于len(P1)。解决方案探测填充方式发送不同长度的明文P1如1字节 15字节 16字节 17字节观察返回的密文C1的长度变化。如果长度总是16的倍数且在一定范围内增长那很可能有填充。消除填充影响如果填充规则已知如PKCS#7我们可以在构造P1时使其长度恰好为块大小的整数倍这样就不会有额外的填充块。例如对于AES16字节构造P1为16、32、48…字节长。调整攻击目标如果无法消除填充可以考虑使用ECB块替换攻击如果算法是ECB模式它不关心整个明文的异或只关心单个块的替换。4.2 服务器返回“Invalid Cipher”或类似错误这说明我们构造的密文在解密时失败了可能原因Base64编码错误确保密文在发送前正确进行了Base64编码并且没有包含换行符等额外字符。字符集问题在将字节转换为字符串进行传输时确保使用正确的编码通常是utf-8。密文被篡改异或计算或块替换过程中可能出错了导致密文字节不合法。建议将中间每一步的字节都打印出来用.hex()进行比对。缺少IV或IV错误如果算法是CBC等需要IV的模式我们构造密文时也需要正确的IV。IV可能包含在密文串的开头如OpenSSL的salted格式也可能通过其他方式传递。我们需要从原始密文中提取出IV并在构造新密文时使用相同的IV。4.3 如何确定加密算法和模式在没有源码的情况下这是一个黑盒测试过程收集数据通过/api/encode获取大量(明文 密文)对。明文要有规律如全零、全一、递增序列、单字节不同等。分析规律ECB模式相同的明文块会产生相同的密文块。尝试加密“A”*32观察前16字节和后16字节的密文是否相同。如果相同很可能是ECB。CBC模式相同的明文块如果IV不同产生的密文块也不同。且密文块依赖于前一个块。流加密如CTR模式或简单异或密文长度与明文长度相同且明文1 XOR 密文1 明文2 XOR 密文2如果密钥流相同。利用工具可以将样本提交给一些密码分析工具或自己写脚本分析统计特性。4.4 提升权限后依然拿不到Flag可能的原因权限提升不彻底role字段可能不是唯一的权限判断标准。可能还需要修改user为某个特定管理员用户名或者存在一个is_admin: true的字段。Flag不在常规路径/flag是常见路径但也可能在/home/ctf/flag./flag.txt 环境变量里或者需要通过执行命令find / -name *flag*来寻找。需要二次跳转提升权限后可能只是解锁了另一个功能界面如命令执行、文件读取。你需要在这个新界面里进行操作才能获取Flag。Session或Token未更新你的权限信息可能存储在服务器的Session中而你的攻击只修改了单次请求的数据。可能需要将恶意密文设置为Cookie中的session值并刷新页面。排查步骤仔细阅读提升权限后的服务器响应看是否有新的链接、提示或功能描述。用新的身份如通过修改Cookie重新访问网站首页或其他功能点。如果存在命令执行或文件读取功能尝试读取/etc/passwd或/proc/self/environ来确认当前权限和环境。使用目录遍历或命令执行寻找Flag文件。4.5 实战技巧备忘录问题场景可能原因排查/解决思路异或后解密乱码编码不一致或长度不对齐1. 确保所有文本操作前都转为字节串。2. 打印并对比每一步的字节长度和Hex值。3. 检查原始明文是否包含非UTF-8字符。替换密文块后服务器报错填充错误1. 确保只替换了数据块没有动填充块。2. 如果替换改变了数据长度可能需要重新计算填充。对于ECB最好保持替换块前后数据总长度不变。找不到/api/encode接口接口路径不同或需要特定参数1. 用目录扫描工具如dirsearch扫描常见API路径。2. 分析前端JS看加密函数调用了哪个URL。3. 尝试参数名如actionencodemethodcrypt等。请求被速率限制题目防护机制1. 在脚本中增加time.sleep()间隔。2. 尝试使用不同的IP或User-Agent。3. 优化攻击脚本减少不必要的请求。最后这类Web题目考察的是一种“黑客思维”——不断假设、验证、调整。从信息收集到漏洞利用每一步都可能有多条分支你需要根据服务器的反馈做出判断。保持耐心细致记录每一个请求和响应成功破解的那一刻带来的成就感正是CTF最大的乐趣所在。我个人的习惯是在解题过程中会用Markdown或文本文件详细记录每一步的操作、观察和猜想这不仅能帮助理清思路赛后复盘时也一目了然。