JS逆向实战-找到那个flag
This_is_Y Lv6

目标站点情况

如下图,可以看到请求和响应都加密了,不过有突破口,可以看到请求参数是{“xxx-param”:”xxxxxxxxx”}这种格式。运气好的话,只需要去F12搜索xxx-param即可定位到目标。

image-20241121201549162

定位加解密函数

直接搜索xxx-param,10秒钟定位到目标函数。但是看到encrypt后面跟的这个字符串,感觉不妙,怕是非对称加密。先不管,根据加密的代码,找一下解密代码,果不其然,现在已经拿到了加解密函数:

  • y[“b”].encrypt()
  • y[“b”].decrypt()

以及加密用到的公钥req-pubkey以及解密需要的私钥resp-prikey

  • MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVYKTzIhIQiUkixXUhLakag96jJ7L+9cBXUjWVCyluXKlX+VeG7BROK6OiV5apk6f36tlm52q61gE1bqRk+m2K2w6KqV…………0C+ouzJ+GU1wKcGAxEZS5+bjzT7HoRgt5QIDAQAB
  • MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAI3ZNKQ18Tdy+Gr/9wsKNziXwYKocj/62szhgptKURTZfaDs2RCFnY1cUt3JIpoFZRb828iyWCzEgaQF9quOOqzFJ/rPtevkwmOswvBEf0RUDVufuHuRRHSc8jy6MwK/1j7t4T22WCHwTw5ioIG9H98KXNvY9nWk4O5asn2IsMb7AgMBAAECgYAL1afmiBjwezdOV2hpSOMTkIG/dhtWIDFPb68sZ/ZngBUoWdUVuPgh+lEa757jQnj319qPHjtsvMEONMXVnrUMDknNPI1L7gxRPUT4ljcLGYPimDsgCEyK/QOtTGmKOblDre0tTu56TlPEtLrI1HR4eD2mS2DSY9jpxKWm……………w+Uc2kmVKlYMhxkAn0VP8gdlDRQ/yNhVhdPSlEZ8UYsY4rIyN/BezRAkEAh3LmxD2Dxmp4OY/3GFNRGdNeoHpzQU2SLW58Xfvia3gtTQegh1DATtWxbmpf4ne2wAQPH/6VHyfr4B9/9Cc76wJAChq/yE5pTFGoIXp7azhsBVMynFiJTyg5zLvkHCB9Yysirv1pXiUNd7SjBmLHwORNi4We7e0U4R5QH9KjzgAAEQJBALR+KB5NmY1e8tsl8RzUNrrTjnk+DdFry3/qRo/lEbDVmcXuNOcx9I2QU4BB1gZwVv5knjDm8Xqy3u3TIhYHTe8=

image-20241121201535938

image-20241121201805875

因为是非对称加密,所以我如果想解密请求参数,或者篡改响应重新加密,就必须拿到请求加密的公钥对应的私钥(req-prikey)和解密响应的私钥对应的公钥(resp-pubkey)。一般情况下是拿不到这东西,不过有些开发可能会用req-pubkey加密响应返回,也就是说,请求和响应的加解密用的是一套密钥。这里我抓一个包,拿到密文请求,在console中尝试使用resp-prikey进行解密。可以看到上面是解密响应密文,正常发挥了明文结果,但是下面是尝试解密请求密文,显然不行,这个系统的开发们还是可以的。

image-20241121203815281

编写脚本

那么在这种情况下,我如果想要在bp中直接抓明文数据包,查看明文响应该怎么办呢,查看明文响应很简单,直接使用JsRpc调用y[“b”].decrypt()即可。那抓明文请求,这里我的思路是,修改加密函数,使之返回明文,然后在bp中通过JsRpc调用y[“b”].encrypt()将函数再加密。思路有了,先把加解密函数代理出来。

1
2
window.rsaencode = y["b"].encrypt
window.rsadecode = y["b"].decrypt

image-20241121205042065

然后放掉debug断点,开始连接JsRpc

image-20241121205306877

image-20241121205332456

