最近把DVWA的brute force模块从Low一路打到High,发现Medium这一档特别有意思。它加了token校验,还塞了个sleep(1)的延时,乍一看比Low严谨不少,但真正动手爆破的时候会发现,这种防护只能拦住刚入门的新手,拦不住会看报文、会写脚本的人。
这篇文章就把我在Medium级别下的完整爆破过程、token处理的几种方案、以及踩过的坑一次说清楚。不管你用的是Burp Suite还是Python脚本,都能在这里找到能直接上手的思路。这套操作全部限定在DVWA本地靶场环境内,DVWA本来就是拿来练手的,别拿这套思路去对着线上系统瞎试,这点边界必须清楚。
1. DVWA暴力破解模块与实际环境准备
1.1 DVWA是什么,为什么练手选它
DVWA(Damn Vulnerable Web Application)是一个开源的PHP+MySQL漏洞靶场,它的设计目标就是给安全测试人员和爱好者提供一个合法的练习环境。和真正线上系统最大的区别在于,它每个漏洞模块都分成了Low、Medium、High、Impossible四个级别,同一款漏洞在不同级别下会有完全不同的防护策略,这种渐进式的设计非常适合用来理解漏洞的本质。
brute force模块就是经典的登录暴力破解实验。Low级别是直接用GET参数提交用户名和密码,服务端没有任何防御,只要遍历字典就行。Medium级别加上了user_token和1秒延时,High级别把token换成了不可预测的随机值且每次会话绑死,Impossible则加上了基于IP的锁定机制。从训练角度来说,Medium是理解"有防护但防护不彻底"的最佳样本。
1.2 启动靶场与登录环境细节
先说环境。我本地用的是Docker方式部署的DVWA,命令很简单:
docker run -d -p 8080:80 vulnerables/web-dvwa访问http://127.0.0.1:8080就能看到登录页。默认账号是admin,密码password。如果用的是本机直接部署的PHP环境,注意确认PHP版本和MySQL服务状态,DVWA老版本在新版PHP下偶尔会有session警告。
登录之后,第一次进会显示数据库没初始化,点一下页面底部的Create / Reset Database,初始化完成后会跳回登录页。这一步容易漏,很多人卡在"登录成功了但没有数据"就是这里的问题。
进入后左侧菜单找到Brute Force,打开就是登录表单。Level切换入口在DVWA Security页面,把安全级别切成Medium再切回Brute Force页面就能看到变化。
1.3 Medium和Low的核心差异(先说结论)
Medium比Low多了两个关键变化,这两个变化直接决定了爆破方案怎么写:
- 表单里多了一个隐藏字段
user_token,每次刷新页面都会生成一个新的随机值,服务端在校验用户名密码之前会先校验这个token,token不合法直接拒绝,并且不返回登录结果。 - 服务端在处理请求时增加了一个
sleep(1),也就是每次都延迟1秒才返回响应,目的是拉长爆破时间,降低自动化的效率。
但是注意,Medium级别的token校验只是"校验必须存在并且等于当前session的值",处理方式比较简单,可以通过每次请求前先取一个新token再提交来绕过。而且它没有锁定机制、没有验证码、没有IP封禁,这些都是Medium继续可以被爆破的根本原因。明白了这几点,后面无论是用Burp还是Python,思路都是围绕"如何自动获取token"展开的。
2. Medium级别防护机制拆解
2.1 user_token到底是什么,它防的是什么
先搞清楚token在这里的角色。DVWA的brute force页面返回的HTML里有这样一行:
<input type='hidden' name='user_token' value='e2a4f3c8...' />这个token是服务端在每次请求时生成的随机字符串,存到了当前PHP session里,同时渲染到表单中。当你提交请求时,服务端会把提交的token和session里的token比对,一致才继续处理用户名密码。
它是为了防CSRF(跨站请求伪造)设计的,不是专门防暴力破解的。攻击者如果想要伪造一个登录请求,就必须先拿到当前会话的token,这确实提升了一点门槛。但是,token就明晃晃地写在页面源码里,对于一个能直接访问目标页面的攻击者来说,先GET一次页面提取token再提交请求,几乎不增加实际成本。
这也解释了为什么很多站点的"防爆破token"形同虚设——只要token能在页面里被正常用户获取到,爆破脚本就能模拟正常用户一样拿token,关键在于脚本有没有自动化的提取能力。
2.2 sleep(1)延时的成本逻辑
Medium级别在server端源码里加了sleep(1),每一次请求无论密码对不对,服务端都会先睡1秒再返回响应。
这个延时的设计思路很简单:如果攻击者要跑1000个密码,每个请求多1秒,总时间就多1000秒,接近17分钟。如果字典更大,假设是100万条,那就是100万秒,约11天半。这确实能淘汰掉一批"直接拿Burp默认配置暴力跑"的初级做法。
但要注意几个现实问题:
- 这个延时是服务端固定返回的,攻击者如果把并发线程拉高,比如同时开20个线程,那么总时长并不会按单线程的1秒/请求线性增长,而是被并发摊薄了。
- sleep(1)也同时拖慢了正常用户的登录体验,如果真拿这个手段防爆破,牺牲的是所有用户的可用性。
- 在Medium级别下,它带来的最大影响其实是:测试时响应变慢,需要在工具里适当调整timeout参数,否则容易误判为请求失败。
我实测下来的感受是,sleep(1)对单线程脚本影响明显,但对并发方案只能算"增加成本",不能算"阻止攻击"。真正有效的防护还是得靠锁定、验证码、速率限制这些更重的手段。
2.3 为什么说Medium防护"看起来强,实则不够"
把Medium的防护逻辑完整串起来看,它的模型是这样的:请求进来 -> 校验token -> 校验用户名密码 -> 结果返回,整个流程还加上1秒延时。这个模型里面有三个可以利用的薄弱点:
- token获取成本极低,GET一次页面就拿到,而且token绑定的是session而不是IP,没有额外验证码干扰。
- 没有任何失败次数统计和锁定策略,可以无限试错。
- 校验逻辑基于"用户名和密码同时正确+token正确",其中用户名和密码是可以穷举的变量,token只是每次请求前取一下的"前置动作"。
这就相当于给大门上了一把好锁,但钥匙就挂在锁旁边。你只需要写一小段逻辑,让程序自动"取钥匙-开锁-再取钥匙-再开锁",循环往复即可。
从学习视角看,这就是Medium级别最值得品味的地方:它代表了很多真实系统的防护水平——有安全措施,但没有把措施落到"不可自动化绕过"的程度。理解这一点,比单纯机械地跑通一个爆破更重要。
3. Burp Suite 实操:抓包与分析
3.1 浏览器代理配置与报文观察
Burp Suite是全流程里最高效的图形化工具,先从它说起。
第一步是配置浏览器代理。我通常用Firefox配合FoxyProxy,代理设为127.0.0.1:8080。确认Burp Proxy模块的Intercept处于开启状态,然后回到DVWA的Brute Force页面,填一个随便的账号密码,比如admin/123456,点击Login。
这时请求会被拦截住,能看到请求报文长这样:
GET /vulnerabilities/brute/?username=admin&password=123456&Login=Login&user_token=e2a4f3c8... HTTP/1.1 Host: 127.0.0.1:8080 Cookie: PHPSESSID=abc123...; security=medium这里有个很容易被忽略的点:security=medium这个Cookie是负责指定当前安全级别的。如果在抓包过程中发现请求参数和之前不一样,排查一下是不是DVWA Security页面的级别没切换,或者是重新登录后Cookie丢了。
响应报文里要注意的是HTTP状态码和页面内容。密码错误的情况下,状态码仍然是200,但页面里会出现Login failed字样。所以爆破结果不能只看状态码,必须看响应内容里的关键字。
3.2 把登录请求丢给Intruder
确认报文结构无误后,右键这个请求,选择Send to Intruder。
在Intruder的Positions标签页里,需要标记的攻击位置有两个:一是username参数的值,二是password参数的值。这里我建议把用户名和密码都设为变量,用一个组合字典去跑,而不是只爆破密码。虽然很多场景下用户名是已知的,但作为攻击者做的完整一点没坏处,还能顺便验证"admin"是不是唯一可用的账号。
Payloads设置上,简单起见可以只爆破密码,用户名保持admin,Payload类型选择Simple list,从字典文件里加载密码列表。Grep-Match功能强烈建议配置:在Options里找到Grep - Match,添加Welcome和Login failed两个关键词。这样攻击结束后,可以直接通过匹配结果快速定位成功的那一条请求。
3.3 token处理的两种现实做法
直接把配置好的攻击跑出去,你会发现所有请求返回的都是Login failed,一个成功的都没有。原因是每次请求里的user_token字段是手工抓包时固定的旧值,服务端校验时发现和session当前值对不上,直接把请求拒绝了。
在Medium级别下,要让Intruder跑通,有两种现实做法:
第一种是正则提取+每次请求前更新token。Burp在Intruder里不能像脚本那样"先GET一下再POST",但可以通过Recursive Grep功能把上一个响应里的token提取出来,作为下一次请求的参数。具体在Intruder的Extract Grep里配置一个正则,比如:
name='user_token' value='([a-f0-9]+)'然后把token位置的Payload类型设为Recursive Grep。这种方案配置起来有点绕,而且免费版Burp对Recursive Grep支持有限,我通常不推荐新手在这里死磕。
第二种是用Python脚本代替Intruder,这也是我最推荐的方式。脚本里天然支持"先GET页面拿token,再POST提交"的循环逻辑,比在GUI里配Recursive Grep直观太多。Burp在这个场景下更适合做"分析报文"和"手工验证"这两个环节,真正的高并发爆破交给脚本。
3.4 结果分析与性能预期
如果按第一种方式把token处理好了,Intruder是可以跑出结果的。攻击结束后,在Results里看Grep匹配的列,如果某一行匹配到了Welcome,那这一行对应的就是正确密码。
但说实话,Burp Community版(免费版)的Intruder默认是低并发,加上Medium级别每个请求要等1秒,跑一个几千条的密码字典会非常慢。我试过跑一个5万条的字典,用免费版单线程,等了将近14个小时。这个体验实在太差,所以我后面基本转向了Python脚本。图形化工具适合理解流程、做小规模验证,真要大规模跑字典,脚本才是效率最优解。
还有一个细节:Burp的默认HTTP超时时间比较短,如果服务端sleep(1)加上网络延迟,可能偶尔会出现请求超时或者工具显示"Connection reset"。遇到这种情况,在Intruder的Options里把Request Timeout调大,比如10秒,能避免误判。
4. Python脚本爆破:更可控的方案
4.1 脚本思路与请求流程
Python方案的核心逻辑很清晰,就三步循环:先用session GET一次页面,从响应HTML里提取最新的user_token;再带着token、用户名、密码发起登录请求;最后根据返回内容判断成功还是失败。
用Session对象而不是requests.get直接调,是因为爆破全程需要保持同一个会话的Cookie,特别是PHPSESSID和security=medium这两个Cookie一丢,token就对不上了。
流程里的一个关键点是:发完登录请求后,服务端会返回一个新的页面,这个页面的HTML里会包含一个新的user_token。所以下一次循环时,不能直接用上一次请求前拿到的token,而是要从最新响应里再提取一次。这也是很多初学脚本时最容易错的地方——复用了旧token,导致后续请求全部被拒。
4.2 关键代码实现
下面是我调通的一个基础版本,注释写得很详细,直接复制下来改一下URL就能用:
import requests import re # 目标环境,改成你自己的地址 base_url = "http://127.0.0.1:8080" login_url = base_url + "/login.php" brute_url = base_url + "/vulnerabilities/brute/" # 需要登录DVWA,先建立会话 session = requests.Session() def get_login_token(): """打开登录页,提取user_token""" resp = session.get(login_url) token = re.search(r"name='user_token' value='([a-f0-9]+)'", resp.text) if token is None: raise Exception("未获取到登录token,请检查登录页HTML结构") return token.group(1) # 第一步:登录DVWA login_token = get_login_token() login_data = { "username": "admin", "password": "password", "Login": "Login", "user_token": login_token, } session.post(login_url, data=login_data) # 切到medium级别 session.get(base_url + "/security.php", params={ "security": "medium", "seclev_submit": "Submit", }) def get_brute_token(): """从brute force页面提取当前token""" resp = session.get(brute_url) token = re.search(r"name='user_token' value='([a-f0-9]+)'", resp.text) if token is None: return None return token.group(1) # 密码字典,按需加载 passwords = [ "123456", "password", "admin", "admin123", "root", "12345678", "qwerty", "letmein", ] found = False for pwd in passwords: token = get_brute_token() if token is None: print("token获取失败,页面结构可能变了") break params = { "username": "admin", "password": pwd, "Login": "Login", "user_token": token, } resp = session.get(brute_url, params=params) if "Welcome" in resp.text: print(f"[+] 爆破成功!密码是: {pwd}") found = True break else: print(f"[-] 尝试密码: {pwd} 失败") if not found: print("[-] 字典跑完,未找到正确密码")这个脚本跑的时候要注意,security=medium这个Cookie是通过访问security.php设置的,如果漏了这一步,后续请求会被当成Low级别处理,虽然也可能成功,但实验就不严谨了。
4.3 跑一轮的结果与踩坑记录
我实际用上面脚本在Medium级别下跑了一轮,用了大约2000条常见的密码列表。由于每次请求前都要先GET一次页面,再加上服务端的sleep(1),2000个密码大约耗时40分钟。如果密码对了,通常会在命中时很快停下。
这个过程中我踩过几个坑,整理一下:
- 第一个坑是正则写错了。DVWA不同版本里隐藏字段的HTML写法会有细微差异,有的是单引号,有的是双引号,有的是
value='xxx',有的是value="xxx"。如果脚本一直报"token获取失败",先打开页面源码看一眼实际格式。 - 第二个坑是请求方法。DVWA的brute force模块是GET提交,不是POST。我第一次用POST去提交,服务端参数没接收到,所有请求都返回异常。抓包看清楚再写代码,能少走很多弯路。
- 第三个坑是响应判断。判断成功不能只靠状态码,因为DVWA对所有登录请求都返回200。要用页面内容里的关键词,比如
Welcome或Login failed。有些版本的DVWA成功后会显示用户名,所以更好的写法是既检查Welcome又检查admin。 - 第四个坑是字典命中后没有及时break。如果循环不停止,就算找到密码,脚本还会继续跑完整个字典,浪费时间。找到后马上break退出。
5. 常见问题与避坑经验速查
5.1 登录态、token失效类问题
爆破半路突然全部失败,或者连续返回"CSRF token mismatch",十有八九是session过期了。DVWA的PHP session默认有效期很短,长时间爆破时尤其容易触发。如果遇到这个问题,看看是否需要在脚本里定期重新登录,或者干脆把session超时时间调长。
另一个常见问题是:脚本在同一个session里先GET了brute页面拿token,然后又用这个token去请求登录,但如果中间有另一个请求把session里的token刷新了,旧token就失效了。所以脚本里要保证"拿token"和"用token"是紧挨着的一对操作,中间不要穿插其他请求。多次调试下来,把token提取逻辑写在循环开头是最稳妥的。
Burp抓包和Python脚本混用时也容易出问题。如果同时开着Burp代理,浏览器和脚本的流量全走代理,可能因为Burp的session和脚本的session不一致导致token互相覆盖。建议做实验时要么全走Burp,要么关掉Burp只跑脚本,别两部同时抢同一个session。
5.2 字典选择与响应判断
字典质量直接决定爆破能不能出结果。DVWA默认的密码是password,但如果你改了密码或者换了题目要求,字典里没有目标密码,就是跑一晚上也跑不出来。建议从小的常见密码集开始试,比如rockyou.txt的前几百条,确认流程通了再加载大字典。
响应判断这一块,可以在脚本里加一个debug模式,打印最近一次请求的响应前200个字符。这样万一判断逻辑写错了,比如把失败当成成功,打印出来一眼就能发现问题。我在调试时见过很多人把"Login failed"写进了成功条件里,结果跑出来的"成功密码"全是错的,这就是判断写反了。
用Burp的Grep-Match功能时也同理,Welcome这个词在DVWA的成功响应里是稳定出现的,但它也可能出现在页面的导航或者说明文字里。最好再加上一个用户名关联的匹配,比如成功页面里会出现Welcome to the password protected area admin,就把Welcome to the password protected area作为匹配关键词更保险。
5.3 关于安全测试的边界提醒
做这一类实验,必须在自建的、授权的靶场环境里进行。DVWA、本地虚拟机、自己搭建的测试服务器都是合法练习的载体。把同样的思路放到别人的系统上,哪怕是内网测试机,只要没有书面授权,都已经越过了法律边界。
从技术人的角度讲,理解漏洞原理是为了写出更安全的代码、配置更完善的防护,不是为了去攻击别人。我在带新人做训练时一直强调:这个模块练的不是"怎么入侵",而是"如果攻击者这么想,我该怎么防"。Brute force模块跑到最后,最有价值的产出是你知道要加锁定策略、要上验证码、要做IP限速,而不是记住几个爆破命令。
如果确实有测试第三方系统的需求,务必先确认授权范围,拿到授权书或者测试合同,再开始任何形式的测试。安全测试这个行当,技术能力只是基础,职业素养和合规意识才决定你能走多远。
6. 从攻击视角反推防护建议
6.1 暴力破解为什么难彻底防住
跑完Medium级别,最有价值的收获其实是站在攻击者的角度理解了"为什么防护这么难做"。
暴力破解的本质是穷举用户名和密码的组合。只要密码空间有限、接口没有限制尝试次数,攻击者就可以一直试下去。token在一定程度上防止了"直接重放请求",但就像前面演示的,只要token能从页面里获取,攻击者就能自动化地提取和提交。sleep延时增加了时间成本,但对并发攻击者来说只是降低了速度,并没有从根上堵住暴力破解这条路。
这就解释了一个现实:单一防护手段几乎都没有绝对效果。token、验证码、延时、锁定、限速,每一样都有绕过或削弱的方法,只有把多种手段组合起来、配合日常的监控告警,才能把风险降到实际可控的范围。
6.2 实际工程里推荐的安全措施
从DVWA Medium的薄弱点可以映射出一套真实系统的防护基线。按优先级排的话:
- 服务端必须有限制登录失败次数的机制,比如同一用户名或同一IP连续失败5次后锁定15分钟。这个策略直接否定了无限穷举的前提。
- 登录接口要支持验证码或二次验证。验证码的价值在于增加自动化获取的难度,尤其是行为式验证码,能让大多数爆破脚本直接失效。
- 登录接口要有速率限制。即使不能完全阻止攻击,也要把尝试频率压到"用大字典跑不现实"的程度。
- 密码策略要合理,强制长度和复杂度,并且接入常见弱密码黑名单,从源头上缩小密码空间。
- 日志和告警要覆盖登录失败事件。如果大量IP在短时间内的失败次数异常增加,应该有自动化的告警和封禁动作。
这些措施在DVWA的Impossible级别里都有体现,比如基于IP的锁定、更严格的token管理。练完Medium再去看Impossible的源码,你会立刻明白"生产级别的防护"和"教学级别的防护"差距有多大。
6.3 一点个人体会
把DVWA的brute force模块认真跑一遍,比看十篇讲暴力破解的理论文章都有用。我在Medium级别上第一次跑出正确密码的时候,印象最深的反而不是"爆破成功了"这个结果,而是意识到:一个隐藏的token字段、一行sleep代码,确实能挡住大多数人,但也仅仅能挡住"没有自动化能力"的人。
安全领域就是这样,攻防两端的知识必须同时储备。你只有真正动手写过提取token的脚本、调过正则去分析响应报文,才能对token防护的局限有体感。下次自己写登录功能的时候,才会条件反射一样地问:这里是不是该加个失败锁定?这个接口会不会被脚本刷?
最后说一句心得:爆破这种技术本身并不高级,真正的高级在于你知道什么时候能用、怎么用才能不逾矩。把DVWA当练习场,把这些防护思路带到真实项目中,这样的学习才有价值。