前言
經過一段時間的準備,終於獲得了回報。我在黑龍江省九月份場次的業餘無線電A證考試中,取得了滿分,順利考取了屬於自己的業餘無線電操作證。
無論是作為一個業餘無線電愛好者,或是持續學習的人,最重要的就是不斷地探索和吸收知識,不要固守成見。我深知這個道理,因此加入了…HamCQ 社區,計劃與同樣熱愛的人們進行友好的交流。
當我發現該社群有「操作證類別認證」功能時,毫不猶豫地填寫了姓名及個人身分證號碼進行驗證,很快就通過了驗證。因為驗證時間很短,我開始懷疑這個驗證是否是人工操作,如果不是,那麼是如何查詢並驗證相關的證書呢?帶著這種好奇心,我開始撰寫這篇文章的研究記錄。
資訊蒐集
經過一番調查,目前已發現只有…業餘無線電台操作技術能力資訊平台以及驗證業餘無線電台操作技術能力,以及資訊管理系統可以查詢證書,但「工業及資訊委員會」的政務服務平台,其業餘無線電台操作技術能力資訊平台,則需要姓名和手機號碼作為查詢條件,並需接收驗證碼才能查詢,顯然不符合社區查詢時所使用的條件。
因此,業餘無線電台的操作技術能力驗證以及資訊管理系統,便成為這次研究的對象。
該系統的查詢條件為姓名、證書編號(可選)、身分證號碼,符合社群查詢條件。推測社群驗證過程可能是社群管理員手動查詢或呼叫查詢介面進行資訊處理和判斷。若對此感興趣,務必進一步探索。
研究過程
對於前後端分離的網站,在處理資料時,尤其是在新增、刪除、修改和查詢等操作,都需要透過向 API 介面發送網路請求來實現。
研究要嚴謹「望聞問切」要對付這種問題,絕對不能草率行事,必須仔細觀察後再採取行動,針對其弱點進行精準的攻擊。
首先從「望」開始,觀察網頁,經典的三部曲:「輸入資訊」、「點擊查詢」、「回傳資訊」。一般思路是輸入資訊,點擊查詢按鈕後向後端發送Get或Post請求,伺服器接受請求後查詢資料庫,並返回一定格式的數據(例如JSON),頁面上的每個列表項目都是一個item,回傳的資訊經過JS解析,再輸出到頁面上。了解了原理後,就可以進行下一步的診斷:「聞」。
什麼?你竟然從沒聽過這種東西?我會盡量用簡單易懂的方式解釋,但要真正理解我在做什麼,還是需要自己具備一定的網路知識。
「問」是指透過分析發送的請求以及回傳的資料,找出介面地址、判斷所使用的技術、分析所需之數據。首先填入正確的資訊來模擬正常操作,查看網路請求的內容。開啟開發者控制台,輸入查詢條件,點擊查詢,運氣不錯,發現唯一的網路請求,這正是今天的主角。