再然后,先把解密函数写完,这边思路比较简单,在处理响应密文时,我保留了原始密文,在原始响应json中新加入了一个decode-data来存放解密后的明文。想过如图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def rsadecode(data):
'''
传入响应中的后的data
'''
jscode = """rsadecode(\"MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAI3ZNKQ18Tdy+Gr/9wsKNziXwYKocj/62szhgptKURTZfaDs2RCFnY1cUt3JIpoFZRb828iyWCzEgaQF9quOOqzFJ/rPtevkwmOswvBEf0RUDVufuHuRRHSc8jy6MwK/1j7t4T22WCHwTw5ioIG9H98KXNvY9nWk4O5asn2IsMb7AgMBAAECgYAL1afmiBjwezdOV2hpSOMTkIG/dhtWIDFPb68sZ/ZngBUoWdUVuPgh+lEa757jQnj319qPHjtsvMEONMXVnrUMDknNPI1L7gxRPUT4ljcLGYPimDsgCEyK/QOtTGmKOblDre0tTu56TlPEtLrI1HR4eD2mS2DSY9jpxKWmezhw0QJBAMTOMorcqGhJI9eFukVKZ………………BezRAkEAh3LmxD2Dxmp4OY/3GFNRGdNeoHpzQU2SLW58Xfvia3gtTQegh1DATtWxbmpf4ne2wAQPH/6VHyfr4B9/9Cc76wJAChq/yE5pTFGoIXp7azhsBVMynFiJTyg5zLvkHCB9Yysirv1pXiUNd7SjBmLHwORNi4We7e0U4R5QH9KjzgAAEQJBALR+KB5NmY1e8tsl8RzUNrrTjnk+DdFry3/qRo/lEbDVmcXuNOcx9I2QU4BB1gZwVv5knjDm8Xqy3u3TIhYHTe8=\",'{}')""".format(data)
url = "http://localhost:12080/execjs"
data = {
"group": group,
"code": jscode
}
print(data)
res = requests.post(url, data=data)
return json.loads(res.text)['data']




@app.route('/decode',methods=["POST"])
def decrypt():
print("==============/decode==================")
dataHeaders = request.form.get("dataHeaders") # 获取 headers
dataBody = request.form.get('dataBody').replace("\n",'') # 获取 body 参数
logging.info("dataHeaders>>\n"+dataHeaders)
logging.info("dataBody>>\n"+dataBody)

data = json.loads(dataBody)
if '000' in data['code']:
print(data['data'])
decodedata = rsadecode(data['data'])
print(decodedata)
# 生成新字段
data['decode-data'] = json.loads(decodedata)
return dataHeaders+"\r\n\r\n\r\n\r\n"+json.dumps(data,indent=4, ensure_ascii=False)
else:
return dataHeaders+"\r\n\r\n\r\n\r\n"+dataBody

image-20241121210045302

image-20241121210107042

再然后是加密过程,首先要修改加密函数,让它返回明文。由于我撇脚的js和hook水平,这里我找AI要了个hook函数。注意,这里要避开JsRpc连接的那个标签页,要重新开一个标签页hook。

1
2
3
4
5
6
7
8
9
10
const originalEncrypt = y["b"].encrypt;
y["b"].encrypt = new Proxy(originalEncrypt, {
apply: function(target, thisArg, argumentsList) {
// 返回第二个参数作为结果
return argumentsList[1];
}
});

// 恢复原始的 encrypt 函数
y["b"].encrypt = originalEncrypt;

image-20241121210651521

现在在这个hook后的标签页中点击各种功能点,就可以发现抓到的包都是密文了。

image-20241121211859308

但是可以看到,我明明传入了companyId,却提示”入参companyId不能为空”。显然我现在还需要将参数再次加密,这里就和前面写解密一样了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def rsaencode(data):
'''
传入格式化后的body
'''
jscode = """rsaencode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVYKTzIhIQiUkixTy0vv4q1ITi6TvQDztWdldTR5hQ4vi6M/xYPJDO5ardJ1FXUhLakag96jJ7L+9cBXUjWVCyluX………………+GU1wKcGAxEZS5+bjzT7HoRgt5QIDAQAB",'{}')""".format(data)
url = "http://localhost:12080/execjs"
data = {
"group": group,
"code": jscode
}
print(data)
res = requests.post(url, data=data)
return json.loads(res.text)['data']



@app.route('/encode',methods=["POST"])
def encrypt():
print("==============/encode==================")
dataHeaders = request.form.get("dataHeaders") # 获取 headers
dataBody = request.form.get('dataBody').replace("\n",'') # 获取 body 参数
logging.info("dataHeaders>>\n"+dataHeaders)
logging.info("dataBody>>\n"+dataBody)

