Python脚本暴力破解隐藏PIN码:从抓包到自动枚举实操
发布时间:2026/10/22 15:44:43 锦皓数字建站

最近在某个CTF练习平台上刷题被一道名为“隐藏PIN小游戏”的入门题吸引了。页面朴素得很一个输入框、一个验证按钮要求输入4位PIN码对了给flag错了返回一句“wrong pin”。这种看似人畜无害的小游戏恰恰是CTF入门阶段练习脚本能力的好材料——你需要先观察它的请求逻辑再写一个Python脚本把0000到9999全部试一遍。今天这篇内容就围绕这道题把我实操的全过程、踩过的坑、以及从里面延伸出来的思路完整拆一遍适合刚接触CTF、想练脚本的同学。1. 项目初探这道隐藏PIN小游戏到底在考什么1.1 题目环境与基础信息的快速收集这种题目一般部署在CTF练习平台的独立靶机环境或者直接给你一个本地服务的访问地址。我碰到的是一个非常简单的网页顶部标题写着“隐藏 PIN 小游戏”中间一个输入框一个“验证”按钮下方是结果显示区。题目描述就一句找出隐藏的PIN码获取flagPIN是4位数字。先手动玩一下随便输入1234点击验证页面显示“wrong pin”。打开浏览器开发者工具切到Network面板再点一次验证能看到请求信息清清楚楚请求地址是/check请求方法是POST请求体是一个JSON{pin: 1234}响应体是一个JSON{success: false, msg: wrong pin}这些信息就是写脚本的全部基础。很多人看到题目就急着敲代码其实前两分钟的抓包比后面的脚本重要得多。接口路径、参数名、数据格式、成功失败的判定字段全在这个环节确定。我把关键信息整理成一张表后面写脚本时随时对照信息项内容接口地址/check请求方法POST请求数据格式JSON请求字段名pin成功判定success为true且响应中带flag失败判定success为falsemsg值为wrong pinPIN范围4位纯数字0000~99991.2 核心考点拆解与影响范围这道题名字叫“简单爆破”考点很直白通过大量尝试寻找正确答案。但真正做下来会发现它把CTF入门阶段好几项基本功串在了一起。你需要理解HTTP请求的基本概念知道POST和GET的区别需要会用浏览器开发者工具看网络面板需要能读懂网页前端代码里的逻辑需要会写Python脚本并会构造requests请求还需要能从响应内容里准确判断“这次到底成没成功”。影响范围其实不小。掌握了这个流程后面遇到参数枚举、密码字典攻击、API接口遍历思路都是同一个先收集信息再构造自动化请求最后根据响应差异判断结果。只不过枚举的对象从PIN码变成了用户名、密码、验证码、订单号、文件ID等。换个说法就是这道题本质上是“用脚本做自动化尝试”这一大类问题的入门模型。2. 上手思路先静态分析再动手爆破2.1 打开开发者工具从源码里找线索拿到题目第一件事不是写脚本而是看源码。按F12打开开发者工具或者直接右键“查看网页源代码”。我习惯先CtrlF搜几个高频关键词pin、check、fetch、ajax、XMLHttpRequest。在这个页面里很快就找到了关键代码function checkPin() { var pin document.getElementById(pin).value; fetch(/check, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({pin: pin}) }) .then(r r.json()) .then(data { document.getElementById(result).innerText data.msg; }); }这段代码透露了几个信息前端点击按钮后会把输入框的值作为pin字段以JSON格式POST到/check拿到响应后只展示msg字段。注意整个前端代码里没有任何正确的PIN明文说明校验逻辑在后端完成静态分析阶段拿不到答案。这里有个对比如果校验写在前端比如if (pin 0735)那直接看源码就能过关根本不需要脚本。这道题叫“隐藏PIN”答案不在源码里而是藏在服务器的某个判断逻辑里所以才需要靠遍历请求去“问”服务器哪个是对的。2.2 确定爆破条件与枚举范围静态分析之后要确认爆破是否可行。我列了一下条件PIN是4位纯数字枚举空间就是0000到9999总共10000种组合接口没有验证码、没有尝试次数限制、没有锁定机制成功和失败的响应差异明显判定条件清晰10000次请求的规模本地服务几秒钟就能跑完公网环境只要不触发限流也就几分钟。这个规模就是“简单爆破”成立的前提。如果PIN变成6位数字那就是100万次请求时间成本和触发风控的概率都会显著上升解题思路就要重新评估了。我还顺手确认了一个细节PIN允许前导0。也就是说正确的PIN可能是0735这种以0开头的数字。所以枚举要从0000开始不能只从1到9999。这个细节单独拎出来说是因为很多人习惯用for i in range(1, 10000)然后拼字符串结果正确PIN带前导0时永远试不到。2.3 手动验证一次请求构造脚本只需三步写完脚本前我先用curl手动发了一个请求把链路彻底打通curl -X POST http://127.0.0.1:5000/check \ -H Content-Type: application/json \ -d {pin:0000}返回结果{success: false, msg: wrong pin}。这个操作有价值确认了服务端确实会响应、确认了请求格式正确、确认了失败响应的具体长什么样。现在构造脚本的思路就非常清晰了三步循环生成PIN字符串、POST到接口、判断响应里success字段是否为true。3. 编写Python爆破脚本从0到1的完整过程3.1 环境准备Python、requests与工具选择脚本语言选Python这是CTF圈子的默认选择没有悬念。需要确认本地有Python3环境命令行执行python --version能正常输出版本号。然后是安装requests库pip install requests为什么用requests而不是Python自带的urllib最直接的原因是可读性和开发效率。requests的post方法自带json参数传一个字典进去它会自动序列化成JSON字符串还会自动设置Content-Type: application/json这两步在urllib里都要手动处理。另外requests的异常体系更清晰ConnectTimeout、ConnectionError这些异常对象可以直接捕获方便做容错。如果你更习惯命令行工具用curl配合shell循环也能做但解析JSON、判断success字段会麻烦很多。所以只要目标是“练习脚本”Python就是首选。3.2 顺序遍历版本从0000试到9999主脚本的逻辑不复杂完整代码如下import requests url http://127.0.0.1:5000/check found False for pin in range(10000): pin_str f{pin:04d} try: resp requests.post(url, json{pin: pin_str}, timeout3) data resp.json() if data.get(success): print(f[] 找到 PIN: {pin_str}) print(f[] 响应内容: {data}) found True break except Exception as e: print(f[-] 请求 {pin_str} 失败: {e}) continue if not found: print([-] 遍历完成未找到正确 PIN)逐个看关键点。range(10000)生成0到9999的整数序列注意是从0开始。f{pin:04d}是格式化字符串作用是把数字补齐到4位0变成“0000”1变成“0001”9999还是“9999”这就解决了前导0问题。requests.post里传json{pin: pin_str}requests自动处理序列化和请求头。resp.json()把响应体解析成字典。data.get(success)在success为true时进入命中分支打印结果并退出循环。timeout3很关键如果某个请求卡住3秒后会抛异常脚本不会无限挂起。异常处理这里直接continue单次请求失败不影响整体遍历。这个版本的脚本逻辑完整也是我建议新手先跑通的版本——不要一上来就写并发顺序版跑通一次你对整个流程的掌控感会强很多。3.3 加进度反馈与异常容错跑起来才安心上面这个版本有个体验问题10000次请求如果服务端响应快几秒钟就结束中间没有任何输出。一旦服务端响应慢脚本就可能像个死掉的黑窗口你完全不知道它跑到哪了。所以我习惯加一个进度提示不用引额外依赖一个取模判断就行if pin % 500 0: print(f[*] 已测试 {pin} 个组合当前 PIN: {pin_str})每测试500个组合打印一次心里有底。另一种做法是引入tqdm库直接显示进度条但入门阶段没太大必要多装一个库不如多理解一行循环。异常处理上我只在真正失败时打印错误信息成功时不打印任何无关输出避免命中信息被刷屏淹没。3.4 跑通后的验证用脚本结果回到页面确认脚本跑完输出大概是这样的[*] 已测试 0 个组合当前 PIN: 0000 [*] 已测试 500 个组合当前 PIN: 0500 [*] 已测试 1000 个组合当前 PIN: 1000 ... [] 找到 PIN: 0735 [] 响应内容: {success: True, flag: flag{pin_brute_force_demo}}找到0735后我拿着这个PIN回到网页上手动输入一次页面显示“验证成功flag: flag{...}”和脚本输出的flag一致。这一步验证很重要它确认了脚本的判断逻辑没有误报。有时候你会遇到接口异常返回奇怪内容恰好被误判成成功如果不做手动复核后面拿到的就是个假flag。3.5 进阶小改动用线程池把速度提上来顺序版本跑通后如果想提速可以用concurrent.futures里的ThreadPoolExecutor做简单并发。我先说明白入门阶段并发不是必须的1万次顺序请求已经很快公网环境这样做反而容易触发防护。但对于本地模拟题或者没有限流的靶场这是一个很自然的进阶练手点。import requests from concurrent.futures import ThreadPoolExecutor, as_completed url http://127.0.0.1:5000/check def try_pin(pin): pin_str f{pin:04d} try: data requests.post(url, json{pin: pin_str}, timeout3).json() if data.get(success): return pin_str, data except Exception: pass return None, None with ThreadPoolExecutor(max_workers10) as pool: futures [pool.submit(try_pin, pin) for pin in range(10000)] for future in as_completed(futures): pin_str, data future.result() if pin_str: print(f[] 找到 PIN: {pin_str}) print(f[] Flag: {data.get(flag)}) pool.shutdown(waitFalse) break这个版本的思路是把1万个PIN分给10个工作线程谁先试中就通知主线程结束。注意pool.shutdown(waitFalse)能尽早打断还在跑的线程避免找到答案后脚本还在空跑。并发版只推荐在确认接口没有限流后再用否则你会看到一片429错误。4. 运行脚本时常见的四个翻车现场与排查技巧4.1 请求格式对不上参数名、JSON还是表单回过头来复盘几个我真实踩过、也看别人踩过的坑。第一个是参数名和请求格式。题目接口要的字段不一定是pin可能是pin_code、passcode、password之类的名字。如果你从浏览器Network面板里看到的是{pin_code: 1234}脚本里却传了{pin: 0000}服务端要么报参数错误要么一直判失败。还有个常见问题是数据格式。很多后端接口接收的是表单格式application/x-www-form-urlencoded不是JSON。requests里对应两种写法# JSON格式 requests.post(url, json{pin: pin_str}) # 表单格式 requests.post(url, data{pin: pin_str})区别就是json参数换成data参数。怎么判断去看Network面板里请求的Content-Type和请求体长什么样。解决原则只有一条让脚本发的请求和浏览器发的请求一模一样拿浏览器的请求做模板。4.2 请求太频繁被限制限速与延迟方案如果脚本跑了一会儿响应开始出现429或者403或者连续返回“操作过于频繁”之类的提示说明触发了服务端的简单风控。这时候要做的是降速不是换IP硬刚——CTF练习题一般只是轻微限制暴力对抗反而破坏题目环境的稳定性。给requests加一个延迟最简单的写法是time.sleep(0.05)每发完一个请求睡0.05秒。有些题目限制更严格可以改成每5次请求睡0.1秒或者用一个随机小数抖动延迟时间模拟人类操作。代价是遍历时间变长10000次请求每次多0.05秒总耗时增加500秒接近10分钟。所以有这种限制的题目你要先评估等不等得起如果等不起说明问题可能另有思路——比如PIN位数没猜对或者根本不该用爆破。4.3 响应不是预期JSON先看text再谈解析有的服务端在没找到PIN时返回的是200状态码加一段HTML而不是JSON。这时候脚本里的resp.json()会直接抛JSONDecodeError整个遍历中断。我之前就遇到过这种情况最后发现服务端把错误提示渲染成了HTML片段需要通过resp.text里的关键字来判断。排查方法是先打印一次完整的resp.text看看真实返回内容是什么。比如返回内容里包含fail字样那就用文本匹配不解析JSONtext resp.text if success in text or flag in text: print(f[] 找到 PIN: {pin_str})还有一种情况是服务端在验证成功后返回302跳转而不是业务响应。处理办法是加参数allow_redirectsFalse然后看响应的Location头或者原始响应内容来判断是否成功。4.4 误判与卡死问题timeout、重试与业务字段判断最后一个高频问题集中在一个点上误判。我见过不少同学写完if resp.status_code 200就直接当成功处理这是最大的坑。HTTP 200只代表请求到达了服务器并正常返回不代表PIN正确。服务端可能对错误PIN也返回200只是success字段是false。要判断业务语义必须看响应体里的业务字段而不是HTTP状态码。另外网络异常和业务失败要分开。一个请求因为连接超时抛了异常你把这个异常吞掉再判成功那叫误报。正确的做法是异常归异常打印之后跳过正常返回之后再根据业务字段判断。最后对于那些不确定成功还是失败的响应宁可多判断一次也不要急着中断脚本。可以在命中后把对应的请求再手工重发一遍做复核。5. 从这道题延伸出去的常见变体与后续练习建议5.1 校验逻辑放前端时的处理方式如果PIN校验逻辑直接写在前端JS里情况就变了。比如源码里有这么一段if (pin 0735) { // 验证通过 }这种题目严格来说不叫爆破叫源码审计你直接看重源代码就能拿到答案。但另一种变体更常见PIN经过一层变换比如用某个函数处理后再比较。function checkPin(pin) { var transformed ; for (var i 0; i pin.length; i) { transformed String.fromCharCode(pin.charCodeAt(i) 1); } return transformed 0846; }这里正确的PIN经过每位ASCII码加1后变成了“0846”反推回去正确答案是“0735”。遇到这类逻辑推荐用脚本模拟变换逻辑做一个简单的本地枚举而不是盲猜。核心还是要读懂代码明白变换规则再决定用脚本暴力遍历还是直接手算。5.2 字符集不只有数字通用枚举写法有些题目PIN不限定纯数字可能是4位数字加小写字母。这时候再用range(10000)就不行了因为字符集变成了string.digits string.ascii_lowercase。通用写法是用itertools.product生成所有组合import requests import itertools import string charset string.digits string.ascii_lowercase url http://127.0.0.1:5000/check for pin_tuple in itertools.product(charset, repeat4): pin_str .join(pin_tuple) data requests.post(url, json{pin: pin_str}, timeout3).json() if data.get(success): print(f[] 找到 PIN: {pin_str}) break写这个循环前先算一下枚举空间字符集是36个字符长度4总组合数是36^4 1679616比纯数字的1万大了160多倍。如果长度是6就是36^6已经超过20亿顺序遍历完全不现实。所以写脚本前用字符集大小和长度估算枚举空间是必须养成的习惯。5.3 连接到哈希验证的本地枚举变体还有一类常见变体题目不要求你发请求而是给了一个固定哈希值让你反推PIN。这种题更适合本地枚举速度极快也不需要网络请求。比如题干给出一个md5值说明某个4位数字的md5等于它那么脚本是这样import hashlib target_hash e10adc3949ba59abbe56e057f20f883e for i in range(10000): pin f{i:04d} if hashlib.md5(pin.encode()).hexdigest() target_hash: print(f[] PIN: {pin}) break这个例子的target_hash我随便写了一个已知的弱口令哈希真实题目会换成自己的值。本地枚举的意义在于哈希计算在CPU上跑得极快微秒级一次1万个组合瞬间完成。这给你一个思路爆破不一定只会发HTTP请求凡是从有限集合里找匹配的问题都可以用枚举来解决。5.4 这道题之后的脚本练习路径把这道“简单爆破PIN”做透之后往后可以沿着几个方向继续练用requests.Session保持Cookie模拟需要登录后的验证流程用并发处理更大的枚举任务同时学会观察什么场景不能并发把“先抓包、再写脚本、再验证”的流程固化下来这是CTF Web方向入门阶段最重要的基本功。我个人做完这道题最大的体会是脚本爆破本身不复杂真正拉开差距的是写脚本之前的信息收集。你是从哪里看到请求路径的请求参数是JSON还是表单成功响应靠哪个字段判断这三件事想清楚脚本其实就是循环加判断。下次再碰到类似的“隐藏XX”题目不妨先花几分钟把请求逻辑摸透再动手敲键盘效果会好得多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。