ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JS前端安全实战:DOM操作、加密识别与断点调试全解析

JS前端安全实战:DOM操作、加密识别与断点调试全解析

1. 先从执行环境说起:为什么JS是前端安全的第一道门

前一阵子我在一个Web安全系列课里翻到第29天的内容,主题是JS应用与前端逆向。说句实话,做渗透测试的人如果只会用Burp抓包改包,早晚会遇到瓶颈——现在稍微有点规模的应用,前端逻辑早就不是十年前那种“按下按钮直接提交”的结构了。参数会加密、字段会动态生成、行为会经过JS动态渲染,整个页面背后其实是一套运行在浏览器里的微型程序。你今天看到一个登录框,它背后可能有七八个JS文件在做校验、拼参数、算签名、渲染结果,这些逻辑全在前端,也就意味着全都能被调试和观察。这节课的价值,就是教你把浏览器当成一个“会亮出内部状态的观察窗”。学完之后你会对自己说:原来以前只是在看页面表面,现在能看到页面背后在跑什么逻辑了。

这节内容适合谁?如果你是刚接触前端安全的初学者,这堂课算是一条捷径;如果你是做爬虫、写自动化脚本的老手,这部分对你同样有用,因为你会知道对方在前端埋了哪些加密步骤,也就能知道该在哪个环节下手绕过。整天的内容主要围绕六个关键词展开:JS应用、DOM树、加密编码库、断点调试、逆向分析、元素属性操作。它们不是孤立的,而是串成一条链——从页面结构出发,沿着代码执行路径,找到数据被加工的位置,再决定是分析还是绕过。

讲得通俗一点,前端安全本质上是在回答三个问题:页面是谁构造的、数据是经过什么处理才变成请求的、我怎么在浏览器里把这些处理过程“重放”或“修改”。这三个问题都离不开对JS的理解。我这篇就按那天课程的主线,把这套思路完整拆开来讲,每一个环节都尽可能带上能直接上手的操作方式和我踩过的坑。

2. DOM树与元素属性操作:页面结构里的突破口

2.1 先建立DOM直觉:页面在你眼里不该是“图片”,而是一棵能改的树

DOM(Document Object Model)是浏览器把HTML解析之后形成的一棵节点树。很多做安全的朋友一开始会忽略它,觉得“我要分析的是请求和响应,管页面结构干嘛”。但实际测试里,DOM的重要性远超你的想象。一个请求的参数名、一个加密开关、一段被隐藏的逻辑,往往就藏在某个节点属性的变化里。

最简单的例子:很多后台系统有一个隐藏字段,比如<input type="hidden" name="role" value="user">,前端表单提交时直接把这个值传给后端。这种结构在网上管理系统里非常常见。安全测试时第一件该做的事,就是打开开发者工具,把页面上所有hidden输入框都扫一遍,看看有没有不该暴露的字段。你在控制台一行代码就能做到:

document.querySelectorAll('input[type="hidden"]').forEach(el => { console.log(el.name, el.value); });

这个操作的原理是:浏览器把HTML解析成DOM后,每个标签都变成了一个可读写的对象。querySelectorAll负责从DOM树里筛出符合条件的节点,forEach遍历它们,然后把name和value属性取出来给你看。整个过程不需要刷新页面、不需要抓包,直接在控制台就能完成。

2.2 元素属性操作:改属性、看逻辑、找入口

DOM树的价值不只是“看”,更在于“改”。浏览器赋予了JS对节点属性的完整读写能力,包括getAttribute()、setAttribute()、removeAttribute(),以及更直接的element.xxx = yyy方式。这套能力在安全测试里就是一把螺丝刀。

常见操作场景有这几种:

  • 修改disabled属性,让原本不可点击的按钮生效。比如某些页面的提交按钮被置灰,理论上需要先勾选协议才能点击,但你直接删掉disabled属性,就能跳过这个前端限制。
  • 修改maxlength属性,绕过长度的前端校验。有些场景后端不做校验,只靠前端限制长度,那你在控制台执行el.maxLength = 9999,就能提交远超限制的数据。
  • 修改href或src属性,观察跳转逻辑。很多前端埋了跳转事件,你的参数不同跳转目标就不同,直接改属性可以快速判断逻辑分支。
  • 修改innerHTML,动态插入测试代码。遇到DOM型XSS测试时,你需要在某个节点里塞一段测试标记,观察是否被执行。

我再举一个实际测试中常碰到的场景。某个系统的前端禁用词检测,是一个oninput事件在过滤用户输入。当你输入敏感词,它直接替换成***。这个逻辑在前端,那就直接在控制台把这个事件解绑:

const el = document.querySelector('#input-area'); el.oninput = null;

之后你再输入任何内容,都不会被前端过滤了。如果后端也做了同样限制,那这个操作至少帮你跳过了一层障碍,方便你单独测试后端逻辑。

2.3 从DOM操作中嗅探JS与后端的交互习惯

还有一个容易被忽略的观察点:通过元素属性操作往往能看出前后端之间传递数据的方式。比如有些页面会在某个节点的>document.querySelectorAll('*').forEach(el => { [...el.attributes].forEach(attr => { if (attr.name.startsWith('data-')) { console.log(el.tagName, attr.name, attr.value); } }); });

实测下来,这个操作经常能翻到一些“本不该出现在前端”的东西,比如用户角色标识、接口路径前缀甚至内部ID。攻防双方的博弈往往就在这些细节里——前端认为隐藏就是安全,但实际上对于能打开控制台的人来说,看到的只是“透明”而已。DOM树这个环节练熟之后,你会形成一种条件反射:拿到一个页面,先扫隐藏字段、再扫自定义属性、再看事件绑定,这比你直接抓包高效得多。

3. 加密编码库:前端数据加工的标准套路

3.1 前端为什么要做加密编码?

在很多初学者眼里,加密是用来保护传输数据的。这个理解对一半,但安全测试里要换个角度:前端加密更准确说是一种“数据加工”,目的是让服务端更轻松、让测试者更难受。它不一定是为了防黑客,更多时候是为了防“顺手抓包的人”——直接看请求就能读懂参数的应用,早就不多见了。

现在常见的前端加密封装手段有几类:编码类(Base64、URL编码、Hex)、摘要类(MD5、SHA-1、SHA-256)、对称加密(AES、DES)、非对称加密(RSA)、国密算法(SM2、SM3、SM4)。注意,编码和加密本质上是两回事。编码是可逆的格式转换,比如Base64解码就是反操作;加密则需要密钥、算法配合才能还原。但在前端世界,它们经常混在一起用,因为对业务开发者来说,差别并不大——反正把明文变成一堆看不懂的字符就算“处理过了”。

3.2 常见编码加密的特征识别

做逆向分析的第一步往往是“认”出对方用的是什么算法。从我的经验看,识别特征比你想的简单,不需要什么专业工具,眼睛加控制台就能完成。

类型典型特征常见输出示例
Base64大小写字母+数字,结尾常带=号YWRtaW4=
URL编码%号加两位十六进制%61%64%6D%69%6E
十六进制纯0-9和A-F组成61646D696E
MD5固定32位十六进制字符串21232f297a57a5a743894a0e4a801fc3
SHA-256固定64位十六进制字符串240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9
AES需要密钥,输出通常为随机字符串或Base64U2FsdGVkX1+...
RSA输出长度较长,通常也是Base64分段字符串,明显比明文长得多
国密SM4输出看起来像乱码,通常用Base64或Hex包裹固定16字节分组的二进制结果

这些特征不是绝对准确,比如Base64和AES输出看起来就很像,但作为第一轮判断已经够用。实践里的流程是:先看长度和字符集,再搜索关键词,如果能找到加密函数名就直接定位;找不到就下断点跟函数调用。

3.3 如何跟到加密函数所在位置

这是整节课程里争议最多的地方,很多新手卡在这一步。最笨也最有效的方法是“搜”。登录页面按下F12,在Sources面板里调出全局搜索(快捷键Ctrl+Shift+F),输入你掌握的字段名,比如password、username、encrypt、sign、token。搜出来以后挨个点进去看,你大概率能找到一段函数,里面写着怎么把明文加工成密文。

看到一个典型的前端加密逻辑长这样:

