Python实现账号密码爆破登录与Token会话维持实战

📅 发布时间:2026/10/20 18:29:21
Python实现账号密码爆破登录与Token会话维持实战
1. 从爆破登录这个词说起它到底在解决什么问题很多人第一次看到账号密码爆破登录这个说法脑子里冒出来的画面可能是电影里那种黑底绿字的终端噼里啪啦敲几下键盘就把别人系统攻破了。但如果你真的做过安全测试或者渗透测试相关的工作就会知道现实完全不是这么回事。所谓爆破登录在合法的技术语境下本质上是对自身系统做一次压力测试和弱口令排查——你是在验证自己的登录接口有没有做好防护用户是不是在用123456这种一撞就开的密码。我之所以想把这个话题单独拎出来写一篇是因为在实际工作中我发现大量开发者对登录安全的理解停留在密码加密存储这一个点上。他们觉得只要数据库里存的是哈希值登录就安全了。但实际上登录接口本身就是一个巨大的攻击面有没有做频率限制失败次数多了会不会锁定返回的错误信息会不会泄露账号是否存在登录成功后发的token有没有过期机制这些问题光靠密码加密是解决不了的。这篇内容我会围绕一个完整的Python实现来展开从模拟账号密码的批量尝试到登录成功后通过token维持会话状态。需要先明确一点这里所有的技术讨论都建立在对自己拥有完全权限的系统进行安全自查的前提下。任何未经授权对他人系统进行登录尝试的行为都是违法的这一点没有任何商量余地。我写这篇的目的是让你能站在防守方的角度理解攻击者可能怎么想、怎么做从而把自己的登录接口做得更扎实。适合阅读这篇内容的人大概有三类一是正在做Web后端开发、需要对自己登录接口做安全加固的工程师二是刚接触安全测试、想理解登录爆破原理和防护思路的初学者三是对token认证机制感兴趣、想搞清楚登录之后到底发生了什么的开发者。不管你是哪一类我都会尽量把原理讲透把代码写清楚把踩过的坑说明白。2. 登录爆破的技术底座HTTP会话与认证机制2.1 为什么登录接口天然容易被批量尝试要理解爆破得先理解登录接口的工作方式。一个最典型的登录流程是这样的客户端把用户名和密码通过POST请求发给服务端服务端拿着这两个值去数据库里比对比对成功就返回一个凭证可能是session ID也可能是token后续请求带着这个凭证就代表我已经登录过了。这个流程里有一个关键特征服务端必须对每一次登录请求都做出响应。它不能因为这个请求看起来像在猜密码就直接不处理因为正常的用户也可能输错密码。这就给了批量尝试的空间——只要服务端没有额外的限制机制理论上你可以用脚本每秒发几百个请求把常见弱口令挨个试一遍。我见过不少内部系统登录接口连最基本的失败次数限制都没有。你用一个简单的Python脚本配合一份几千条的常见密码字典几分钟就能把一批弱口令账号全部筛出来。这不是什么高深的技术纯粹是因为防护缺失。2.2 从session到token认证方式的演进逻辑早期的Web应用普遍用session来维持登录状态。服务端在内存或数据库里存一份session记录客户端拿到的只是一个session ID每次请求带上这个ID服务端去查表就知道你是谁。这种方式的问题在于服务端有状态。用户量一大session存储就成了瓶颈多台服务器之间还要做session共享部署起来很麻烦。token方式的出现就是为了解决这个问题。服务端不再存session而是把用户身份信息编码成一个token通常是JWT格式签名后发给客户端。客户端每次请求带上token服务端验证签名就能确认身份不需要查任何存储。这就是所谓的无状态认证。对于爆破测试来说这两种方式的区别在于session方式下你登录成功后拿到的是一个随机字符串后续请求把它放在Cookie里token方式下你拿到的是一个结构化的字符串通常放在请求头的Authorization字段里。理解这个区别对写自动化脚本很重要因为你要知道登录成功后该从哪里取凭证、后续请求该往哪里放凭证。2.3 爆破脚本的核心组件拆解一个完整的登录爆破脚本拆开来看其实就四个部分目标接口分析搞清楚登录请求发到哪个URL、用什么方法、需要哪些参数、有没有额外的校验字段比如CSRF token、验证码。字典准备用户名列表和密码列表。用户名可能是从其他渠道收集的也可能是按规则生成的密码列表通常是常见弱口令的集合。请求发送与结果判定用脚本批量发送登录请求并根据响应判断这次尝试是成功还是失败。判定依据可能是状态码、响应内容长度、响应中的某个字段值等。凭证提取与复用一旦登录成功从响应中提取token或session用于后续的接口访问。这四个部分里最容易出问题的是第三部分——结果判定。很多系统的登录失败响应并不是简单的200表示成功、401表示失败而是不管成功失败都返回200只在响应体里用一个字段区分。如果你不仔细分析响应结构脚本跑完了你都不知道到底有没有成功。3. 用Python把爆破流程跑通从请求构造到结果判定3.1 环境准备与依赖选择写这类脚本Python里最常用的两个库是requests和httpx。requests胜在简单稳定、文档丰富适合大多数场景httpx支持异步和HTTP/2在需要高并发的时候更有优势。我这里用requests来演示因为它的同步模型更容易理解调试起来也直观。pip install requests除了HTTP库你还需要一个东西字典文件。用户名和密码各准备一个文本文件每行一个。密码字典的质量直接决定了测试的效果常见的弱口令组合比如用户名123、admin888、password这类应该优先放在字典前面。提示字典文件建议按最可能命中的顺序排列因为很多系统在失败几次后就会锁定账号或增加延迟把高概率的密码放前面能提高效率。3.2 分析登录请求的真实结构在写脚本之前你必须先用浏览器开发者工具或者抓包工具把一次正常的登录请求完整看一遍。重点看这几项观察项说明常见坑请求URL登录接口的完整地址有些系统登录接口和页面地址不同请求方法通常是POST少数系统用PUT或自定义方法请求体格式JSON、表单、还是multipart格式不对直接返回400必填字段除了用户名密码还有啥可能有隐藏的csrf_token、timestamp响应结构成功和失败分别返回什么失败也可能返回200凭证位置token在响应体还是响应头有的在Set-Cookie里我踩过最典型的一个坑是某系统的登录请求里带了一个_csrf字段这个值是从登录页面里解析出来的每次都不一样。如果你直接构造请求不带这个字段服务端会直接拒绝你试一万次也试不出正确密码。所以先分析、再动手这一步省不得。3.3 构造登录请求与结果判定逻辑假设经过分析登录接口是POST /api/login请求体是JSON格式包含username和password两个字段成功时返回{code: 0, token: xxx}失败时返回{code: 1, msg: 用户名或密码错误}。import requests import json import time LOGIN_URL http://target.example.com/api/login USERNAME_FILE usernames.txt PASSWORD_FILE passwords.txt def load_list(path): with open(path, r, encodingutf-8) as f: return [line.strip() for line in f if line.strip()] def try_login(username, password): payload {username: username, password: password} headers {Content-Type: application/json} try: resp requests.post(LOGIN_URL, jsonpayload, headersheaders, timeout10) data resp.json() if data.get(code) 0: return True, data.get(token) return False, None except Exception as e: return False, None def run(): usernames load_list(USERNAME_FILE) passwords load_list(PASSWORD_FILE) for username in usernames: for password in passwords: ok, token try_login(username, password) if ok: print(f[] 命中: {username} / {password}) print(f[] Token: {token}) break else: print(f[-] 失败: {username} / {password}) time.sleep(0.5) if __name__ __main__: run()这段代码的逻辑很直白双层循环遍历用户名和密码每次尝试后根据响应里的code字段判断是否成功。成功就打印出来并跳出内层循环失败就继续。但这里有几个细节值得展开说。第一time.sleep(0.5)是故意加的延迟。不加延迟的话请求发得太快一方面容易触发服务端的频率限制另一方面也可能把目标系统打挂。做安全测试要有分寸别把测试变成拒绝服务。第二异常处理里我直接返回了False这是简化写法。实际使用中你应该区分密码错误和网络超时这两种情况因为后者不代表密码不对可能是网络抖动需要重试。3.4 登录成功之后token的提取与复用拿到token只是第一步真正有价值的是用这个token去访问需要认证的接口。token的用法取决于服务端的实现常见的有两种放在请求头里Authorization: Bearer token放在Cookie里Cookie: sessiontokendef access_protected(token): headers {Authorization: fBearer {token}} resp requests.get(http://target.example.com/api/userinfo, headersheaders) return resp.json()这里有个容易忽略的点token是有有效期的。有些系统的token几小时就过期有些能撑好几天。如果你在做长时间的测试需要在token过期后重新登录获取新的。另外有些系统会在你每次请求时刷新token的有效期滑动过期这种情况下只要你持续访问token就不会失效。4. 那些让我卡了半天的坑爆破脚本的实战排错4.1 响应判定错误明明成功了却显示失败这是我遇到最多的问题。有一次测试一个系统登录成功和失败都返回HTTP 200我一开始用状态码判断结果所有尝试都被判定为成功输出刷了一屏。后来仔细看响应体才发现成功时返回的JSON里有个success: true失败时是success: false。还有一种更隐蔽的情况成功和失败的响应体长度不一样但结构相同。比如成功时返回的token字段比较长失败时msg字段比较短。这种情况下你可以用响应内容的长度作为辅助判断依据但最可靠的还是找到那个明确区分成功失败的字段。排查这类问题的思路是手动发一次正确的登录和一次错误的登录把两个响应完整对比一遍。用diff工具或者直接肉眼比对找出差异点。这个差异点就是你的判定依据。4.2 频率限制与账号锁定脚本跑着跑着就失灵了很多系统有登录失败次数限制比如连续失败5次就锁定账号15分钟。如果你的脚本跑得太快可能前5次尝试都是正常的失败响应第6次开始就变成了账号已锁定的响应。如果你不识别这个响应会误以为密码不对继续傻傻地试。更麻烦的是IP级别的频率限制。有些系统不锁定账号但会限制单个IP的请求频率超过阈值就返回429或者直接封IP。这种情况下你的脚本会突然全部失败而且换密码也没用。应对方法有几个一是降低请求频率加足够的延迟二是识别特殊响应一旦发现账号锁定或请求过于频繁的提示就暂停一段时间再继续三是控制尝试次数对同一个账号不要无限试下去设定一个上限。注意做安全测试前一定要和目标系统的负责人确认好测试范围和方式避免因为频率限制导致正常用户被误伤。4.3 验证码与CSRF token自动化路上的拦路虎验证码是爆破脚本的天敌。一旦登录接口带了图形验证码纯脚本方案基本就走不通了。有些简单的验证码可以用OCR识别但准确率有限复杂的验证码比如滑块、点选就需要更复杂的方案成本很高。CSRF token则是另一种情况。它不是用来阻止自动化的而是用来防止跨站请求伪造的。但它的存在确实给脚本增加了工作量——你需要在每次登录前先请求登录页面从HTML里解析出token值再带着这个值去发登录请求。import re def get_csrf_token(session): resp session.get(http://target.example.com/login) match re.search(rnamecsrf_token value([^]), resp.text) return match.group(1) if match else None这里的关键是用同一个session对象来保持Cookie。因为CSRF token通常和session绑定你用不同的session去请求token就对不上。4.4 编码与特殊字符密码里的坑密码字典里如果包含特殊字符比如、、在表单提交时可能需要进行URL编码。如果你用的是requests的data参数它会自动帮你编码但如果你手动拼接请求体字符串就可能出问题。还有一种情况是密码里包含中文或emoji。不同系统对密码字符集的支持不一样有些系统在注册时允许中文密码但登录接口在处理时编码方式不一致导致你明明输对了密码却登录失败。遇到这种情况先确认目标系统的编码方式通常是UTF-8再检查你的脚本有没有做对应的编码处理。5. 站在防守方如何让你的登录接口扛住爆破5.1 频率限制与账号锁定的正确实现既然我们知道了爆破的原理防守的思路就很清晰了。最基础的两条限制单IP的请求频率和限制单账号的失败次数。频率限制可以用令牌桶或滑动窗口算法来实现。简单说就是给每个IP维护一个计数器单位时间内超过阈值就拒绝。账号锁定则是记录每个账号的连续失败次数超过阈值就锁定一段时间。但这里有个平衡问题锁定策略太严格会影响正常用户。比如你设置失败3次就锁定24小时那用户手滑输错几次密码就被锁一天体验极差。常见的做法是失败5次锁定15分钟或者采用递增延迟——第一次失败等1秒第二次等2秒第三次等4秒以此类推。防护策略优点缺点建议固定次数锁定实现简单可能误伤正常用户5次失败锁15分钟递增延迟不影响正常用户对分布式爆破效果有限配合IP限制使用IP频率限制能挡住大部分脚本可能误伤共享IP的用户阈值设宽松些验证码效果好影响自动化体验失败3次后触发5.2 token设计中的安全细节token不是随便生成一个随机字符串就完事了。如果你用的是JWT有几个关键点必须注意签名密钥要足够强用弱密钥的话攻击者可以自己伪造token。密钥长度至少32字节且要随机生成。设置合理的过期时间token有效期太长一旦泄露影响面就大太短又会影响用户体验。一般access token设15分钟到2小时配合refresh token来续期。不要在token里放敏感信息JWT的payload是base64编码的不是加密的任何人都能解码看到内容。所以密码、身份证号这类信息绝对不能放进去。考虑token撤销机制无状态token的缺点是无法主动撤销。如果用户登出或者token被盗服务端没法让这个token立即失效。解决方案是维护一个黑名单或者用短有效期refresh token的组合。5.3 日志与监控让爆破行为无处遁形再好的防护也有被绕过的可能所以日志和监控是最后一道防线。你需要记录每一次登录尝试的时间、IP、用户名、成功与否。然后设置告警规则比如单IP在1分钟内失败超过20次单账号在10分钟内失败超过10次同一IP尝试了大量不同的用户名说明在撞库这些异常模式一旦出现就应该触发告警让安全人员介入。很多系统被爆破了很久都没人发现就是因为没有监控。我在实际工作中还见过一种情况攻击者用大量不同的IP、每个IP只试一两次这种低频慢速的爆破很难通过频率限制挡住。对付这种就需要更上层的风控策略比如设备指纹、行为分析等。当然这是另一个话题了。6. 关于这套东西我的一些个人体会写到这里我想聊几句代码之外的东西。登录爆破这个技术本身并不复杂一个稍微有点Python基础的人看几篇教程就能写出能跑的脚本。但真正重要的是你用它来做什么。我见过太多人把精力花在怎么绕过防护上却很少去想怎么把防护做好。其实这两件事是一体两面的——你只有理解了攻击者会怎么做才能设计出真正有效的防护。反过来你在做防护时踩过的坑也会让你在写测试脚本时少走弯路。另外一点体会是不要迷信任何单一防护手段。频率限制可以被分布式IP绕过验证码可以被OCR识别账号锁定可以被低频慢速绕过。真正有效的防护是多层叠加的——频率限制验证码行为分析日志监控每一层都不完美但叠在一起就能把攻击成本提高到不划算的程度。最后说个实操中的小技巧。如果你在做自己系统的安全自查建议先在测试环境跑一遍完整的爆破流程确认脚本能正常工作、结果判定准确再考虑在生产环境做受控测试。生产环境测试一定要提前通知相关同事控制好频率和范围别因为一次测试把线上服务搞出问题。这个教训我是真金白银换来的希望你别再踩一遍。