
1. 项目概述一次由AI招聘平台引发的百万级数据泄露事件复盘最近在参与一个SRC安全应急响应中心的众测项目时我遇到了一个非常典型的案例。目标是一个新兴的AI招聘平台主打利用人工智能算法进行人岗匹配听起来技术含量很高。起初我只是想进行常规的Web应用安全测试看看有没有常见的SQL注入、XSS这类漏洞。但测试的深入程度完全超出了我的预期——从一个看似不起眼的接口信息泄露最终竟牵扯出平台上近百万份求职者的完整简历信息包括姓名、电话、邮箱、工作经历、教育背景等敏感数据。整个过程与其说是一次漏洞挖掘不如说是一次对现代Web应用架构安全盲点的深度“体检”。今天我就把这次实战的完整思路、技术细节和踩过的坑毫无保留地分享出来。无论你是刚入门漏洞挖掘的新手还是有一定经验的渗透测试工程师相信都能从中获得一些启发。这次渗透测试的核心并非依赖多么高深的0day漏洞而是对目标资产信息收集的彻底性以及对“信息泄露”这一古老但永不过时漏洞类型的深刻理解。2. 目标侦察与攻击面梳理从域名到API的全面测绘在拿到目标域名假设为career-ai.com后我并没有急于上手去戳它的登录框或搜索栏。盲目测试效率极低且容易触发告警。我的第一步永远是尽可能全面地绘制目标资产的数字地图。2.1 子域名枚举与资产发现一个主站往往只是冰山一角真正的脆弱点可能隐藏在子域名、测试环境、API网关或旧的遗留系统中。我通常会组合使用多种工具进行交叉验证确保覆盖率。工具组合与策略被动收集使用subfinder、amass等工具利用证书透明度日志、搜索引擎、DNS数据集等公开情报源被动地收集子域名。这不会对目标服务器产生直接请求非常隐蔽。subfinder -d career-ai.com -silent | tee subdomains_passive.txt amass enum -passive -d career-ai.com -o subdomains_amass.txt主动爆破使用altdns生成可能的子域名变体如dev-、api-、staging-等前缀再结合massdns或puredns进行高速DNS解析验证。这里的关键是准备一个高质量的字典。# 生成变体 altdns -i subdomains_passive.txt -o altdns_output.txt -w words.txt # 解析验证 puredns resolve altdns_output.txt --resolvers resolvers.txt -q端口与服务扫描对发现的存活子域名和主域名使用naabu或nmap进行快速端口扫描识别开放的非标准端口如 3000, 5000, 8080, 8443 等这些端口上可能运行着管理后台、API服务或调试接口。naabu -list alive_subdomains.txt -top-ports 1000 -o ports.txt # 对关键端口进行服务识别和脚本扫描 nmap -sV -sC -p 80,443,8080,8443,3000,5000 -iL alive_subdomains.txt -oA nmap_scan本次实战发现通过上述组合拳我发现了几个关键资产www.career-ai.com主站React前端。api.career-ai.com核心API服务提供所有业务接口。admin.career-ai.com管理后台入口需要登录。static.career-ai.com静态资源服务器。legacy.career-ai.com一个旧的系统似乎已停用但未下线。在api.career-ai.com的 8443 端口上还发现了一个Jenkins构建服务这是一个重大发现但需要凭证。注意子域名枚举的字典质量直接决定发现率。我维护了一个自定义字典融合了常见业务词汇如job,resume,profile,upload、云服务商前缀如aws,azure,gcp以及公司内部可能使用的项目代号。同时resolvers.txtDNS解析器列表的纯净度和速度也至关重要建议定期更新。2.2 Web应用指纹识别与目录爆破确定了主要的Web入口点后下一步是识别它们使用的技术栈并寻找隐藏的目录和文件。指纹识别使用httpx或webanalyze快速获取HTTP响应头、状态码、Title并识别Web框架、前端库、服务器、CDN等信息。cat alive_web_targets.txt | httpx -title -tech-detect -status-code -o web_fingerprint.json识别出主站使用Nginx ReactAPI服务使用Nginx Django REST Framework。Django框架的默认调试页面、admin路径是需要重点关注的。目录与文件爆破使用feroxbuster或ffuf进行递归目录爆破。这里有两个关键点字典选择不能只用通用字典。我针对Django使用了包含admin/,api/,swagger/,redoc/,debug/等路径的专用字典。针对可能的备份文件使用了包含.git/,.svn/,.bak,.tar.gz,~等条目的字典。递归扫描对发现的每一个目录进行递归扫描特别是api/、v1/、upload/这类目录。feroxbuster -u https://api.career-ai.com -w /path/to/api_dirs.txt -x php,json,txt,bak -t 50 -d 3 -C 404,403本次实战发现在api.career-ai.com上feroxbuster发现了/api/v1/docs/返回403和/api/v1/schema/返回了JSON格式的API Schema。这是一个关键的信息泄露点它完整地列出了后端的所有API端点、参数格式甚至部分描述。这相当于拿到了系统的“地图”。2.3 API接口分析与参数提取拿到API Schema后我立即对其进行分析。它包含了用户注册、登录、简历上传、职位搜索、企业查看简历等数十个端点。我重点关注两类接口数据查询类如GET /api/v1/resumes/GET /api/v1/companies/GET /api/v1/applications/。这些接口通常涉及大量敏感数据。文件操作类如GET /api/v1/resumes/{id}/download/POST /api/v1/upload/。可能存在路径遍历或未授权访问。同时我使用gauGetAllURLs和waybackurls工具从历史档案中获取目标域名曾经出现过的URL有时能发现一些已被删除但后端逻辑还在的“幽灵接口”。echo career-ai.com | gau | grep api | sort -u historical_api_urls.txt3. 漏洞挖掘的核心突破点从信息泄露到批量数据获取信息收集阶段结束后攻击面已经非常清晰。我的突破口就选在了那个泄露的API Schema和几个可疑的数据查询接口上。3.1 接口未授权访问与IDOR漏洞首先测试的是GET /api/v1/resumes/接口。按照常理这应该需要企业账号权限才能访问。我直接用一个未登录的会话或使用Burp Suite的Repeater模块不带任何认证头去请求这个接口。第一次请求返回401 Unauthorized。这很正常。尝试绕过我注意到在Schema中该接口的认证方式标注为SessionAuthentication和TokenAuthentication。我尝试将请求的Content-Type改为application/json并在URL后添加常见的参数如?formatjson、?debugtrue均无效。路径探索我回想起目录爆破时还有一个/api/v1/resumes/的兄弟路径/api/v1/profiles/。请求后竟然返回了200 OK并且内容是当前登录用户的信息此时未登录返回了匿名用户的数据结构但字段齐全。这说明/api/v1/profiles/接口对未授权访问的处理可能有问题它没有严格校验用户上下文而是返回了一个默认的、空的Profile对象。这本身是一个低危问题但提示我认证逻辑可能不一致。真正的突破来自于对另一个接口的测试GET /api/v1/applications/{id}/。这个接口用于查询某个职位申请的具体信息。根据业务逻辑只有申请者本人和招聘企业才能查看。我构造了一个请求GET /api/v1/applications/123/ HTTP/1.1 Host: api.career-ai.com我预期是403或401。但服务器返回了404 Not Found。这很有趣404意味着服务器处理了这个请求并去数据库查询了ID为123的申请记录因为没找到所以返回404而不是因为认证失败返回40x。这说明接口可能先执行了数据查询再执行权限校验或者权限校验存在缺陷。为了验证我需要一个存在的、且不属于我的application_id。我从哪里来这时之前信息收集的另一个成果派上了用场。在测试主站搜索功能时我通过Burp抓包发现搜索职位列表的接口GET /api/v1/jobs/返回的数据中包含每个职位的id以及一个application_count申请人数字段。我找了一个申请人数较多的职位假设其ID为job_456。接下来我猜测申请记录的ID可能是顺序生成的或者与职位ID、用户ID有关联。我尝试请求GET /api/v1/applications/?job_id456 HTTP/1.1这次服务器返回了200 OK并且列出了一些申请记录的摘要信息如申请时间、状态但关键字段如申请人姓名、简历ID被模糊化了显示为***。然而每一条申请记录的真实IDid字段是明文返回的我瞬间获得了数十个有效的application_id。我随机选取其中一个application_id例如789再次请求详细接口GET /api/v1/applications/789/ HTTP/1.1Bingo!服务器返回了200 OK并且包含了完整的申请信息申请人的用户IDuser_id、简历IDresume_id、以及一份简历的文本快照这是一个典型的不安全的直接对象引用IDOR漏洞。我可以通过枚举application_id越权查看所有申请记录。3.2 利用IDOR漏洞构造数据遍历拿到一个有效的user_id例如user_999和resume_id例如resume_888后攻击路径瞬间拓宽。遍历用户简历直接尝试访问简历下载接口。GET /api/v1/resumes/888/download/ HTTP/1.1返回200 OK我成功下载了一份不属于我的、完整的PDF格式简历包含所有个人敏感信息。构造批量爬取漏洞利用的逻辑链条已经形成。从公开的职位列表接口获取job_id。通过GET /api/v1/applications/?job_id{job_id}获取该职位下的所有application_id。遍历每个application_id通过GET /api/v1/applications/{id}/越权获取user_id和resume_id。使用获取到的resume_id调用GET /api/v1/resumes/{id}/download/下载完整简历。同时还可以通过user_id尝试访问GET /api/v1/profiles/{id}/如果存在获取用户个人资料。我写了一个简单的Python脚本来自动化这个过程。为了避免对服务器造成过大压力这也是渗透测试的职业道德我加入了延迟和控制遍历深度的参数。import requests import time import json BASE_URL https://api.career-ai.com HEADERS {User-Agent: Mozilla/5.0} def get_jobs(): # 获取职位列表 resp requests.get(f{BASE_URL}/api/v1/jobs/?limit100, headersHEADERS) return resp.json().get(results, []) def get_applications(job_id): # 获取某个职位的申请列表 resp requests.get(f{BASE_URL}/api/v1/applications/?job_id{job_id}, headersHEADERS) return resp.json().get(results, []) def get_application_detail(app_id): # 越权获取申请详情拿到简历ID resp requests.get(f{BASE_URL}/api/v1/applications/{app_id}/, headersHEADERS) if resp.status_code 200: data resp.json() return data.get(resume_id), data.get(user_id) return None, None def download_resume(resume_id, filename): # 下载简历文件 resp requests.get(f{BASE_URL}/api/v1/resumes/{resume_id}/download/, headersHEADERS) if resp.status_code 200: with open(filename, wb) as f: f.write(resp.content) print(f[] 简历 {resume_id} 下载成功: {filename}) return True else: print(f[-] 简历 {resume_id} 下载失败: {resp.status_code}) return False def main(): jobs get_jobs() for job in jobs[:10]: # 控制范围只测试前10个职位 job_id job[id] print(f[*] 处理职位: {job[title]} (ID: {job_id})) applications get_applications(job_id) for app in applications: app_id app[id] resume_id, user_id get_application_detail(app_id) if resume_id: filename fresume_{resume_id}.pdf download_resume(resume_id, filename) time.sleep(1) # 礼貌延迟 time.sleep(2) if __name__ __main__: main()实操心得在编写自动化脚本时务必注意请求频率和超时处理。过于频繁的请求会被WAFWeb应用防火墙或风控系统识别为攻击。我通常会在请求间加入1-3秒的随机延迟并处理好各种HTTP状态码如429 Too Many Requests。此外所有爬取的数据仅用于漏洞验证和报告编写并在测试结束后立即删除。3.3 发现更大的“宝藏”分页参数暴露数据总量在测试过程中我注意到GET /api/v1/applications/?job_idxxx这个接口的响应头里包含了X-Total-Count字段其值为3521。这告诉我仅这一个职位就有超过3500份申请。而通过遍历多个热门职位我估算出可访问的application_id总量非常庞大。更严重的是当我尝试直接访问GET /api/v1/resumes/不带参数时虽然返回401但通过添加一个错误的查询参数如?limit-1或?page999999服务器返回了一个不同的错误信息其中竟然包含了数据库查询的片段隐约提到了total_count。这提示我分页逻辑可能存在缺陷。我尝试了另一个常见的测试向量GET /api/v1/resumes/?limit1000000。服务器返回了400 Bad Request提示limit参数超出最大值。但错误信息里清晰地写着“Maximum limit is 1000”。这说明只要我通过某种方式通过认证我就能一次性拉取最多1000条简历记录。那么认证如何绕过我回想起了之前发现的legacy.career-ai.com旧系统。旧系统往往安全措施更薄弱。经过一番测试我在旧系统上发现了一个使用默认凭证或弱密码的后台入口此处细节省略涉及其他漏洞类型。通过旧系统我获得了一个具有较低权限的会话Cookie。我将这个Cookie用于新的API系统。令人震惊的一幕发生了当我用这个旧系统的会话Cookie访问GET /api/v1/resumes/?limit1000时服务器返回了200 OK并且是一个包含1000条简历摘要信息的列表虽然每条摘要只包含id、user_id、created_at等少量字段但这已经是一个灾难性的信息泄露。我可以通过这个接口以1000条/页的速度遍历获取平台上几乎所有的resume_id。结合之前越权下载简历的漏洞这意味着整个平台的简历库对我敞开了大门。4. 漏洞链整合与影响范围评估至此我已经掌握了多条独立的漏洞它们可以串联成一个威力巨大的攻击链信息泄露API Schema暴露提供了系统完整的接口蓝图。不安全的直接对象引用IDOR允许越权访问任意职位申请记录从而获取user_id和resume_id。认证逻辑不一致/旧系统权限提升通过旧系统的薄弱点获取了一个有效会话该会话在新API系统上拥有不当的数据读取权限。分页参数暴露与批量枚举利用高权限会话和分页接口可以批量枚举全站简历ID。影响范围评估我停止了进一步的批量爬取开始评估影响。通过有限的抽样查询和接口返回的X-Total-Count等信息我保守估计可越权访问的职位申请记录数量数十万条。可通过分页接口枚举的简历ID数量接近百万级。每份简历包含的信息姓名、手机号、邮箱、工作经历、教育背景、技能、期望薪资等。风险等级严重Critical。这属于大规模个人敏感信息泄露违反《网络安全法》《个人信息保护法》等相关规定。5. 漏洞修复建议与防御思考在向平台方提交漏洞报告后我也从防御角度梳理了修复方案这不仅仅是给厂商的建议也是我们自己在开发或审计系统时需要牢记的点5.1 立即缓解措施移除公开的API Schema生产环境不应暴露schema、docs、redoc等调试接口。如果必须提供应施加IP白名单或强制认证。修复IDOR漏洞在所有对象级访问的接口中加入严格的权限校验。必须在业务逻辑层进行校验确保当前请求的用户通过Session或Token识别有权限访问目标资源application_id、resume_id等。Django可以使用get_object_or_404配合权限类或在视图函数中显式检查request.user与资源所有者的关系。修复旧系统并统一认证下线或彻底加固legacy系统。新旧系统的用户体系和权限系统必须隔离或通过统一的、强健的认证网关如OAuth 2.0进行对接避免低权限会话在高权限系统内通行。实施速率限制和监控对GET /api/v1/resumes/、GET /api/v1/applications/等高敏感接口实施严格的基于用户/IP的速率限制如每分钟最多60次请求并记录所有访问日志对异常批量访问行为进行实时告警。5.2 长期架构优化实施资源隔离与最小权限原则API接口的设计应遵循最小权限原则。普通用户会话绝不应有访问resumes/列表的权限。企业用户只能访问投递给自己的职位的申请记录。输入验证与输出过滤对所有输入参数如id、limit、page进行严格的类型、范围和业务逻辑校验。对返回给前端的数据进行彻底的敏感信息过滤和脱敏避免像本次事件中返回真实的resume_id。定期安全审计与渗透测试建立常态化的安全开发流程SDL在系统上线前和重大更新后进行代码审计和渗透测试。特别要关注新旧系统整合处的安全边界。加强资产与配置管理定期进行子域名、端口扫描清理无人维护的测试、陈旧环境。确保所有对外服务都有明确的责任人和安全基线配置。6. 渗透测试中的常见问题与排查技巧在这次实战中以及以往的经验里有一些共性的问题和技巧值得总结Q1: 如何判断一个接口是否存在IDOR漏洞A1: 核心方法是替换资源标识符。如果你能访问/api/users/123你自己的资料尝试访问/api/users/124。如果124也能访问大概率存在IDOR。测试时要使用不属于你但确实存在的ID可以通过其他信息泄露途径获取或根据系统规则猜测如顺序数字、UUID。Q2: 遇到403/401就放弃吗A2: 绝不。403/401只是表明默认的认证/授权检查失败了。要尝试修改HTTP方法GET403试试POST、PUT、DELETE甚至PATCH。修改Content-Typeapplication/json不行试试application/xml或text/plain。添加或删除尾部斜杠/api/resource和/api/resource/可能由不同路由处理。尝试HTTP参数污染HPP添加重复的参数如?id123id124看服务器如何处理。检查是否存在路径遍历如/api/v1/users/../admin/。Q3: 信息收集时如何高效处理海量的子域名和URLA3: 我的工作流是subfinder/amass被动 -altdns生成变体 -puredns解析 -httpxHTTP探测 - 结果导入到类似Aquatone或gowitness的工具中进行截图和分类。对于存活的Web目标再用nuclei进行初筛。整个过程可以编写Shell脚本或使用xargs进行管道串联实现自动化。Q4: 如何避免在测试过程中触发警报或被封IPA4:使用代理池对于主动扫描和爆破使用高质量的代理IP轮询。控制速率在所有自动化工具中设置-rate-limit或-delay参数。模拟正常用户使用真实的浏览器User-Agent添加Referer等头部请求间加入随机延迟。分时段测试避免在业务高峰时段进行高强度扫描。遵守测试范围严格在授权范围内测试不触碰明确排除的资产如支付接口、核心数据库直连地址。Q5: 像API Schema泄露这种“低级错误”为什么在现代开发中依然常见A5: 这往往是开发和运维脱节导致的。开发者在调试阶段启用DEBUGTrue并安装django-rest-framework的文档模块而上线时忘记关闭或移除。在CI/CD流水线中缺乏一个强制性的“安全配置检查”环节。防御方法是在Django的settings.py中根据环境变量动态设置DEBUG和INSTALLED_APPS并确保生产环境的配置经过安全评审。这次从AI招聘平台到百万信息泄露的实战再次印证了安全是一个整体任何一个环节的疏忽过时的子系统、不当的权限校验、敏感信息的过度暴露都可能被串联起来造成严重的后果。对于渗透测试者而言耐心、细致和系统性的思维往往比掌握一个最新的0day漏洞更重要。每一次测试都是一次与开发者思维模式的对话理解他们如何构建系统才能更有效地找到他们遗漏的角落。