1. 前端加密在拦谁:先搞懂漏洞测试为什么会被卡住
做渗透测试的朋友一定遇到过这种场景:目标系统开了登录接口,密码在浏览器开发者工具里看到的是明文,但在Burp Suite里面抓到请求包,发现password字段全是一长串看不懂的密文。你以为是自己抓包姿势不对,改了密文里的一个字符再放行,服务器直接给你返回“参数错误”。这时候如果有人告诉你“你得先把payload加密之后再发过去”,你可能会更懵:这前端加密到底是绕过了,还是没绕过?
先说清楚一个事实:前端加密从来不是为了“绝对安全”设计的。它就是一道前置的“门禁”,目的是让攻击者不能直接用明文去操作接口。服务器拿到密文以后,自己会解密、再校验,所以真正的业务逻辑漏洞(SQL注入、越权、逻辑绕过等)其实还存在于后端,只是你只能看到密文,没办法直接把注入语句塞进去。换句话讲,前端加密拦的不是“漏洞”,而是“你直接测试漏洞的这条路”。
我见过不少新手在这个阶段选择放弃,觉得目标系统“加密做得好、很安全”。但换个角度想:如果前端加密真的能挡住漏洞,那为什么还能反复爆出验证码绕过、短信轰炸、优惠券薅羊毛这些大新闻?因为这些漏洞的攻击面根本不在加密本身,而是在加密之后的业务逻辑上。只要你能想办法在正确的位置、以正确的方式把测试数据送到服务器,后面的测试内容跟普通Web渗透测试几乎没有区别。
所以这篇文章的定位就是:把“被前端加密拦住之后怎么办”这件事讲透。我会从识别加密逻辑、修改前端行为、调用加密函数、构造合法密文等方面走一遍完整的测试链路。这套方法适用于登录接口测试、搜索接口注入、越权测试、ID枚举等场景,也是我平时做授权测试时反复用到的套路。适合刚接触Web渗透测试、希望从“只会用工具扫描”进阶到“能自己分析接口逻辑”的读者。
2. 先看清你面对的是哪种加密:一次完整的加密逻辑识别
你不能一上来就想着“绕过”,那等于闭着眼睛拆炸弹。我建议按下面这个顺序去识别前端加密的类型和位置,整个过程十分钟之内就能完成。
2.1 常见前端加密算法与特征识别
前端加密绝大多数是依赖JavaScript实现的,运行在浏览器环境里。常见类型大致可以分为几类:
- 哈希类:MD5、SHA1、SHA256等。特征是密文固定长度(比如MD5是32位十六进制),没有密钥,理论上不可逆。常用于密码传输前的摘要处理。
- 对称加密:AES、DES、3DES。特征是密文长度跟明文长度有关系,可能带填充,需要一个密钥。常用于整个请求体的加密。
- 非对称加密:RSA、ECC。特征是会生成公钥和私钥一对,前端用公钥加密,后端用私钥解密,适用于密钥交换或密码加密。
- 编码类:Base64、Hex、URL编码。严格来说不是加密,只是编码,一眼就能看出来,但很多系统会套几层编码来“增加难度”。
- 自定义混淆:有些系统会把加密逻辑写在一起,甚至用JavaScript混淆工具处理过,需要你花更多时间去还原逻辑。
怎么快速判断?第一步是看密文的格式。在Burp Suite里面抓到包以后,先看加密字段的长度和字符集:全是0-9a-f且长度是16、32、64,优先怀疑MD5或SHA系列;结尾带有“==”的,大概率是Base64;密文长度和明文字节数有明显对应关系(比如16的倍数),可能是AES;密文特别长,长度在128字节或256字节左右,字符集混乱,可能是RSA。
我之前测过一个后台系统,它的登录密码是“小写字母+数字”组成的32位字符串,一开始我以为是MD5,结果去解的时候发现解不出来。后来看了站点的JS,发现它先是把明文密码做了MD5,接着又把MD5结果跟一个时间戳拼在一起,再做了一次MD5。所以经验是:只看密文长度可以缩小范围,但不能确定算法,必须配合JS代码来确认。
2.2 用开发者工具定位加密函数
打开浏览器开发者工具(F12),切到Sources(源代码)面板,然后刷新页面,看加载了哪些JavaScript文件。不要急着去读所有文件,先在全局搜索框里搜几个关键词:
- encrypt(加密)
- decrypt(解密)
- AES、RSA、DES、MD5
- setPublicKey、publicKey(公钥相关)
- CryptoJS(一个非常常见的前端加密库)
如果你看到引用了CryptoJS这个库,那么加密算法基本就是AES、DES、SHA系列这些标准算法;如果看到jsencrypt这个库,那就是RSA加密。我见过不少系统直接把整个库文件打进来了,一眼就能认出来。
定位到具体函数之后,在Sources面板里给加密函数那行打一个断点。然后回到页面,随便输入一个测试账号密码,点击登录。当JS执行到加密函数的位置时,程序会自动断住。此时你可以在右侧的Scope(作用域)面板里看到当前变量的值,比如明文密码、密钥、加密后的结果等。这样你就能弄清楚整个加密过程:明文进去,经过哪几步变换,最终变成什么密文。
这一步做完,你对目标加密逻辑的理解应该达到90%以上。剩下10%是某些系统会把密钥动态获取,比如从某个接口拉取一个token再当密钥用。这个我们在第4章里专门讲。
3. 绕过前端加密的几条路线:从改JS到直接调加密函数
识别完加密逻辑,接下来就是动手阶段。根据测试场景不同,绕过方式也不同。我把常见的情况分成五类,按优先级排序,你在实战中可以先从最容易操作的开始试。
3.1 最简单的方式:直接修改前端代码
有些系统的前端加密就是把登录按钮绑定了一个加密函数,你修改页面上的JS代码,让它在提交请求前不加密,或者在加密后顺手把明文也放进请求里,这样Burp里就能看到明文参数。听起来很暴力,但确实有效。
操作大概是这样:在开发者工具的Sources面板里找到加密入口函数,右键点击“Overwrite content”或者直接用本地替换(Snippets)功能改代码。把它加密完的返回值改成明文,或者干脆在加密函数下加一行console.log("明文:" + password),先确认明文在内存里的位置。然后你在控制台手动调用加密函数生成密文,把密文拿到Burp里面去替换掉原有密文即可。
这里有一个关键点:修改前端JS只会影响当前页面的运行,服务器完全不知道你改了前端。所以如果你改了页面上的加密逻辑,并且页面上其他功能依赖这个加密函数,那有可能会引起报错。我建议修改前先保留原JS代码,测试完立即恢复。
3.2 直接调用加密函数,拿到合法密文
这是我最推荐的通用方案。既然服务器只认密文,那你就在浏览器控制台里主动调用站点本身的加密函数,把你想发的payload加密成合法的密文,然后把密文放进Burp重新发送。
具体来说,如果前端用了CryptoJS库,控制台执行:
CryptoJS.AES.encrypt("admin' or '1'='1", key).toString()就能生成一个合法的AES密文。如果用了jsencrypt:
var encryptor = new JSEncrypt(); encryptor.setPublicKey(publicKey); var encrypted = encryptor.encrypt("admin' or '1'='1");关键是函数名和参数顺序。这些信息在你打完断点之后就已经全部暴露了,所以识别环节一定不能省。
这种做法的优势是:你不需要修改页面任何代码,服务器收到的密文跟正常用户生成的一模一样,校验也能通过。缺点是需要你在控制台做一次“加密测试”,然后再把结果复制到Burp里。遇到参数多、字段多的情况,效率会有些低,所以更推荐配合Burp插件自动化处理。
3.3 在Burp Suite里加解密:要改包的话这样最省力
Burp Suite的“插件机制”提供了BoringsBurp等第三方扩展,里面的cyber-chef之类的处理器可以在请求和响应之间自动加解密。不过大多数国产系统加解密逻辑复杂,内置处理器不一定匹配。我自己常用的做法是写一个小的Burp扩展插件,逻辑不复杂:请求发出前,用Python或Java调用同样的加密算法处理payload,再替换到请求参数里。
这个方法的好处是可以在Intruder爆破的时候自动加密每个字典值,不用你手动一条条去加密。坏处是对编程能力有一点要求。不过如果你只是做少量测试,用Python脚本跑一遍字典,生成一份“明文-密文对照表”,然后把加密后的列表导入Burp的payload里面,也能达到同样效果。
提示:调用加密函数生成密文的时候,注意有些系统会加入时间戳、随机数或者会话绑定。如果密文里包含时间戳,你离线生成一大把密文后发现服务器不认,很可能就是这个原因。这种情况可以考虑在浏览器里直接用控制台生成,或者用脚本模拟一个同样的随机源。
3.4 如果密钥是从接口动态获取的,怎么处理
不少系统为了“安全”,不会在前端JS里写死密钥,而是登录前先请求一个接口拿一个token、一个时间戳或者一个随机key,然后拿这个key作为AES密钥或RSA公钥去加密。遇到这种动态密钥,直接离线加密就不太行了,因为你每次拿到的key可能都不一样。
解决办法是分两步:第一步,用Burp手工发一次完整的前置接口请求,拿到当前这次会话的key;第二步,在控制台里把key填进加密函数生成密文,然后立刻用Burp发送这个密文。由于前端是“拿key → 加密 → 提交”一气呵成的,你只要在几步之内完成操作,服务器多半不会做额外的时效校验。如果时效校验严格,可以尝试修改页面JS,把加密后的密文临时存到window全局变量里,然后再在控制台手动改值,这样能争取一点时间。
3.5 绕过之后别忘了:核心还是要测后端逻辑
前面这些操作,本质都是为了把测试数据“翻译”成服务器能识别的格式。等你能正常往服务器发送数据了,接下来的测试跟普通接口测试就一样了:SQL注入、XSS、越权、ID枚举、水平权限、逻辑绕过,该测的都要测。前端加密并不能拦截这些后端漏洞,它只能拦住你输入明文。一旦你能生成合法密文,漏洞面就全打开了。
我遇到过不少系统,前端加密做得很认真,AES+RSA都上了,结果登录后的“查看用户详情”接口直接能用另一个用户ID换取他人信息,妥妥的水平越权。这说明什么?说明那次测试真正的突破口根本不在登录接口,而是在业务接口上。你花大力气去搞前端加密,不如先把加密这关过了,然后把精力集中在后面的业务逻辑测试上。
4. 实战记录:被AES加密折磨的登录接口,我是怎么拿下漏洞点的
说了这么多理论,来一个完整案例。我在本地搭过Pikachu漏洞测试平台,也改过几套自己的靶机环境,其中有一台刻意加了一套前端AES加密来模拟真实系统的样子。接下来我按当时的操作顺序,把完整过程整理出来。
4.1 环境准备与抓包观察
目标站点是经典的登录接口。打开浏览器,输入账号密码,在Burp里打开拦截,点击登录。抓到如下请求:
POST /login HTTP/1.1 Host: 192.168.1.10 Content-Type: application/x-www-form-urlencoded username=admin&password=7c4c3f0b8c8e1a2b3c4d5e6f7a8b9c0dusername是明文,password是一段32位十六进制字符串。从长度和字符集看,MD5的可能性最大,但我没有急着下结论,而是先去看了JS。
打开开发者工具,Sources面板里发现一个login.js文件,内容不长,核心逻辑如下:
function encryptPassword(pwd) { var key = CryptoJS.enc.Utf8.parse("1234567890123456"); var iv = CryptoJS.enc.Utf8.parse("abcdefghijklmnop"); var encrypted = CryptoJS.AES.encrypt(pwd, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); }原来是AES-CBC加密,不是MD5。32位十六进制是因为Base64编码后的密文被转成了十六进制字符串。密钥和偏移量都写在JS里了,是硬编码的,这给测试省了很多事。
4.2 在控制台手动加密payload
我直接打开控制台,手动执行同样的加密逻辑:
var key = CryptoJS.enc.Utf8.parse("1234567890123456"); var iv = CryptoJS.enc.Utf8.parse("abcdefghijklmnop"); var encrypted = CryptoJS.AES.encrypt("admin' and '1'='1", key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); encrypted.ciphertext.toString();拿到密文之后,复制到Burp的password字段,发送请求,服务器返回了一串正常业务逻辑下的SQL报错。说明我的payload成功进入了后端SQL查询,前端加密被完全绕过了。
4.3 自动化生成一批payload密文
单条手工测试只能验证可行性,后面要测SQL注入的深度和注出数据量,不可能一条条去控制台加密。我的做法是用Python脚本复现同样的AES加密逻辑,批量生成密文。
from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 key = b'1234567890123456' iv = b'abcdefghijklmnop' def encrypt(payload): cipher = AES.new(key, AES.MODE_CBC, iv) padded = pad(payload.encode(), AES.block_size) encrypted = cipher.encrypt(padded) return encrypted.hex() payloads = [ "admin' or '1'='1", "admin' -- -", "admin' union select 1,2,3-- -", "admin'; waitfor delay '0:0:5'-- -" ] for p in payloads: print(p, "=>", encrypt(p))这段脚本生成的是十六进制密文,跟浏览器控制台生成的格式一致。拿到Burp里,放到Intruder的payload列表里直接跑就行。如果目标把密文转成了Base64,那就在脚本里输出base64.b64encode(encrypted).decode(),按需调整。
4.4 遇到动态密钥怎么办
继续往前测的时候,发现另一个系统的登录接口AES密钥不是写死的,而是藏在前置接口返回里:
GET /api/security-key { "key": "f8a2c1e3b4d5a6b7c8d9e0f1a2b3c4d5" }前端拿到这个key之后再去加密密码。这种场景下,我先把返回的key复制到控制台变量里,然后再调用加密函数生成密文。因为key每次可能都不一样,所以离线脚本的方法就不太好使了,只能借助浏览器控制台手动执行。
为了效率,我写了一段可以粘贴到控制台的一次性代码,把加密逻辑封装成一个函数:
function enc(pwd) { var key = CryptoJS.enc.Utf8.parse(currentKey); var iv = CryptoJS.enc.Utf8.parse("abcdefghijklmnop"); return CryptoJS.AES.encrypt(pwd, key, {iv: iv, mode: CryptoJS.mode.CBC}).toString(); }然后当前key变了,只需要在控制台更新currentKey这个变量,再调用enc("admin' or '1'='1")就能拿到新密文。整个过程不超过十秒,比每秒钟重启一次Burp抓前置接口效率高得多。
5. 实战中的那些坑:加密字段、编码、会话时效与弱随机数
绕过前端加密这件事,听起来无非就是“复制加密函数、生成密文、替换发送”,但实际操作中会遇到很多细节问题。这部分整理的都是我踩过的坑,比其他文章里容易略过的点要实在得多。
5.1 注意加密字段的编码问题
不少系统的加密结果不是直接用Hex或Base64传输,还会再做一层URL编码或者JSON转义。比如Base64编码结果里可能含有+、/、=,这些字符在URL里会被特殊处理。你在Burp里替换密文的时候,如果原请求里密文是URL编码后的格式,那你替换进去的内容也必须做同样的编码,否则服务器解码后拿到的是错误密文。
我之前就遇到过这个坑:脚本生成的Base64密文里有一个+号,直接放进请求,服务器返回解密失败。排查了一会儿才发现,Burp的请求里那个位置被自动做了URL编码,+变成了%2B。正确做法是发送前先对整个密文做一次URL编码,或者在脚本里直接输出URL编码后的结果。
5.2 时间戳和随机数的陷阱
为了防重放,很多系统会在加密内容里塞入时间戳、随机数或者会话ID。你抓到的是一个含有时间戳的密文,如果你把密文复制出去,隔了三五分钟之后再放回Burp发送,服务器校验时间戳已经过期,会直接拒绝。
遇到这种情况,最稳妥的方案是实时生成:在浏览器控制台里执行加密函数,立即把结果复制到Burp,马上发送。如果系统对时间窗口卡得很严(比如限定2秒内),那就要考虑用Burp插件或者Python脚本在发送前自动调用加密函数生成新密文,并把时间戳这部分动态更新进去。
5.3 弱随机数和固定IV的判断
有些系统的IV是固定的,甚至直接硬编码在JS里,如上面那个例子。这带来一个典型问题:同一个明文,在不同时间生成的密文完全一样。此时你可以用已知明密文对去推测密钥,或者在爆破的时候直接爆破明文,密文不用变,因为密钥和IV固定了,穷举效率会高很多。
反过来,如果系统使用了随机IV,那么同一个明文每次加密后密文都不一样,这时候你在爆破的时候就要给每个payload单独生成新IV和新密文,否则服务器会因为IV不符而拒绝。识别方法很简单:连续提交两次相同密码,观察密文是否相同。相同说明IV固定或使用ECB模式;不同则说明大概率是CBC模式加随机IV。
5.4 JS压缩与混淆代码的处理
真实系统里的前端JS往往经过压缩,所有变量名都变成了a、b、c这样的短名字,函数名也可能被改掉。遇到混淆得比较厉害的JS,直接在Sources面板里读代码会很痛苦。我建议先去找这个站点是不是用了webpack或类似的打包工具,如果是,很多模块会被合并成一个大的bundle文件,里面有大量代码。
这时候优先利用控制台,而不是去阅读源码。你可以在控制台里执行CryptoJS相关对象的方法,如果库还在全局范围内,直接调用就行。如果库被包在某个模块里没有暴露到全局,那就需要去页面源码里搜索关键词(比如“encrypt”),定位到具体位置,在那个函数上打断点,通过运行时的调用关系去获取密钥或明文。
我自己的习惯是:先打断点,再看调用栈,最后才去读源码。断点能直接告诉你这个函数被谁调用、传入什么参数、内部变量是什么值。这比站在源码外面猜要快得多。
5.5 别忘了会话绑定问题
有些系统的加密密钥虽然在前端JS里写死了,但服务器端在实际解密之后,还会校验密钥是否属于当前会话。比如登录接口的加密密钥可能是“临时token+固定密钥”拼接出来的,token由另一个接口下发给当前浏览器。如果你把密文从Burp里复制走,换一个环境发送,token可能对不上,服务器就会返回校验失败。
这种场景下,你要做的不是在Burp里改密文,而是把目标站点页面上JS里的token取出来,跟后续业务接口的token对比,看看它们是否是同一个值。如果是同一个,那加密逻辑和会话逻辑就绑定在一起了,测试的时候需要保证整个流程都发生在同一个会话内。我的做法是全程通过浏览器操作交互,Burp只做流量观察,只有确认具体要测某个接口时,才在Burp里单独构造请求并同时从页面里抓取当前token补进去。
6. 前端加密封不住的是业务逻辑:为什么还是要把重心放在接口层
前端加密这道关卡,说到底只是增加了测试的前期成本。真正决定系统安不安全的,仍然是后端接口的逻辑是否严密。我做了这么多年授权测试,见过太多“加密做得很花哨、业务逻辑烂成一团”的系统。前端加密能拦住脚本小子,但拦不住真正愿意去分析交互逻辑的人。
举几个实际见过的例子:
- 某系统登录接口做了AES加密,但注册接口完全没加密,而且注册接口存在用户枚举漏洞,通过返回信息能判断某个手机号是否已注册。
- 某系统所有前端参数都加密,但有一个导出功能,导出请求里的参数是明文,而且没有做权限校验,普通用户可以导出管理员数据。
- 某系统密码加密很扎实,但密码找回接口只校验“用户名+密保问题答案”,而且密保问题答案可以直接通过另一个未加密接口获取。
这些漏洞都是在前端加密之外暴露出来的。所以我的观点是:遇到前端加密,不要一头扎进去研究怎么破解它,而是先把它当作一个“提示”——提示你重点测哪些接口有保护,哪些接口可能没有保护。测试顺序上,优先测那些没有加密的接口、暴露参数更多的接口、涉及业务操作的接口。前端加密的接口集中在登录、支付、个人信息等核心位置,对应的后端逻辑往往相对完善;反而是那些“看起来不重要的”业务接口,更容易出现越权、枚举、逻辑绕过这类问题。
在实际授权测试里,我花在前端加密上的时间通常不会超过整个测试周期的20%,剩余时间都用在业务功能测试、权限绕过和漏洞利用上。前端加密只是第一道门,门开了之后,里面的世界才刚展开。
提示:本文所有方法仅用于授权测试、漏洞挖掘学习、安全加固自查等合法场景。未经授权对他人系统进行测试属于违法行为。建议想练手的朋友优先使用Pikachu、DVWA等本地漏洞靶场,或在有书面授权的环境中操作。安全测试的第一原则是合法合规。