致远OA前端密码加密JS逆向分析与Python复现实战

📅 2026/6/23 5:05:35
致远OA前端密码加密JS逆向分析与Python复现实战
1. 项目概述从一次登录请求开始的逆向之旅最近在分析一个企业级应用时遇到了一个典型的场景需要模拟登录流程但提交的密码是经过前端加密处理的。目标系统是致远OA一个广泛使用的协同办公平台。当你打开登录页面输入密码点击登录浏览器会向服务器发送一个POST请求。如果你用开发者工具查看这个请求的载荷会发现密码字段并不是你输入的明文而是一长串看似无规律的字符。这就是前端JavaScript在提交前对密码进行了加密处理。我们的目标就是逆向分析出这个加密算法从而能够用脚本模拟登录。这不仅是爬虫或自动化测试中的常见需求更是理解现代Web应用安全机制的一个绝佳切入点。今天我就以“致远OA”的前端密码加密为例带你走一遍完整的JS逆向分析流程从定位加密代码到还原算法再到Python复现分享其中踩过的坑和总结的技巧。2. 逆向环境准备与初步抓包分析2.1 工具链选择与配置工欲善其事必先利其器。对于JS逆向一套顺手的工具能极大提升效率。我的核心工具组合是Chrome DevTools Node.js环境。首先确保你的Chrome浏览器是最新版本开发者工具功能最全。打开目标登录页面例如http://oa.example.com/seeyon/直接按F12。这里有个关键设置在开发者工具的设置右上角齿轮图标中找到“Preferences”下的“Sources”确保“Enable JavaScript source maps”是勾选的。这能让我们在调试混淆代码时有机会看到更友好的映射信息虽然对于强混淆的代码可能帮助有限但养成好习惯很重要。其次我强烈建议在本地安装Node.js。不是为了运行目标网站的JS而是为了搭建一个干净、可控的测试环境。我们可以把关键的、疑似加密函数的JS代码片段抠出来在Node里运行和调试这比在浏览器控制台里反复刷新页面测试要高效得多。你可以使用npm init -y初始化一个项目然后安装一个辅助模块crypto-js有时目标站点可能使用了这个库我们可以用它来做算法验证对比。2.2 网络请求抓包与关键定位一切就绪后在登录页面随意输入账号如test和密码如123456先不要点击登录。打开开发者工具的Network网络面板勾选上“Preserve log”保留日志防止页面跳转后请求记录被清空。然后点击登录按钮。瞬间你会看到Network面板里刷出一系列请求。我们的目标是找到那个提交登录信息的请求。通常它可能叫login.do、login.jsp、ajaxLogin或者就是一个单纯的/seeyon/main.do。请求方法一般是POST。点击这个请求查看它的Headers和Payload。在Headers里注意Content-Type通常是application/x-www-form-urlencoded或application/json。这决定了数据格式。重点看Payload如果是表单数据则查看Form Data标签页。你会看到类似这样的结构login_username: test login_password: 7C4A8D09CA3762AF61E59520943DC26494F8941B或者是一个JSON{ username: test, password: aBcDeFgHiJkL...很长一串 }这里的login_password或password对应的值就是加密后的密文。记下你输入的明文密码如123456和这个密文这是后续验证算法是否正确的最直接证据。另一个容易被忽略但极其重要的细节是Initiator发起者列。它显示了是哪个JS文件发起了这个网络请求。点击它旁边的链接可以直接跳转到Sources源代码面板对应的JS代码行。这往往是定位加密函数的“快速通道”。如果这里显示的是一个很通用的JS文件如jquery.min.js那说明加密逻辑可能在别处但至少它告诉了我们请求触发的准确位置。3. 加密代码定位与关键函数追踪3.1 全局搜索与断点调试如果通过“Initiator”没有直接找到加密点我们就得用更通用的方法关键词搜索。在开发者工具的Sources面板按CtrlShiftFWindows或CmdOptFMac打开全局搜索。搜索什么关键词呢这需要一些经验。首先尝试搜索密文中的片段。比如你得到的密文是7C4A8D09CA3762AF61E59520943DC26494F8941B可以取前6-8个字符7C4A8D09去搜索看是否有JS代码直接包含这个字符串可能是硬编码的盐值或测试用例。不过这种方式成功率不高。更有效的是搜索与密码字段相关的参数名。在Payload里密码的参数名是login_password那么就在所有JS文件里搜索login_password。你会找到所有对这个参数进行赋值或操作的地方。通常加密发生在表单提交submit事件处理函数里或者是在某个按钮的onclick事件里也可能是在像$.ajax的beforeSend函数或data序列化过程中。找到可疑的代码行后毫不犹豫地打上断点。在行号左侧点击即可添加一个蓝色的断点标记。然后回到页面再次点击登录。执行流会立即暂停在断点处。3.2 调用栈分析与变量监控当代码在断点处暂停时不要只看当前一行。右侧的Call Stack调用栈面板是宝藏。它显示了当前函数是被谁调用的层层回溯可以帮你理清整个加密的执行路径。你可以点击调用栈中的上一级函数查看当时的上下文和变量状态。同时在Scope作用域面板你可以看到当前作用域下的所有变量Local, Closure, Global等。找到那个存储着明文密码的变量可能叫password、pwd、inputPwd等。然后逐步执行F10是Step OverF11是Step Into进入函数。我们的目标是找到那个将明文密码转换成密文的函数。当你执行到类似encryptedPwd someFunction(plainPwd);或data.password encrypt(data.password);这样的语句时就接近核心了。用F11键“步入”这个someFunction或encrypt函数内部。3.3 致远OA的典型加密定位根据我对多个致远OA版本的分析其前端密码加密常见于一个名为login.js或包含rsa、encrypt关键字的JS文件中。加密函数可能被命名为encryptPassword、RSAEncrypt或者直接内联在提交逻辑里。一个典型的模式是代码会引入一个RSA公钥通常是一个很长的16进制字符串或Base64编码的字符串然后使用这个公钥对密码进行加密。你可能会看到类似new JSEncrypt()、setPublicKey(key)、encrypt(str)这样的调用。这就是使用了常见的jsencrypt库进行RSA非对称加密。另一种可能是使用自定义的哈希算法比如看到CryptoJS.MD5、CryptoJS.SHA1或者hex_md5这样的函数调用。这时就需要跟进这些函数的具体实现了。注意有些站点会对核心的加密JS代码进行混淆函数和变量名都变成了a、b、c、_0x123abc这种形式。这增加了阅读难度但基本逻辑不会变。我们的策略是不追求读懂每一行混淆后的代码而是通过调试观察输入明文和输出密文的对应关系并尝试在Node环境中复现这个转换过程。4. 加密算法分析与Python复现4.1 算法识别与参数提取假设我们通过调试确定了加密函数入口。比如我们跟进去发现核心代码是function encryptPwd(pwd) { var key MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...很长一串; var encryptor new JSEncrypt(); encryptor.setPublicKey(key); return encryptor.encrypt(pwd); }这很明显是RSA加密。我们需要提取两个关键信息1. 公钥key2. 使用的RSA填充方案Padding。JSEncrypt库默认使用的是RSA-OAEP填充具体是RSAES-OAEPwithSHA-1MGF1。这在后续Python复现时必须保持一致。如果看到的是哈希比如function encryptPwd(pwd) { return hex_md5(pwd a1b2c3d4); }那么算法是MD5并且使用了盐值Salta1b2c3d4是明文拼接后取哈希。4.2 Node.js环境验证算法在将算法移植到Python前最好先在Node.js环境验证我们的理解是否正确。在开发者工具的Console控制台中或者将抠出的相关函数代码包括依赖的jsencrypt.min.js或md5.js复制到一个本地JS文件中用Node运行。例如对于RSA情况创建一个test.js// 假设我们抠出了JSEncrypt库的源码保存为jsencrypt.js const JSEncrypt require(./jsencrypt.js).JSEncrypt; // 或者使用npm安装的jsencrypt库 const publicKey MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...; function encrypt(password) { const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); const encrypted encryptor.encrypt(password); console.log(明文: ${password}); console.log(密文: ${encrypted}); return encrypted; } // 测试 encrypt(123456);运行node test.js看输出的密文是否和抓包抓到的一致。如果一致恭喜你算法完全正确。如果不一致检查公钥是否正确、是否还有其他的预处理比如密码是否先被转成了UTF-8字节是否加了时间戳。4.3 使用Python进行算法复现验证成功后就可以用Python来实现了。Python拥有强大的密码学库cryptography和pycryptodome。情况一RSA加密复现如果目标使用RSA我们需要用Python模拟JSEncrypt的加密过程。JSEncrypt默认使用PKCS#1 OAEP填充方案。from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_OAEP import base64 def rsa_encrypt_password(password, public_key_pem): 使用RSA公钥加密密码模拟JSEncrypt行为。 :param password: 明文密码字符串 :param public_key_pem: PEM格式的公钥字符串 :return: Base64编码的加密结果 # 加载公钥 key RSA.import_key(public_key_pem) # 创建加密器使用默认的SHA-1哈希算法与JSEncrypt默认一致 cipher PKCS1_OAEP.new(key) # 加密。密码需要编码为bytes encrypted_bytes cipher.encrypt(password.encode(utf-8)) # JSEncrypt默认输出Base64 encrypted_b64 base64.b64encode(encrypted_bytes).decode(utf-8) return encrypted_b64 # 使用抓取到的公钥注意是PEM格式通常以-----BEGIN PUBLIC KEY-----开头 public_key -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC... -----END PUBLIC KEY----- plain_pwd 123456 cipher_text rsa_encrypt_password(plain_pwd, public_key) print(f加密结果: {cipher_text})实操心得有时从网页JS中提取的公钥可能是去掉头尾和换行的“裸”Base64字符串。你需要手动为其加上-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----头尾并确保格式正确每64字符换行。这是PythonRSA.import_key方法所要求的PEM格式。情况二MD5带盐哈希复现如果算法是MD5加盐那就简单多了。import hashlib def md5_salt_encrypt(password, salt): MD5(密码盐值) 的Hex输出。 # 将密码和盐值拼接 s password salt # 创建MD5对象更新数据 m hashlib.md5() m.update(s.encode(utf-8)) # 返回十六进制字符串 return m.hexdigest() plain_pwd 123456 salt a1b2c3d4 # 从JS代码中提取的盐值 cipher_text md5_salt_encrypt(plain_pwd, salt) print(fMD5加盐结果: {cipher_text})确保这里的cipher_text与抓包数据一致。5. 逆向过程中的常见陷阱与解决方案5.1 动态密钥与时间戳混淆有些系统的加密并非静态。你可能会发现公钥或者盐值不是硬编码在JS里的而是通过一个额外的Ajax请求从服务器获取的。或者加密前的字符串拼接了一个动态生成的时间戳或随机数。应对策略在Network面板仔细查找登录请求之前发生的其他请求。可能有一个获取密钥或初始化参数的请求。你需要用脚本先模拟这个请求拿到动态的密钥或盐值然后再进行加密计算。这相当于将“登录”这个动作拆解成了“获取加密参数”和“提交加密凭证”两个步骤。5.2 代码混淆与反调试手段现代网站会采用各种手段增加逆向难度。变量名混淆将encryptPassword变成_0x12ab3c。这并不影响执行逻辑但让代码难以阅读。调试时不要纠结于变量名而是关注输入流和输出流。在关键函数入口和出口设置断点观察参数和返回值。控制流扁平化将简单的if-else或switch语句打散成用一个大switch调度执行的代码块破坏可读性。对付这个依然是调试。跟着断点一步步走记录下真实的执行路径。反调试有些代码会检测开发者工具是否打开如果打开则跳转到错误页面或进入死循环。常见手段有检查console.log的引用、检测代码执行时间差等。解决方案可以尝试使用“无头浏览器”如Puppeteer或Playwright进行自动化它们通常不受前端反调试影响。或者在Chrome中可以尝试在开发者工具打开的状态下先刷新页面再快速设置断点条件断点有时能绕过。也有浏览器插件可以禁用反调试。5.3 编码与填充细节差异这是导致Python复现结果与JS不一致的最常见原因。字符串编码JS中字符串是UTF-16吗加密前是否调用了unescape(encodeURIComponent(str))这类函数进行UTF-8转换在Python中我们通常直接str.encode(utf-8)但必须和JS端保持一致。Base64编码JS的btoa函数对非Latin1字符处理有问题所以常用Base64.encode或自己实现的函数。Python的base64.b64encode输出是bytes需要.decode(ascii)才是字符串。注意是否有URL安全的Base64将/替换为-_。RSA填充除了PKCS1_OAEP还有PKCS1_v1_5。必须确认JS库使用的是哪一种。JSEncrypt默认是OAEP。排查技巧在JS加密函数的入口和出口分别用console.log打印出中间变量的类型和值比如加密前的字节数组、加密后的字节数组、Base64编码前的字节数组。然后在Python中在对应步骤也打印出来进行逐字节比对。这是最笨但最有效的方法。6. 构建健壮的自动化登录脚本6.1 脚本结构设计当我们成功逆向出加密算法后目标就是写一个稳定的脚本。脚本不应该只是硬编码加密函数而应该具备一定的健壮性。一个健壮的登录脚本结构如下会话维持使用requests.Session()对象它会自动处理Cookies模拟浏览器会话。参数获取如果加密需要动态密钥先写一个函数get_encryption_params()来获取。加密模块将验证通过的加密算法封装成一个独立的函数或类如PasswordEncryptor。登录执行构造请求头特别是User-Agent、Content-Type发送POST请求。状态验证检查返回的响应。成功登录后服务器通常会设置一个会话Cookie如JSESSIONID并可能跳转或返回特定的JSON状态码。后续的请求都需要携带这个Cookie。6.2 请求头模拟与错误处理网站可能会检查请求头。至少需要模拟以下头部headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, # 根据实际情况调整 Referer: http://oa.example.com/seeyon/index.jsp, # 登录页地址 Origin: http://oa.example.com, # 协议域名 X-Requested-With: XMLHttpRequest # 如果是Ajax请求 }错误处理至关重要。网络请求可能超时、服务器可能返回4xx/5xx错误、登录可能因密码错误/账号锁定而失败。使用try...except包裹请求代码并对响应状态码和内容进行判断。import requests from requests.exceptions import Timeout, ConnectionError session requests.Session() login_url http://oa.example.com/seeyon/login.do try: # 1. 获取动态参数如果需要 # params get_dynamic_params(session) # 2. 加密密码 encrypted_pwd encrypt_password(your_password, public_key) # 或 params[key] # 3. 构造数据 data { login_username: your_username, login_password: encrypted_pwd, # 可能还有其他隐藏字段如CSRF token # lt: params[lt], # execution: params[execution] } # 4. 发送请求 resp session.post(login_url, datadata, headersheaders, timeout10) resp.raise_for_status() # 如果状态码不是200抛出HTTPError # 5. 判断登录结果 if 登录成功 in resp.text or main.jsp in resp.url: print(登录成功) # 保存session用于后续请求 # 例如session.get(http://oa.example.com/seeyon/main.do) else: print(登录失败响应内容:, resp.text[:500]) # 打印前500字符用于调试 except Timeout: print(请求超时) except ConnectionError: print(网络连接错误) except Exception as e: print(f发生未知错误: {e})6.3 应对登录验证码与风控一些系统在多次失败登录或检测到异常行为如高频请求、陌生IP后会触发验证码图片、滑块、点选等或更强的风控。验证码如果是简单图片验证码可以考虑使用OCR库如ddddocr、tesseract但识别率需测试进行识别。对于复杂验证码可能需要接入打码平台。更“友好”的做法是在脚本中识别到验证码后暂停并提示用户手动输入。风控避免高频请求在请求间加入随机延时如time.sleep(random.uniform(1, 3))。使用高质量的代理IP池来切换IP地址。模拟更真实的浏览器指纹通过selenium或playwright这类自动化浏览器工具可以更好地做到这一点但资源消耗更大。7. 进阶算法通用化与经验抽象完成一个案例后我们应该尝试将经验抽象成通用方法这样下次遇到类似问题就能更快上手。7.1 建立JS逆向分析 checklist每次分析都可以按以下清单进行抓包定位找到登录请求确认密码被加密。搜索关键词搜索密码参数名、encrypt、password、RSA、MD5、SHA等。断点调试在可疑函数设断点跟踪明文到密文的转换过程。定位核心函数找到执行加密的最终函数。提取关键参数提取公钥、盐值、模式、IV对于AES等。算法识别判断是标准算法RSA、AES、MD5、SHA还是自定义算法。环境验证在Node.js或浏览器控制台复现加密过程。Python移植使用对应密码学库复现算法。集成测试将加密函数嵌入到登录脚本中进行端到端测试。7.2 常见加密模式速查与应对RSA非对称特征是有公钥。使用JSEncrypt或node-rsa库。Python对应cryptography或pycryptodome的PKCS1_OAEP/PKCS1_v1_5。AES对称特征是有密钥Key和可能有的初始化向量IV。JS常用CryptoJS.AES.encrypt。Python使用Crypto.Cipher.AES注意模式CBC、ECB等和填充PKCS7。哈希MD5、SHA1、SHA256特征是不可逆输出固定长度。JS可能用CryptoJS.MD5或自己实现的函数。Python用hashlib。自定义算法可能是几种算法的组合如先MD5再Base64再反转字符串。只能通过调试一步步记录转换过程然后用Python忠实还原每一步。7.3 保持学习与工具更新前端安全技术在不断演进新的混淆技术如WebAssembly用于加密、新的反调试手段层出不穷。保持对以下方面的关注新工具如AST抽象语法树反混淆工具虽然可能被滥用但了解其原理有助于理解混淆、浏览器自动化框架Puppeteer, Playwright。标准协议深入了解HTTPS、WebSocket、JWT等有时加密信息会藏在协议头或Token里。社区关注安全社区和逆向爱好者的分享了解最新的对抗案例和解决方案。逆向分析就像解谜需要耐心、细致的观察和逻辑推理。每一次成功的逆向不仅解决了一个具体的技术问题更深化了对Web应用前后端交互、数据安全传输的理解。记住技术的价值在于应用在合法合规的前提下利用这些技能去提升工作效率、进行安全测试或学习研究才是正道。