JS逆向实战:破解泛微OA前端密码加密,掌握自动化登录核心技能
1. 项目概述从一次登录请求说起最近在做一个企业办公系统的自动化对接项目客户用的是泛微OA。大家都知道这类系统为了安全登录时的密码肯定不会明文传输。我像往常一样打开浏览器的开发者工具准备抓个包看看接口结果发现提交的password字段是一串长得离谱的、毫无规律的字符明显是经过前端加密处理了。这活儿一下就变得有意思了——不搞清楚这个加密逻辑后续的自动化登录、单点登录集成啥的都无从谈起。这其实就是典型的JS逆向场景目标明确就是逆向分析前端JavaScript代码找到密码的加密算法和密钥最终能在我们自己的脚本或程序里复现这个加密过程。“JS逆向三百例”这个系列就是想通过一个个真实的、有代表性的案例把JS逆向的思路、工具和技巧串起来讲透。今天这个泛微OA前端密码加密就是一个非常经典的案例。它涉及到了常见的混淆、加密库的识别、关键逻辑的定位以及如何将零散的JS代码片段整理成我们可用的Python或Node.js代码。整个过程就像侦探破案从网络请求这个“结果”出发一步步回溯到产生这个结果的“源代码”。无论你是做爬虫遇到加密参数还是做安全测试、自动化运维需要模拟登录这套分析方法都是通用的核心技能。2. 核心思路与工具准备逆向分析不是漫无目的地看代码得有清晰的思路和顺手的工具。我的核心思路永远是“由外而内顺藤摸瓜”。2.1 逆向分析的核心路径定位入口抓包这是所有工作的起点。在登录页面输入账号密码比如账号test密码123456点击登录在开发者工具的Network面板中找到提交登录请求的那个XHR或Fetch请求。重点关注Form Data或Payload里那个被加密的密码字段记下它的样子。同时留意请求的initiator发起者栈它可能会告诉你哪个JS文件发起了这个请求。搜索关键点全局搜索在Sources面板对当前页面加载的所有JS文件进行全局搜索。搜索关键词可以是加密后的密码字段中的某一段如果太长取前10个字符更常见的是搜索像password、encrypt、encode、CryptoJS、RSA、AES等与加密相关的函数名或变量名。下断点调试动态调试搜索到疑似加密函数后在相应的行号上打上断点。刷新页面重新执行登录操作代码执行会在断点处暂停。这时你可以查看调用栈Call Stack了解函数是如何被调用的可以查看局部变量Scope和监视表达式Watch观察参数和返回值的变化。这是理解加密逻辑最直接的方法。逻辑分析与代码还原通过调试理清加密的步骤是简单的Base64是MD5或SHA系列哈希还是对称加密如AES或非对称加密如RSA密钥从哪里来有没有额外的偏移IV或填充Padding最后用你熟悉的编程语言Python/Node.js将这个过程重新实现出来。2.2 必备工具清单工欲善其事必先利其器。以下是进行JS逆向的“瑞士军刀”浏览器开发者工具Chrome DevTools / Firefox Developer Edition这是主战场。尤其是Sources面板用于查看、搜索、调试JS、Network面板用于抓包、Console面板用于执行临时代码测试。代码美化工具Pretty Print线上JS代码通常被压缩成一行难以阅读。DevTools中有一个{}Pretty Print按钮可以一键将混淆压缩的代码格式化这是逆向的第一步。Overrides功能本地代码替换在Sources面板的Overrides标签页可以指定一个本地文件夹然后将线上JS文件映射并保存到本地。你可以在本地修改代码例如添加日志、修改逻辑刷新页面后浏览器会加载你本地的版本极大方便调试和测试。Fiddler / Charles抓包与改包虽然浏览器自带抓包但这类专业工具功能更强大可以拦截和修改任意请求/响应设置断点进行压力测试等在复杂场景中非常有用。Node.js环境用于在本地执行和测试还原出来的JavaScript加密函数。很多时候加密逻辑依赖浏览器环境的一些特有对象如window、document需要在Node.js中模拟这些环境或者将代码改写成不依赖浏览器的纯逻辑。Python环境及相关库最终我们往往需要在Python中实现加密。常用的库有execjs 用于执行JavaScript代码。当你把关键的JS加密函数抠出来后可以直接用这个库在Python中调用。PyExecJS 类似execjs的另一个选择。Crypto/cryptography Python原生的加密库功能强大。如果逆向发现用的是标准算法如AES-128-CBC完全可以用这些库原生实现比调用JS引擎效率更高。requests 用于模拟最终的HTTP请求。注意逆向分析可能涉及法律和道德边界。务必确保你的行为在授权范围内进行例如对自己负责的系统、公开的测试环境或已获得明确授权的目标进行分析。切勿将其用于非法入侵、数据窃取等用途。3. 泛微OA密码加密实战拆解理论说再多不如动手干一次。我们以一次模拟的泛微OA登录为例来走通整个流程。请注意不同版本、不同配置的泛微OA加密方式可能不同这里的步骤是通用方法论具体细节需要你针对自己的目标进行分析。3.1 抓包与初步观察打开泛微OA登录页输入信息点击登录。在Network中我们找到了登录请求/login/Login.jsp。查看请求负载Payload我们看到了类似这样的结构{ logintype: 1, loginid: zhangsan, userpassword: U2FsdGVkX1oH5Q...非常长的一串字符, submit: 登录 }显然userpassword字段就是我们的目标。它是一串Base64编码样式的字符串通常包含/、、等字符长度固定且较长这强烈暗示它可能使用了AES或DES等对称加密并且很可能采用了CBC模式因为ECB模式的结果通常不会这么长且规整。另一个重要线索是在同一个请求或之前的页面请求中我们可能会发现一个名为cryptkey的字段或者隐藏在某个JS变量、HTML标签属性中的密钥信息。这是后续搜索的关键。3.2 全局搜索与定位加密函数在DevTools的Sources面板对所有JS资源进行全局搜索快捷键CtrlShiftF。第一轮搜索直接搜索加密后的密码U2FsdGVkX1的前几位。如果运气好可能直接找到赋值或调用加密函数的地方。第二轮搜索搜索userpassword。可能会找到表单序列化或提交前处理密码的代码。第三轮搜索最常用搜索encrypt、encode、CryptoJS、AES。在泛微OA的案例中有很大概率会搜索到引用了CryptoJS这个著名前端加密库的代码。假设我们搜索CryptoJS.AES在一个名为login_encrypt.js名称可能不同的文件中找到了关键代码。使用{}美化后我们看到类似下面的结构function encryptPassword(password, key) { var iv CryptoJS.enc.Utf8.parse(xxxxxxxx); // 偏移向量IV var srcs CryptoJS.enc.Utf8.parse(password); var encrypted CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 返回Base64格式的密文 }同时我们在页面的其他地方可能是另一个JS文件或内嵌脚本找到了密钥key的生成或获取逻辑。例如// 从某个隐藏的input框获取密钥 var cryptKey document.getElementById(cryptKey).value; // 或者是一个固定的值或者是由服务器下发的动态值 // var cryptKey a1b2c3d4e5f6g7h8;3.3 动态调试验证逻辑在encryptPassword函数的第一行打上断点。清空Network记录重新点击登录。代码执行会在断点处暂停。查看调用栈在Call Stack面板可以看到是哪个函数调用了encryptPassword这有助于我们理解整个加密的触发时机和上下文。监视变量在Watch面板添加表达式查看传入的password参数此时应该是明文123456key参数的值以及iv的值。单步执行按F10Step Over或F11Step Into一步步执行观察encrypted变量的生成过程。最终将encrypted.toString()的结果与Network中抓包看到的userpassword值进行对比如果一致恭喜你成功定位了加密核心3.4 代码还原与Python实现现在我们已经掌握了所有要素算法AES、模式CBC、填充PKCS7、密钥key、偏移向量iv、输入明文密码、输出格式Base64。接下来就是还原。方案一使用execjs调用抠出的JS函数这是最直接的方法尤其当加密代码复杂或涉及大量自定义逻辑时。import execjs # 1. 抠出完整的JS加密函数保存到一个字符串中 js_code function encryptPassword(password, key) { var CryptoJS require(crypto-js); // 需要在Node环境中安装crypto-js var iv CryptoJS.enc.Utf8.parse(xxxxxxxx); var srcs CryptoJS.enc.Utf8.parse(password); var encrypted CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } # 2. 创建JS上下文并执行 ctx execjs.compile(js_code) encrypted_pwd ctx.call(encryptPassword, 123456, your_secret_key_here) print(encrypted_pwd) # 应与抓包结果一致实操心得使用execjs时确保本地Node.js环境已安装代码中require的模块如crypto-js。如果JS代码依赖浏览器对象如window、document你可能需要手动模拟这些对象或者使用像jsdom这样的库来构造一个简单的浏览器环境这会让过程变得复杂。因此优先考虑用Python原生实现。方案二使用Python原生加密库实现推荐既然算法是标准的AES-CBC-PKCS7我们完全可以用Python的pycryptodome库来实现性能更好依赖更清晰。from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 def encrypt_password_aeS_cbc(password: str, key: str, iv: str) - str: 模拟前端CryptoJS的AES-CBC-PKCS7加密 :param password: 明文密码 :param key: 密钥UTF-8字符串 :param iv: 偏移量UTF-8字符串 :return: Base64编码的密文 # 将字符串转换为bytes key_bytes key.encode(utf-8) iv_bytes iv.encode(utf-8) plaintext_bytes password.encode(utf-8) # 确保密钥和IV长度符合AES要求16, 24, 32字节 # 前端CryptoJS会自动处理这里我们需要手动确保或截断/填充 # 通常密钥和IV是前端给定的固定值长度是符合的。 # 例如AES-128要求16字节key和16字节iv。 # 创建AES加密器模式CBC cipher AES.new(key_bytes, AES.MODE_CBC, iv_bytes) # 对明文进行PKCS7填充并加密 padded_data pad(plaintext_bytes, AES.block_size) ciphertext_bytes cipher.encrypt(padded_data) # 将加密后的bytes进行Base64编码 ciphertext_b64 base64.b64encode(ciphertext_bytes).decode(utf-8) return ciphertext_b64 # 使用示例 key a1b2c3d4e5f6g7h8 # 16字节AES-128 iv xxxxxxxxxxxxxxxx # 16字节 password 123456 encrypted encrypt_password_aeS_cbc(password, key, iv) print(f加密结果{encrypted})运行这段Python代码得到的加密结果应该与浏览器中抓包得到的结果完全一致。如果不一致请检查以下几点1) 密钥key和iv的值是否正确2) 密钥和IV的长度是否符合AES要求3) 前端是否在加密前对密码做了其他处理如加盐、多次哈希等。4. 深度解析可能遇到的混淆与对抗手段真实的泛微OA或其他系统前端代码不会这么“友好”。开发者会使用各种手段增加逆向难度。4.1 代码混淆Obfuscation这是最常见的手段。变量名、函数名被替换成无意义的短字符如_0x1a2b3c字符串被编码控制流被扁平化或插入垃圾代码。应对策略美化Pretty Print这是第一步让代码结构可视。重命名手动推理虽然工具可以反混淆但很多时候需要手动分析。通过调试观察哪个变量存储了密钥哪个函数被调用来加密然后在你本地的Overrides文件中给这些关键变量和函数起一个有意义的名字方便后续阅读。使用反混淆工具对于简单的混淆可以尝试在线或离线的JS反混淆工具但它们对复杂的、自定义的混淆方案效果有限。不要过度依赖工具理解逻辑才是根本。4.2 加密逻辑分散与动态加载加密函数可能被拆分成多个部分分散在不同的JS文件甚至通过eval、Function构造函数动态生成或者通过Ajax动态加载一段加密代码。应对策略关注网络请求在Network面板中过滤JS请求看看有没有在登录前动态加载的、名称可疑的JS文件。搜索eval和Function在这些函数调用处下断点查看其执行的字符串内容很可能就是被隐藏的加密逻辑。整体分析即使逻辑分散最终必然有一个入口函数来整合所有部分。通过调试登录按钮的点击事件监听器一步步跟进总能找到汇聚点。4.3 环境检测与反调试有些代码会检测是否打开了开发者工具如果检测到可能会改变执行逻辑、进入死循环甚至直接报错。应对策略禁用断点在遇到反调试时可以先禁用所有断点让页面正常加载执行。重写检测函数在Overrides中找到检测开发者工具的函数常通过检查console.log、debugger关键字或窗口尺寸实现将其重写为一个空函数或直接返回false。使用无头浏览器在自动化脚本中可以使用Puppeteer或Selenium等无头浏览器它们默认的开发者工具状态与普通浏览器不同有时可以绕过检测。4.4 密钥非明文存储密钥可能不是硬编码在JS里而是通过一个复杂的算法实时计算出来或者从服务器接口获取。应对策略搜索与追踪全局搜索cryptkey、key、secret等关键词即使它是经过计算得到的。调试计算过程如果密钥是计算出来的在计算它的函数上下断点一步步跟踪把计算逻辑也还原出来。这可能涉及时间戳、用户ID、随机数等多种因素的组合。模拟请求如果密钥是从类似/api/getCryptKey这样的接口获取的那么在你的自动化脚本中就需要先模拟这个请求拿到密钥再用它去加密密码。5. 常见问题排查与实战技巧在实际操作中你肯定会遇到各种报错和意外情况。这里记录了一些典型问题的排查思路。5.1 加密结果不一致这是最常见的问题。你的Python代码运行了但结果和浏览器抓包的对不上。检查清单算法/模式/填充确认三要素完全一致。是AES还是DES是CBC还是ECB是PKCS7还是ZeroPadding一个字母都不能错。密钥和IV确认值和编码完全一致。浏览器里看到的key和iv是字符串在加密时被CryptoJS.enc.Utf8.parse转换成了WordArray。在Python中我们直接用.encode(utf-8)得到bytes。确保两者在转换前字符串内容一致。特别注意是否有不可见字符。明文处理确认加密前的明文是否完全一致。前端有没有对密码进行trim()去除首尾空格有没有先进行了一次MD5哈希通过调试在加密函数入口处打印或查看明文值与你的Python输入进行比对。输出格式CryptoJS.AES.encrypt返回的是一个CipherParams对象toString()默认输出Base64格式的密文。但也有可能是ciphertext属性WordArray的Hex字符串。确认你对比的是同一种格式。5.2execjs执行JS报错ReferenceError: window/document is not defined说明抠出的JS代码依赖浏览器环境。你需要提供一个简单的模拟环境。import execjs # 在JS代码前注入模拟环境 js_code // 模拟window和document对象 var window this; var document { getElementById: function(id) { // 返回你需要的值例如密钥 if (id cryptKey) { return { value: your_key_here }; } return null; } }; your_original_js_code ctx execjs.compile(js_code)Error: Cannot find module crypto-js你的Node.js环境没有安装crypto-js模块。在命令行执行npm install crypto-js进行安装。或者更简单的方法直接使用CDN上的源码内联到你的JS代码字符串中避免require。# 将 crypto-js.min.js 的文件内容读取出来作为字符串拼接到 js_code 开头 with open(crypto-js.min.js, r, encodingutf-8) as f: crypto_js_lib f.read() js_code crypto_js_lib \n your_encrypt_function_code5.3 如何应对加密算法升级如果某一天你发现之前的脚本突然登录失败了很可能是后端更新了加密算法。快速验证手动在浏览器登录一次抓包看加密后的密码格式是否变化长度、字符集。如果从Base64样变成了Hex样或者长度变了算法很可能变了。重新分析按照本文的步骤重新进行抓包、搜索、调试。重点关注是否有新的JS文件被加载或者原有加密函数是否被更新。版本管理在你的自动化脚本中可以将加密逻辑抽象成一个独立的函数或类。当算法变更时你只需要更新这个模块并在调用时通过某种方式如配置文件、接口探测决定使用哪个版本的加密器。5.4 效率与维护建议缓存密钥如果密钥在一定时间内如一个会话是固定的不要每次登录都去计算或请求缓存起来。错误重试与降级网络请求可能失败加密函数可能因环境问题报错。在你的脚本中加入适当的异常捕获和重试机制。代码注释在还原的Python代码中详细注释算法、模式、填充、密钥来源、IV来源。几个月后你自己回头看或者交接给同事时会感谢现在的自己。单一职责将加密函数、网络请求函数、数据处理函数分离使代码结构清晰易于测试和维护。逆向分析是一个需要耐心和细心的过程。每一个加密参数都像一把锁的钥匙缺一不可错一不可。从泛微OA这个案例延伸出去你会发现很多Web系统的前端加密都大同小异核心思路就是定位、调试、还原。掌握了这套方法再遇到类似的sign参数逆向、token生成逻辑分析你都能从容应对。记住浏览器的开发者工具是你最强大的盟友而清晰的逻辑思维是破解一切混淆的终极武器。

相关新闻