上新!更强大的JS引擎:goja
字数 1133 2025-08-19 12:42:07
Goja JavaScript引擎替换Otto的全面教学文档
1. 背景与动机
Yaklang引擎原本使用Robertkrimen/otto(简称otto)作为JavaScript解释器,但遇到以下问题:
- otto的语法树解析实现存在缺陷
- 处理压缩后的JavaScript第三方依赖(如CryptoJS)时出现解析错误
- 性能较低
因此决定采用dop251/goja(简称goja)作为替代方案。
2. Goja的优势
2.1 功能特性
- 完整的ECMAScript 5.1实现
- 部分ECMAScript 6.0功能支持
- 通过了大部分tc39测试套件(官方ECMAScript一致性测试)
2.2 性能提升
- 比otto快6~7倍
2.3 兼容性
- 80%的API保持不变
- 升级对用户几乎无感知
3. 不兼容点
以下功能可能存在差异:
js.ASTWalkjs.GetSTTypeValue结构体方法
4. 新特性:内置第三方JavaScript依赖API
4.1 主要特点
- 快速使用JavaScript第三方依赖
- 初次编译后缓存,后续无需重新编译
- 解决常见库(如CryptoJS)的使用问题
4.2 使用示例
_, value = js.Run(`
CryptoJS.HmacSHA256("Message", "secret").toString();
`, js.libCryptoJSV3())
println(value.String())
5. 实战案例:CryptoJS.AES(CBC)前端加密登陆表单
5.1 目标分析
分析Vulinbox靶场中的高级前端加解密与验签实战案例,前端使用CryptoJS.AES(CBC)模式加密登录表单。
5.2 前端加密逻辑
var iv = CryptoJS.lib.WordArray.random(128/8);
function generateKey() {
return CryptoJS.enc.Utf8.parse("1234123412341234") // 16位密钥
}
const key = generateKey()
function Encrypt(word) {
return CryptoJS.AES.encrypt(word, key, {iv: iv}).toString();
}
function getData() {
return {
"username": document.getElementById("username").value,
"password": document.getElementById("password").value,
}
}
function outputObj(jsonData) {
const word = JSON.stringify(jsonData);
return {
"data": Encrypt(word),
"key": key.toString(),
iv: iv.toString(),
}
}
function submitJSON(event) {
event.preventDefault();
const url = "/crypto/js/lib/aes/cbc/handler";
let jsonData = getData();
let submitResult = JSON.stringify(outputObj(jsonData), null, 2)
// 发送submitResult到后端
}
5.3 提炼加密代码
key = CryptoJS.enc.Utf8.parse("1234123412341234");
iv = CryptoJS.lib.WordArray.random(128/8);
function Encrypt(word) {
return CryptoJS.AES.encrypt(word, key, {iv: iv}).toString();
}
function getData(username, password) {
return {
"username": username,
"password": password,
}
}
function outputObj(jsonData) {
const word = JSON.stringify(jsonData);
return {
"data": Encrypt(word),
"key": key.toString(),
iv: iv.toString(),
}
}
jsonData = getData("username", "password");
submitResult = JSON.stringify(outputObj(jsonData), null, 2);
5.4 Yak爆破实现
for user in ["user", "admin"] {
for pass in ["pass", "123456"] {
vm, _ = js.Run(`
key = CryptoJS.enc.Utf8.parse("1234123412341234");
iv = CryptoJS.lib.WordArray.random(128/8);
function Encrypt(word) {
return CryptoJS.AES.encrypt(word, key, {iv: iv}).toString();
}
function getData(username, password) {
return {
"username": username,
"password": password,
}
}
function outputObj(jsonData) {
const word = JSON.stringify(jsonData);
return {
"data": Encrypt(word),
"key": key.toString(),
iv: iv.toString(),
}
}
jsonData = getData(%#v, %#v);
submitResult = JSON.stringify(outputObj(jsonData), null, 2);
` % [user, pass], js.libCryptoJSV3())
body = vm.Get("submitResult").String()
rsp, _ = poc.Post(
"http://127.0.0.1:8787/crypto/js/lib/aes/cbc/handler",
poc.replaceBody([]byte(body), false)
)
println(string(rsp.RawPacket))
}
}
6. 迁移指南
6.1 准备工作
- 确认现有代码中是否使用了otto特有的API
- 备份现有项目
6.2 迁移步骤
- 更新依赖到goja版本
- 替换导入路径
- 测试核心功能
- 检查不兼容点(如ASTWalk等)
- 更新第三方JS库的调用方式
6.3 测试要点
- 性能基准测试
- 功能一致性测试
- 第三方库兼容性测试
7. 最佳实践
- 性能优化:利用goja的缓存机制,避免重复编译
- 错误处理:增加对ES6特性的兼容性检查
- 安全实践:隔离敏感操作的JavaScript执行环境
- 调试技巧:利用
vm.Get()检查中间状态
8. 常见问题解答
Q1: 为什么我的otto代码在goja中不工作?
A1: 检查是否使用了otto特有的API或行为,特别是AST相关操作。
Q2: 如何确保第三方JS库的兼容性?
A2: 使用js.libXXX()系列函数加载标准库,并测试压缩和非压缩版本。
Q3: 性能提升不明显怎么办?
A3: 检查是否有频繁创建VM实例的操作,尽量复用实例。
Q4: 如何处理ES6特性?
A4: goja对ES6支持有限,建议使用Babel等工具转译为ES5。