1. 项目概述与核心价值最近在整理JS逆向的实战案例发现很多朋友对办公自动化OA系统的安全研究特别是前端加密逻辑的逆向分析有着浓厚的兴趣。泛微OA作为国内广泛使用的协同办公平台其登录、表单提交等环节的前端加密机制是学习JS逆向一个非常经典的“靶场”。这个案例之所以值得深入不仅仅是因为它涉及了常见的加密算法和混淆手段更因为它能让你理解一个成熟商业产品是如何在前端保护关键数据的这种思路对提升我们自身代码的安全意识也大有裨益。简单来说我们今天要拆解的就是当你在泛微OA的登录页面输入密码点击提交时那个明文密码在变成网络请求中那一长串“乱码”之前究竟经历了什么。这个过程涉及到JavaScript代码的定位、算法识别、参数追踪以及本地模拟是JS逆向工程师的必修课。无论你是想深入前端安全领域还是在进行合规的渗透测试或系统对接时需要绕过前端加密掌握这套分析方法都至关重要。我会以一个典型的泛微OA登录场景为例带你一步步走完从页面分析到本地复现的完整流程过程中会穿插大量我踩过的坑和总结出的技巧。2. 逆向环境准备与目标锁定2.1 分析工具的选择与配置工欲善其事必先利其器。对于JS逆向浏览器开发者工具是我们的主战场。我强烈推荐使用基于Chromium内核的浏览器如Chrome或新版Edge。打开开发者工具F12有几个面板需要特别关注“Sources”源代码用于查看和调试JS文件“Network”网络用于捕获和分析HTTP请求“Console”控制台用于执行代码和查看日志。为了应对代码混淆我们还需要一些“外挂”。Tampermonkey或Violentmonkey这类油猴脚本管理器是必备的它可以加载我们自定义的调试脚本。此外一个能格式化混淆代码的浏览器插件如“Pretty print”能极大提升代码可读性。在开始分析前记得在开发者工具的“Settings”中勾选“Disable JavaScript”的选项暂时不要启用因为我们第一步需要让页面正常执行。同时打开“Network”面板并勾选“Preserve log”保留日志防止页面跳转时请求记录被清空。这些准备工作看似简单却能为后续的追踪节省大量时间。2.2 明确分析目标与入口点我们的核心目标是找到密码的加密函数。通常密码加密发生在表单提交的瞬间。因此最直接的入口点就是登录按钮的点击事件。我们可以通过多种方式定位第一种直接在“Elements”面板找到登录按钮的HTML元素查看其onclick属性或相关的事件监听器。第二种在“Sources”面板全局搜索与密码字段name或id如password、pwd相关的字符串。第三种也是我最常用的在“Network”面板先进行一次登录尝试使用测试账号观察提交的请求中密码参数的名字是什么比如可能是encodedPwd、ciphertext等然后以这个参数名作为关键词在所有已加载的JS文件中进行搜索。以泛微OA为例进行一次失败的登录尝试后你很可能在Network中看到一个指向login或doLogin的POST请求。查看其“Payload”或“Request Payload”会发现密码不再是明文而是一个长长的、看似随机的字符串。记下这个参数名比如password对应的值变成了RSA_1024#AABBCCDDEEFF...这样的格式。这个RSA_1024前缀就是我们的关键线索它直接指明了加密算法和密钥长度。3. 加密逻辑定位与算法解析3.1 关键代码的搜索与定位拿到加密后的参数值如RSA_1024#...后我们在“Sources”面板的搜索框快捷键CtrlShiftF中进行全局文件搜索。搜索关键词可以尝试多个“RSA_1024”、“encodePassword”、“encrypt”、“doLogin”。通常加密函数不会离调用它的地方太远。搜索后你可能会在几个高度混淆的JS文件中找到相关代码。这些文件名字可能像是login.min.js、framework.js或一串无意义的哈希值。点击进入这些文件代码通常是压缩成一行的。点击左下角的“{}”Pretty print按钮进行格式化。格式化后代码结构会清晰很多但变量和函数名可能仍然是单字母的如a, b, c, d这是混淆的常见手段。此时我们需要结合上下文和网络请求的调用栈来定位。在Network面板中找到那个登录请求右键点击它选择“Replay XHR”有时不奏效更好的方法是右键选择“Copy - Copy as cURL”然后在控制台Console中执行copy()命令后的内容不更有效的是在该请求的“Initiator”标签页下可以看到调用这个请求的JavaScript调用栈。点击调用栈中最顶层的那个通常是某个事件处理器或XMLHttpRequest.send可以直接跳转到发起请求的代码行附近这里往往就是加密函数被调用的地方。3.2 加密算法识别与逆向推导通过调用栈我们通常能找到类似data.password encrypt(password)的代码。跟进这个encrypt函数。在泛微OA的案例中加密算法很可能是RSA。前端RSA加密通常使用JavaScript的加密库如jsencrypt或node-rsa也可能是厂商自己实现的。如何判断呢首先看函数内部是否有明显的new JSEncrypt()、setPublicKey或encrypt方法调用。其次搜索“BEGIN PUBLIC KEY”或“BEGIN RSA PUBLIC KEY”这样的字符串这是RSA公钥的PEM格式标识。找到公钥后加密逻辑就相对清晰了前端从服务器获取或硬编码了一个RSA公钥然后用这个公钥对明文密码进行加密。加密后的结果通常是Base64编码的字符串。我们看到的RSA_1024#前缀后面拼接的就是这个Base64密文。有些实现可能会在加密前对密码进行额外的处理比如加盐Salt或者进行一次哈希如MD5、SHA1然后再用RSA加密哈希值。这就需要我们仔细阅读加密函数内部的每一步。注意这里存在一个关键点。前端RSA加密使用的通常是PKCS#1 v1.5填充方案。在本地模拟时如果你使用Python的rsa或Crypto库需要确保使用相同的填充模式。否则即使公钥和明文相同加密结果也会不一样导致模拟失败。3.3 核心加密函数提取与抽象定位到核心加密函数后我们的目标不是理解每一行混淆的代码而是提取出可独立运行的加密逻辑。这通常需要将函数及其依赖的所有变量和子函数都复制出来。一个技巧是在开发者工具的“Sources”面板找到这个函数在函数体的开头打上断点点击行号。然后回到页面再次触发登录动作。代码会在断点处暂停。此时在“Console”面板你可以尝试逐行执行并观察每一步变量的变化。更重要的是你可以尝试将整个函数体包括它内部依赖的、在外部定义的工具函数选中右键“Copy function definition”或手动复制然后粘贴到一个文本编辑器中。接下来就是“瘦身”工作删除与加密核心逻辑无关的代码比如UI更新、日志记录只保留从输入明文到输出密文所必需的步骤。对于混淆的变量名如果它们不影响逻辑可以保留如果为了可读性可以将其重命名为有意义的名称但务必确保所有引用关系同步修改。4. 本地模拟复现与代码重构4.1 依赖分析与环境搭建提取出的加密函数很可能依赖于某些浏览器环境特有的对象或函数比如window、document、navigator或者一些第三方库如jsencrypt。我们的任务是在Node.js或Python环境中模拟这些依赖。识别依赖仔细检查提取的代码。如果看到了new JSEncrypt()那么你需要在前端项目中引入jsencrypt库或者在Node.js中安装node-jsencrypt包。如果代码使用了CryptoJS常见于MD5、AES等则需要安装crypto-js。模拟浏览器对象对于简单的window或navigator引用如果它们只是被用来获取一些不影响加密结果的属性有时混淆代码会用它来生成随机数种子我们可以在本地用一个空对象模拟或者直接将其替换为一个固定值。例如// 在Node.js中模拟一个简单的window对象 global.window {}; global.navigator { userAgent: 模拟 };处理异步操作有些加密逻辑可能会异步获取密钥。我们需要将其改造成同步方式或者在自己的模拟代码中使用async/await。4.2 加密逻辑的Python/Node.js重写为了更灵活地集成到其他系统或测试脚本中我们通常会用Python或Node.js重写加密逻辑。这里以Python为例假设我们确定是RSA加密。首先你需要拿到公钥。公钥可能硬编码在JS文件里也可能通过一个单独的接口动态获取。如果是硬编码的直接复制出来。注意PEM格式的公钥通常包含-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----。确保复制完整。然后使用Python的rsa或pycryptodome库进行加密。这里使用pycryptodome因为它更通用。from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import base64 def encrypt_password(password, public_key_pem): 模拟前端RSA加密 :param password: 明文密码 :param public_key_pem: PEM格式的公钥字符串 :return: 加密后的字符串通常为Base64 # 加载公钥 key RSA.import_key(public_key_pem) # 创建加密器使用PKCS#1 v1.5填充这是前端jsencrypt的默认方式 cipher PKCS1_v1_5.new(key) # 加密输入需要是bytes类型 encrypted_bytes cipher.encrypt(password.encode(utf-8)) # 转换为Base64字符串 encrypted_b64 base64.b64encode(encrypted_bytes).decode(utf-8) return encrypted_b64 # 示例使用 public_key -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1...你的公钥内容 -----END PUBLIC KEY----- plain_pwd your_password cipher_text encrypt_password(plain_pwd, public_key) print(f加密结果: {cipher_text}) # 可能需要拼接上前缀如 fRSA_1024#{cipher_text}关键点确保Python库使用的填充方案与前端一致。jsencrypt默认使用PKCS1_v1_5填充。pycryptodome的PKCS1_v1_5模块与之对应。如果前端做了额外的哈希处理你需要在加密前用Python的hashlib库先对密码进行相同的哈希运算。4.3 验证与调试重写完成后必须进行验证。方法是用同一组明文密码和公钥分别在前端页面和你的本地脚本执行加密对比输出结果是否完全一致。前端获取密文在登录页面输入一个测试密码如test123通过开发者工具的“Network”面板捕获提交的请求记下加密后的密码字符串。本地脚本加密在你的Python脚本中使用相同的测试密码和提取出的公钥运行加密函数。对比比较两个结果。如果完全一致恭喜你成功了。如果不一致按以下步骤排查检查公钥确认复制的公钥完整无误没有多余的空格或换行。检查编码确保密码字符串的编码一致通常都是UTF-8。检查填充和算法参数确认RSA的密钥长度如1024、填充模式PKCS1_v1.5是否完全匹配。检查预处理前端是否对密码进行了MD5、SHA1或加盐等预处理你需要仔细回溯加密函数看明文密码在进入RSA加密函数之前是否经过了其他函数处理。使用Node.js模拟有时直接将提取的JavaScript加密函数在Node.js环境中运行是验证逻辑最直接的方式。你可以创建一个.js文件将提取的函数和依赖粘贴进去然后导出加密函数进行调用。5. 深度技巧与疑难问题排查5.1 对抗混淆与反调试策略现代Web应用会采用更高级的混淆和反调试技术。例如代码控制流扁平化将顺序执行的代码打乱用switch语句或数组跳转来执行极大地增加阅读难度。对付这种需要耐心或者借助一些反混淆工具如jsnice、ast-explorer在线解析尝试还原部分可读性但完全自动化还原很难。无限Debugger在代码中插入debugger;语句或通过setInterval不断检查开发者工具状态导致调试时不断暂停。应对方法在Sources面板找到对应的行右键选择“Never pause here”或者通过条件断点将其禁用。更彻底的是在开发者工具设置中勾选“Disable JavaScript”再刷新页面然后取消勾选并快速进行搜索定位避免反调试代码执行。环境检测代码会检测是否运行在浏览器调试环境中通过判断window.console、window.devtools等对象的某些特性。我们可以在代码执行前在Console中重写这些检测函数使其返回false。例如// 在Console中执行绕过某些环境检测 Object.defineProperty(window, isDebug, { get: function() { return false; } });5.2 动态密钥与参数追踪有些系统不会硬编码公钥而是在登录前通过一个接口动态获取。这就需要我们多追踪一个网络请求。在登录页面加载时注意观察Network中是否有getPublicKey、init或key之类的请求。这个请求返回的数据中可能就包含公钥可能是JSON格式的modulus和exponent也可能是完整的PEM字符串。在本地模拟时你的脚本就需要先模拟这个请求获取到最新的公钥然后再用这个公钥加密密码。这就构成了一个两步流程。此外加密可能还依赖其他动态参数比如时间戳、随机数Nonce这些参数也需要从页面或之前的请求响应中提取并一同参与加密计算。5.3 常见错误与解决方案速查表在逆向和模拟过程中你肯定会遇到各种报错。下面是一些常见问题及解决思路问题现象可能原因排查步骤与解决方案本地加密结果与前端不一致1. 公钥不正确或格式错误。2. 填充模式不匹配。3. 密码预处理步骤遗漏哈希、加盐。4. 字符编码问题。1. 核对公钥字符串确保是完整的PEM格式首尾标记正确。2. 确认前端使用的加密库如jsencrypt默认填充为PKCS1_v1.5在Python中使用PKCS1_v1_5。3. 仔细调试前端加密函数用console.log在每一步打印输出对比本地模拟的中间结果。4. 确保密码在加密前都转换为UTF-8字节。Node.js运行提取的JS代码报错xxx is not defined浏览器环境特有的对象如window、document或全局变量未定义。1. 在Node.js脚本顶部模拟这些对象global.window {};。2. 如果代码依赖了第三方库如JSEncrypt需要通过npm安装并在脚本中require。无法在混淆代码中定位加密函数关键词搜索无结果或调用栈不清晰。1. 尝试搜索加密后字符串的特征如RSA_、BASE64等。2. 在密码输入框的oninput或onchange事件上打XHR断点。3. 在Network面板对登录请求右键选择“Break on - XHR/fetch Breakpoints - URL contains: login”。加密函数被多次嵌套调用逻辑复杂代码经过高度混淆和封装。1. 不要试图完全理解专注于输入输出。在加密函数入口和出口打日志确认传入的明文和传出的密文。2. 尝试将整个函数及其内部依赖的所有函数块一起复制出来形成一个独立的、封闭的JS文件。请求发送后服务器返回“加密错误”或“非法请求”1. 加密结果正确但请求格式或头部信息不对。2. 缺少必要的令牌如CSRF Token。3. 请求顺序有要求如先获取密钥再登录。1. 用工具如Postman精确对比你的模拟请求和浏览器发出的请求检查Headers、Cookies、RequestBody的格式是否完全一致。2. 检查登录页面HTML中是否隐藏了csrfToken之类的字段需要随请求一同提交。3. 完整模拟用户操作流程访问首页 - 获取动态密钥/令牌 - 携带所有必要参数发起登录。6. 安全思考与合规建议完成这样一个逆向案例后我们除了获得技术上的成就感更应该有一些安全层面的思考。前端加密无论是RSA还是AES其主要目的不是防止密码在传输过程中被窃听因为HTTPS已经很好地解决了这个问题而是为了增加攻击者进行自动化攻击如撞库的难度以及避免密码在浏览器内存中以明文形式存在防范某些内存扫描恶意软件。它是一种“防君子不防小人”的增强措施因为加密逻辑和公钥对客户端都是公开的。因此作为开发者我们绝不能依赖前端加密来保证密码的绝对安全。后端必须进行严格的校验、哈希加盐存储如使用bcrypt、Argon2、速率限制、二次验证等。作为安全研究人员或测试人员进行此类逆向分析必须在合法授权的范围内进行仅限于对自己拥有管理权限的系统或明确允许测试的目标用于提升系统安全性或完成正当的系统集成工作。任何未经授权的测试都可能触犯法律。最后这个泛微OA的案例只是一个引子。市面上不同的OA系统、不同的版本其加密方式可能千差万别可能用到AES、DES甚至是自定义的加密算法。但分析方法论是相通的抓包定位、搜索关键参数、追踪调用栈、分析加密函数、提取并模拟。掌握这套流程再配合耐心和细致的调试大部分前端加密逻辑都能被攻克。在实际操作中最花时间的往往不是算法本身而是在浩瀚且混淆的JavaScript代码中找到那几行关键的逻辑。多练习多总结你会形成自己的“代码嗅觉”。