請進行分析
請求標頭
點擊標頭,可以得知這是一個使用 POST 方法的請求,API 介面地址是:http://82.157.138.16:8091/CRAC/app/businessSupport/cracOperationCert/getOperCertByParamWeb,請檢查請求標頭,請求中沒有加密參數,Cookie 也是常見的方式來延長 Session 的生命週期。JSESSIONID這表示這個介面可以隨意發送請求,並且不需要任何驗證。
接受:
*/*
accept-encoding:
gzip, deflate
accept-language:
zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
connection:
keep-alive
content-length:
114
content-type:
application/json; charset=UTF-8
cookie:
JSESSIONID=74D062BFB81FCD71961AD181B4C9D2D5
host:
82.157.138.16:8091
mm:
null
origin:
http://82.157.138.16:8091
qm:
null
referer:
http://82.157.138.16:8091/CRAC/crac/pages/list_cert.html
user-agent:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
x-requested-with:
XMLHttpRequest
請求體
與 GET 請求不同之處,POST 請求包含「請求體」,而非將參數附加到 HTTP 連結後。因此,若要了解我們點擊按鈕後發送了什麼內容,就需要查看請求體。

{"req":{"頁碼":"1","每頁數量":"100","姓名":"***", 證書編號:"",車牌號碼:"******************"}}
這段請求體採用 JSON 格式,簡單分析一下欄位:page_no:頁碼、page_size:頁面最多顯示的數量。name:姓名、certificateNo:證書編號、idCarNumber:身分證號
既然已經了解了請求體的內容和格式,就可以繼續進行接下來的操作了。
「切」這個部分,是整個研究中想像力發揮最豐富的階段。透過使用各種介面技術,我將使用簡單易懂的 Python 來進行介面呼叫。
在 Python 中有很多用於處理網路請求的函式庫,其中比較實用的莫過於:requests只需幾步簡單的步驟即可完成模擬網路請求。 在這個腳本中,為了使程式碼更完善,在輸入時自動進行輸入驗證,腳本會自動偵測輸入是否有效並顯示提示;在請求時自動判斷是否成功。 請求成功後,將取得的資訊輸出到介面上。
錯誤輸入結果

正確輸入結果

原始碼如下:
```python
import re
import requests
import json
# 中文姓名正则表达式
chinese_name_pattern = r'^[\u4E00-\u9FA5]{2,4}$'
# 身份證號碼正規表示式
_IDRe18 = re.compile(r'^([1-6][1-9]|50)\d{4}(18|19|20)\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$')
_IDre15 = re.compile(r'^([1-6][1-9]|50)\d{4}\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\d{3}$')
# 輸入提示
page_no = 1
page_size = 100
while True:
name = input("請輸入姓名:")
if re.match(chinese_name_pattern, name):
break
print("姓名應為2-4位中文,請重新輸入。")
while True:
certificateNo = input("請輸入證號(可選):")
if not certificateNo:
break
pattern = r'^[ABC]\d{9}$'
if re.match(pattern, certificateNo):
break
print("證號格式應為首字母 ABC 加 9 位數字,請重新輸入。")
while True:
idCarNumber = input("請輸入身份證號碼:")
if _IDRe18.match(idCarNumber) or _IDre15.match(idCarNumber):
break
print("身份證號碼格式不正確,請重新輸入。")
# API 請求
url = "http://82.157.138.16:8091/CRAC/app/businessSupport/cracOperationCert/getOperCertByParamWeb"
headers = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"Connection": "keep-alive",
"Content-Type": "application/json; charset=UTF-8",
"Cookie": "",
"Host": "82.157.138.16:8091",
"Origin": "http://82.157.138.16:8091",
"Referer": "http://82.157.138.16:8091/CRAC/crac/pages/list_cert.html",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"X-Requested-With": "XMLHttpRequest",
"mm": "null",
"qm": "null"
}
data = {
"req": {
"page_no": page_no,
"page_size": page_size,
"name": name,
"certificateNo": certificateNo,
"idCarNumber": idCarNumber
}
}
try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status() # 檢查請求是否成功
except requests.exceptions.RequestException as e:
print(f"發送請求時發生錯誤:{e}")
exit()
try:
prc_list = response.json()['res']['prcList']
except json.JSONDecodeError:
print("回應資料不是有效的 JSON 格式")
exit()
if not prc_list:
print("未找到相關證件資訊")
exit()
# 提取有用資訊並格式化輸出
for item in prc_list:
print(f"身份 ID: {item.get('id', '')}")
print(f"姓名: {item.get('name', '')}")
print(f"性別: {item.get('sex', '')}")
print(f"操作類別: {item.get('type', '')}")
print(f"證號: {item.get('certificateNo', '')}")
print(f"通過日期: {item.get('passDate', '')}")
print(f"通過地點: {item.get('passAddr', '')}")
print(f"核發日期: {item.get('issueDate', '')}")
print(f"核發機構: {item.get('addr', '')}")
print("---------------------------")
```
結尾
透過這次的分析,我發現該查詢介面並沒有實施加密措施,而且容易被呼叫,因此這個社群很可能自行利用該介面,並進行二次開發以完成憑證驗證。這次的分析讓我重新體驗了從判斷頁面類型、模擬網路請求到分析網路請求並進行呼叫測試的過程,感覺良好。這是我在…HamCQ 社區發表第一篇文章,希望能未來與更多喜歡的人交流,73!再見!