data = json.loads(dataBody)
# print(data['xxx-param'])
# 生成新字段
encodedata = rsaencode(data['xxx-param'])
return dataHeaders+"\r\n\r\n\r\n"+json.dumps({"xxx-param":encodedata})

image-20241121213826092

现在把这个密文数据包发送,再配置之前写好的解密模块。

image-20241121213001662

image-20241121213209416

最后就是针对GET,特殊符号的脚本优化过程,这里就不写了。

在我把脚本给同事用了一段时间后,他和我说这系统能直接接收明文参数,而且如果你传入的数据是明文,那返回的也是明文。如下图:

image-20241121213721149

然后晚上回家后,在测这个系统的app端时,也意外发现我抓到的包是明文请求明文响应,域名地址和web端相同,但是第二天上班再抓就发现变成了密文,这意味着服务器是可以同时接收明文参数和密文参数。而且在app上两次抓包结果不一样,说明系统应该有一个参数控制请求参数是否需要加密,而我现在就想找到这个参数。

思路比较简单暴力,在y[“b”].encrypt()加密前,一层一层回溯,查找仔细看if判断和switch类的代码。最后经过枯燥的回溯后,找到这里。

image-20241121222021395

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (e.prev = e.next) {
case 0:
if (C || _(t.url)) {
e.next = 4;
break
}
return t.headers = v(v({}, t.headers || {}), {}, {
"xxx-method": "rsa"
}),
e.next = 4,
a();
case 4:
case "end":
return e.stop()
}

在这个代码的if中,有一个 || 或判断,在这里,只要C或者 _(t.url)中至少有一个为true,就会走下面的e.next = 4然后结束循环,反之就走下面的代码,虽然也会有e.next = 4,但是下面的a(),跟进后可以看到就是加密函数

image-20241121223010547

所以思路一下子就清晰了,要让C或者 _(t.url)返回true。

C

先说C。C变量是false,由于学艺不精和工作上的干扰,花了半个多小时的回溯才找到了C的定义,但又是因为学艺不精,看不懂代码(先挖个坑,明天上班的时候补上)。

不过可以记录一下找这个C变量的方法,断点到相关代码,在右边作用域中一层一层找,找到这个闭包(2d81),然后开始搜关键字2d81,主要是去查闭包(2d81)的定义位置,查看整个代码中有关 2d81 的定义部分,可能会找到变量 C 的赋值语句,这里搜了一下,不多。

image-20241121232835818

但是这个代码块太大,我把文件下载下来后,在vscode中格式化查看,然后再大小写敏感的搜了一下C,就找到了

1
C = !Object(p["p"])() && Object(p["d"])()

image-20241121233022782

浏览器中的返回结果也确定了这就是C的定义代码

image-20241121233134762

跟进这两个函数后可以看到,p是检查ua中是否存在xxxsdk,而d是检查ua中是否存在EQBANDROID、EQBIOS、QYBHarmonyOS这三个字符串。

image-20241121233950859

那总结一下,如果要C为true,则需要ua中不能出现xxxsdk,且要出现EQBANDROID、EQBIOS、QYBHarmonyOS这三个字符串其中一个或多个,而且字符串大小写不敏感。

测试一下,用浏览器插件改个ua,再刷新一下页面

image-20241121234449051

image-20241121234804594

ok,解决!

_(t.url)

C解决了,再来看 _(t.url),这个_函数的入参是当前请求的url,而JSON.parse(sessionStorage.getItem(“fetchignore”))中是一堆接口url,有一些验证码、登录相关的接口,可能是白名单,无需加密。所以可以大致猜测这个函数是判断当前请求是否在白名单中。

image-20241121224213698

image-20241121224333098

其实分析函数作用也没啥用,直接改个返回就可以了,把这个函数的返回结果改为true就可以了,直接写在burp的replace rule中,然后重新加载这个index.js文件就好了,这里有个细节是,最好在burp中找到相关字符串拿过来进行替换,而不是用浏览器中的字符串,浏览器中的js代码一般是格式化美化过的,可能会多一些缩进和空格,burp会匹配不到。

image-20241121225714076

image-20241121225914549

(此外,关于app,由于app部分页面是h5加载的,所以可以在它加载js时修改_函数,不过app的js是在chunk.package.js中,函数内容一样。)

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
访客数 访问量