function encryptLoginData(username, password) { const timeStamp = Date.now(); const raw = `${username}|${password}|${timeStamp}`; return { data: CryptoJS.AES.encrypt(raw, secretKey, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString(), time: timeStamp }; }

这一眼就能看懂:先拼字符串,再用AES的CBC模式加密,密钥来自一个全局变量secretKey。到这里,逆向分析的目标就清晰了——要么找到secretKey,要么在调用时把encryptLoginData的结果“截胡”下来。

这里我想分享一个我常用来定位加密函数的方法:在控制台重定义可疑函数。比如你怀疑某个函数是加密入口,你可以在控制台里做一次“函数替换”:

const originalFunc = window.encryptData; window.encryptData = function(...args) { console.log('encryptData被调用,参数是:', args); const result = originalFunc.apply(this, args); console.log('返回结果是:', result); return result; };

这种方式叫“Hook”,不用改页面代码,只用控制台在当前环境里把目标函数包一层。每次函数被调用时你都能看到入参和出参,甚至还能在result上做二次修改再返回。实际测试中,Hook比断点调试更容易上手,因为它不需要精确知道调用位置,只要函数挂在window或者某个全局对象上就能拦住。

3.4 前端加密的本质局限

一个必须明确的安全观点:前端加密防不住真正想分析的人。因为密钥、算法、逻辑全都跑在你的浏览器里,你拥有完全的执行环境,可以任意查看、修改、截断。前端加密解决的是“数据传输可读性”问题,不是“数据安全”问题。用前端加密来保护核心业务的做法,本质上就是给自己设一道栅栏,而不是一堵墙。这道栅栏的意义在于增加分析成本,把“随手抓包看明文”的人挡在外面,而认真做测试的人花个把小时总能绕过去。

我在测试里学到的一个心态很重要:对待前端加密,不要一上来就想着还原算法、写解密脚本。很多时候你根本不需要解密。你只需要让请求能发出去即可。比如登录密码加密了,你不知道密钥是什么,但你可以直接在控制台调用页面上现成的加密函数,用同样的方式生成一份密文,塞进请求里。这就是“重复调用代替破解”的思路,是快速突破口。

4. 断点调试:把执行过程“定格”下来

4.1 断点调试为什么要单独练

加密函数找到了,人能到这一步往往还不够,因为代码可能是压缩过的、混淆过的,函数嵌套很深,你想逻辑时间太长。这时候断点调试就成了核心工具。断点的本质是告诉浏览器:“执行到这里的时候,暂停给我看。”这对于安全测试的意义很大——你可以把动态过程拆成一个个静态片段,逐帧观察变量的值怎么变、函数怎么进怎么出、参数从哪里来,最后拼出完整的逻辑链。

浏览器自带的开发者工具里,其实有好几种断点,不只是平时网上教程里那种“在行号上点一下”的普通断点:

断点类型触发时机适用场景
行断点代码执行到指定行定位某个具体赋值或加密调用
DOM断点节点被修改、属性被改变时观察是谁改了页面元素
XHR断点发Ajax请求时拦截所有网络请求入口
事件监听器断点点击、输入等事件发生时找到按钮点击后的处理逻辑
异常断点抛出异常时遇到反调试报错或不合理行为时

4.2 XHR断点:监听请求才是最快路径

我刚接触断点调试时,习惯先找行号,浪费很多时间。后面发现,对安全测试来说,最高效的断点是XHR断点。因为页面最终总要发请求,请求的URL和参数会经过所有加密和处理,相当于所有线索最终都会汇聚到请求上。打断点的位置不是某一行代码,而是开发者工具里的事件监听器断点面板,勾选上XHR事件。之后只要有网络请求发生,代码就会停在发请求的地方。这时候往右侧调用栈面板一看,从上到下就是完整函数调用链,顺着这个链往上走,就找到了所有参数加工的位置。

这种方式特别适合处理“压缩过、混淆过的一行JS”场景。压缩代码让人没法一眼读懂,但调试时浏览器会把执行位置标得很清楚,你不需要人类去读全篇,你只需要在关键位置断下,然后顺着调用栈走。

4.3 断点调试实操:定位参数生成过程的完整步骤

结合课程里演示的案例,我把一个典型流程拆成五步,这个流程我后来在多个目标上反复验证过,稳定性很好:

第一步:打开开发者工具,切到“网络”面板,在页面上做一个操作,比如点一次登录按钮。观察发出的请求里有哪些参数是固定的、哪些是动态的。

第二步:确定目标参数。假设登录密码字段是encryptedPwd,值是abc123...密文。

第三步:切到“源代码”面板,按Ctrl+Shift+F全局搜索encryptedPwd这个字段名。这一步就能把生成这个字段的代码位置找出来。

第四步:在搜索结果里找到赋值语句,在那一行打上行断点。

第五步:重新触发登录操作。代码会在断点处暂停,此时观察右侧“作用域”面板,展开各个变量,确认密文的来源。

大多数时候你会看到类似这样的代码:

let encryptedPwd = rsaEncrypt(plainPwd + publicKeySuffix);

然后你就可以在控制台里直接调用rsaEncrypt函数,验证它是不是纯前端逻辑。如果是,那就意味着你完全可以在不询问服务端的情况下,自己构造合法密文。

4.4 断点调试的三个反面教训

断点调试有一些细节,初学者很容易踩坑。

第一,XHR断点默认会把所有请求都拦下来。你做一个操作可能触发十几个页面统计类请求,一个一个放行非常烦人。解决方法是只保留你关心的URL关键词。这块在DevTools里可以配置XHR断点的URL过滤规则,支持子串匹配,实测能大幅减少干扰。

第二,断点打到“匿名函数”里时,变量列表经常是空的。这种情况多半是压缩代码把变量名变成了t、n、e这样的单个字母,不是没传参,只是打包工具把上下文藏起来了。这时候不要急着放弃,在控制台手动输入t、n、e试试看,好多次关键密文就藏在其中一个字母变量里。

第三,不要忘了“格式化”按钮。Sources面板左下角有一个{}图标,点一下,压缩成一行的大函数就会展开成可读的多行代码。这是逆向分析的第一步操作。不格式化就分析,行号和括号配对都能让你崩溃。格式化之后虽然变量名还是短字母,但至少控制流能看清楚了,后续断点位置也更准确。

5. 逆向分析思路:从“看不懂”到“能改”的进阶路径

5.1 压缩、混淆和加密的前端代码怎么看

前端逆向的核心难点在于代码被处理过。处理层级大概分三种:压缩、混淆、加密。压缩只是删掉了空格和换行,变量名可能不变;混淆则会把短变量名替换、字符串挪位、控制流打乱;加密就更加粗暴,直接用WebAssembly或者自定义解码器把核心逻辑藏起来。大多数商业站点的选择,是“压缩+轻度混淆”,真正上高强度混淆的还是少数。

对于压缩和轻度混淆,我的经验是一句口诀:先格式化,再搜索关键词,最后跟调用栈。格式化你只干了一次,搜索关键词找入口,调用栈反应出生命路径。这三板斧能覆盖绝大多数场景。

5.2 Hook技术:不改代码,只改运行时

前面提过一次Hook,这里展开说。Hook这种手法的独特之处,是不需要理解全部代码逻辑,只需要关注你关心的那个函数。你像装了一个监控摄像头:无论函数的参数来自哪里、返回结果给谁,你都能记录得清清楚楚。

实际测试中最常Hook的对象有三类:

  • window.btoa和window.atob:Base64编码解码,登录页高频操作。
  • CryptoJS.AES.encrypt:AES加密入口,经常和登录、支付相关。
  • Date.now或Math.random:时间戳或随机数生成,签名参数常用。

举一个实际例子。某站点登录请求里有sign参数,看起来像一串MD5。搜索定位后发现代码调用了md5(rawString)。但你不知道rawString怎么拼的。这时候直接Hook掉全局md5方法:

const origMd5 = window.md5; window.md5 = function(s) { console.log('MD5入参:', s); return origMd5(s); };

刷新页面再登录,控制台里就会打印出原始字符串拼接规则。整个过程不打断点、不翻代码,一行都没改页面静态资源。这种操作方式非常适合做快速验证。

5.3 逆向分析的两个结果走向

逆向JS的目标一般有两种结局。一种是你把逻辑还原了,知道参数怎么生成,于是自己写脚本模拟整个过程,这是自动化工具要用的路径。另一种是你没有去完全还原,但通过在浏览器里动态调用页面函数,“借用”它的能力生成合法参数。比如预约抢票系统、投票系统,多数情况下都是靠第二种思路突破的。

这两种思路在效率上差别很大。写脚本还原逻辑,往往需要搞懂所有细节,耗时按小时算;动态调用页面函数,只需要在当前浏览器环境里执行一行代码,耗时按秒算。不过动态调用有一个限制:页面必须开着,页面闭包变量被垃圾回收后就失效了。

我个人的建议是:快速验证场景用动态调用,长期自动化脚本再考虑完整还原。考试也好、实战也罢,先动起来比先搞明白更重要。

5.4 常见难度升级情况

现在有些站点已经开始升级JS防护手段,常见的有:

  • debugger检测:代码里直接写死debugger,DevTools一打开就陷入无限断点。绕过办法是在Sources面板右键该断点,“Never pause here”,或者用Ctrl+F8开关断点。
  • 格式化检测:检测代码是否被格式化,如果格式化了,逻辑就变了。需要禁用格式化功能,直接把源码手动重新缩进后保存为本地文件,用本地替换模式加载。
  • 时间差异检测:代码执行时间太短或太长,会触发反调试分支。需要在设置里关闭JavaScript源码映射,或手动修改变量绕过。
  • WebAssembly加密:这种基本是为了防逆向,分析成本极高,实战中优先考虑找旧版接口或其他终端。

这些内容听起来玄乎,但你在日常小目标上碰到的概率很低。能用上断点调试和Hook,你就能解决掉百分之八十的前端加密问题。

6. 一条完整的串联实战:从元素属性到发送请求

6.1 完整演练目标

用一个虚构场景把所有知识点串起来:某后台管理系统有个“修改手机号”功能,页面打开后要求输入旧手机、新手机、短信验证码,点击提交会发送一个POST请求。抓包看到请求体如下:

{ "userId": "U10293", "newPhone": "dG9tMTIzNDU2", "verifyCode": "k3x7e2", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" }

你发现newPhone不是明文,sign看起来是32位哈希。现在的任务是搞清楚这两个参数怎么生成,并判断有没有可利用的点。

6.2 实操步骤与观察

第一步,看页面源码。在控制台跑一下隐藏字段扫描:

document.querySelectorAll('input, button, form').forEach(el => { console.log(el.tagName, el.type, el.name, el.value); });

发现表单里有一个隐藏字段userId,值就是U10293。同时注意到提交按钮绑定了onclick="submitChange()",说明逻辑在全局函数里。

第二步,搜索关键词。在Sources面板全局搜索newPhone和sign,发现一段代码:

function submitChange() { let phone = document.getElementById('newPhoneInput').value; let params = { userId: getUserId(), newPhone: encodeBase64(phone), verifyCode: document.getElementById('verifyCode').value, sign: md5(userId + phone + verifyCode + "salt") }; sendRequest(params); }

这就清楚了:newPhone是Base64编码,sign是MD5。密钥盐值是salt,硬编码在JS里。你不需要逆向加密算法,你只需要理解逻辑即可。

第三步,验证可控性。用一个新手机号替换Base64值,同时用控制台重新计算MD5:

const userId = 'U10293'; const phone = '13800138000'; const code = '123456'; const sign = md5(userId + phone + code + "salt"); const params = { userId, newPhone: btoa(phone), verifyCode: code, sign };

这样你就拿到了一个合法参数组。如果你还能控制userId,就可以尝试越权改其他用户的手机号。这里的风险点不是加密算法不够强,而是前端逻辑把该藏的盐值和算法全部暴露了。

6.3 链路上的安全反思

这个练习充分体现了课程开头强调的链路观念:DOM树让你看到了隐藏字段,元素属性操作让你摸清了表单结构,加密编码库帮你识别出Base64和MD5,断点调试让你定位到字段拼接逻辑,逆向分析帮你构造出合法请求。你从头到尾没有破解任何密码,只是把前端自己提供的工具链用了一遍。

这条链路对测试人员的启发是:不要总想着从后端寻找突破口。前端代码本身就是一张地图,上面标注了参数生成规则、密钥盐值、接口地址。做安全测试时,打开浏览器按F12的那一刻,你已经拿到了一半的答案。

7. 实际操作中的几条经验总结

最后分享几条我自己在学习和实战中反复验证过的经验,不是什么大道理,就是踩坑踩出来的心得。

第一,DevTools的Sources面板里那个“{}”格式化按钮,养成习惯,看到压缩代码先点它。我遇到很多人花半小时在一行密密麻麻的代码里找某个参数,其实格式化后十秒钟就看清了。

第二,遇到加密参数,先问自己一句:“我不解开它,能不能直接用?”大部分时候答案是能。前端既然能加密,就一定有生成密文的函数在浏览器里,直接调用它比自己逆向解密快得多,也稳得多。

第三,DOM断点不是摆设。遇到“页面元素突然被替换掉”“某个输入框被自动填满”这类诡异情况,在对应节点上打一个DOM断点,触发变化时它会把修改属性的代码精确定位出来。这个技巧在分析动态渲染页面时特别好用,建议没事就在几个关键节点上试试。

第四,Hook的时机很关键。要在页面加载早期就注入,否则你等页面已经跑完逻辑再重新覆盖窗口方法,很多调用已经过去了。动手前先刷新页面、提前把Hook代码准备好放进Sources里的Snippets(代码片段)功能里,一键运行,不污染Console。

第五,做逆向分析一定要记录。我在分析过程中习惯用一个文本文件记录函数名、参数、调用栈和结果。因为分析过程会很长,变量名又多,纯靠大脑记,过半个小时必然乱。整理成笔记,后面写自动化脚本时可以直接照着变量名来。

这门课学到最后一节时,课程组织者给了一句话,让我印象非常深:“前端安全不是让你去破解代码,而是让你学会阅读代码的执行轨迹。”用这个视角去看JS应用,你会发现之前觉得难啃的加密、混淆、断点调试,都变得顺理成章。工具永远是那些工具,真正拉开差距的是你看待问题的方式。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进