在資料處理和資安測試中,從 JSON 資料擷取資訊和進行 DNS 列舉是常見的需求。本文將介紹如何使用 jq 工具提取 JSON 資料中的特定欄位,例如電子郵件、姓名和職位,並將其格式化為 TSV 格式,方便後續分析。此外,我們也會探討如何使用 Bash 指令碼自動化 DNS 列舉和資產探查,特別是如何從公司的網域名稱出發,找出相關的 Microsoft 365 租戶和網域名稱,並檢查 Microsoft Defender for Identity 的佈署情況。這些技術對於滲透測試和安全評估至關重要,能幫助安全人員快速發現潛在的資安風險。透過結合 jq 的資料處理能力和 Bash 的指令碼功能,我們可以更有效率地進行資料分析和資安研究。
使用 jq 提取與格式化 JSON 資料
擷取與格式化 JSON 資料
在進行測試或資料分析時,常常需要從 JSON 資料中提取特定資訊。這裡我們使用 jq 工具來處理 JSON 資料。首先,讓我們來看看如何檢視 JSON 資料的結構。
假設我們有一個包含員薪水訊的 JSON 檔案 employees.txt,我們可以使用以下命令將其內容顯示在終端機上:
cat employees.txt
這樣可以幫助我們瞭解 JSON 資料的結構。通常,JSON 資料的第一層會是一個名為 data 的鍵。接下來,我們可以使用 jq 來提取這些資料。
基本的 jq 查詢
最簡單的 jq 查詢是 jq ..,這個查詢會將輸入的資料原封不動地輸出。由於我們需要存取 data 鍵下的資料,因此我們的查詢會從 .data[] 開始。以下是範例命令:
cat employees.txt | jq -r '.data[]'
這個命令會顯示所有位於 data 鍵下的資料。其中 -r 選項表示以原始資料形式輸出,避免顯示逃避字元和引號。
內容解密:
.data[]:這個查詢表示我們要存取data鍵下的所有元素。-r:這個選項告訴jq以原始資料形式輸出,避免顯示逃避字元和引號。
提取巢狀資料
在 data 鍵下,員工的電子郵件、名字和職位都位於 emails 鍵下。因此,我們需要進一步巢狀查詢。以下是範例命令:
cat employees.txt | jq -r '.data.emails[]'
這樣可以看到所有員工的電子郵件、名字和職位資訊。
內容解密:
.data.emails[]:這個查詢表示我們要存取data.emails鍵下的所有元素。- 在查詢巢狀資料時,使用
.first_level.second_level[]的形式來存取更深層次的資料。
提取特定欄位
假設我們想要提取電子郵件地址、名字和職位,可以使用以下查詢:
cat employees.txt | jq -r '.data.emails[] | [.value, .first_name, .last_name, .position]'
這樣可以得到一個包含電子郵件、名字和職位的陣列。
內容解密:
[.value, .first_name, .last_name, .position]:這個查詢表示我們要提取value,first_name,last_name, 和position欄位。.value:表示電子郵件地址。.first_name:表示名字。.last_name:表示姓氏。.position:表示職位。
格式化為 TSV
為了便於進一步處理,我們可以將這些資料格式化為分隔符號(例如 TSV)格式。首先,檢視 jq 的手冊來瞭解如何進行格式化:
man jq
在手冊中,我們可以找到關於格式化字串和轉義的部分。其中提到 CSV 和 TSV 分別用 @csv 和 @tsv 表示。因此,我們可以將前面的查詢結果轉換為 TSV 格式:
cat employees.txt | jq -r '.data.emails[] | [.value, .first_name, .last_name, .position] | @tsv'
內容解密:
| @tsv:這個部分將前面的結果轉換為 TSV 格式。@tsv:表示使用分隔符號格式。
命名規則與 DNS 列舉
在測試過程中,常常需要根據電子郵件地址推測使用者名稱或其他相關資訊。以下是一些可能的命名規則:
first.lastf.lastfirst_last
假設我們想要將名字和姓氏格式化為「首字母.姓氏」的形式,可以使用以下命令:
cat employees.txt | jq -r '.data.emails[] | [.value, .first_name, .last_name, .position] | @tsv' | awk '{print tolower(substr($2,1,1) "." $3)}'
內容解密:
{print tolower(substr($2,1,1) "." $3)}:這個部分將第二欄(名字)的第一個字母轉換為小寫並加上點號(.)和第三欄(姓氏)。$2: 第二欄即為名字。$3: 第三欄即為姓氏。tolower(): 轉換為小寫。
DNS 列舉
在測試過程中,DNS 列舉是一個重要的技巧。DNS 列舉可以幫助我們發現目標系統中的其他資產。以下是一些基本概念:
- 頂級域(Top-Level Domain, TLD):例如
.com,.org,.net等。 - 根域(Root Domain):例如在
www.example.com中,根域是example.com. - 子域(Subdomain):例如在
www.example.com中,子域是www.
DNS 列舉範例
假設我們有一個已知根域 (example.com) ,我們可以使用 Bash 和其他工具來發現相關子域或其他根域。以下是一些常見的 DNS 列舉工具和命令:
- dig:用來查詢 DNS 資訊。
- nslookup:用來查詢 DNS 名稱伺服器。
- host:用來查詢主機名稱。
例如,使用 dig 查詢所有 A 資料記錄:
dig example.com AXFR
內容解密:
dig example.com AXFR: 查詢所有 A 資料記錄,幫助我們發現相關子域或其他根域。
DNS 擴充與資產探查
擴充套件探查範圍使用 Bash
這一部分專注於從公司的網域名稱開始,探查暴露在網際網路上的相關資產。許多公司使用 Microsoft 365。如果一家公司註冊為 Microsoft 租戶並啟用了 Microsoft Defender for Identity(MDI),這段指令碼可以發現租戶名稱並列舉同一租戶下的所有網域名稱。這是從簡單的網域名稱開始,發現相關網域名稱的有效方法。
這段指令碼需要一個網域名稱作為輸入。你可以在本章的 GitHub 資料夾中找到它,命名為 ch08_check_mdi.sh。我將把程式碼分成幾個小部分來逐步解釋每個部分。將指令碼開啟在電腦螢幕上與以下敘述比較會很有幫助。
#!/usr/bin/env bash
get_domains() {
# 從第一個命令列引數中建立域變數
domain=$1
# 建立 HTTP 要求的 XML 主體
body="<?xml version=\"1.0\" encoding=\"utf-8\"?>
<soap:Envelope xmlns:exm=\"http://schemas.microsoft.com/exchange/services/2006/messages\"
xmlns:ext=\"http://schemas.microsoft.com/exchange/services/2006/types\"
xmlns:a=\"http://www.w3.org/2005/08/addressing\"
xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">
<soap:Header>
<a:RequestedServerVersion>Exchange2010</a:RequestedServerVersion>
<a:MessageID>urn:uuid:6389558d-9e05-465e-ade9-aae14c4bcd10</a:MessageID>
<a:Action soap:mustUnderstand=\"1\">http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation</a:Action>
<a:To soap:mustUnderstand=\"1\">https://autodiscover.byfcxu-dom.extest.microsoft.com/autodiscover/autodiscover.svc</a:To>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
</soap:Header>
<soap:Body>
<GetFederationInformationRequestMessage xmlns=\"http://schemas.microsoft.com/exchange/2010/Autodiscover\">
<Request>
<Domain>${domain}</Domain>
</Request>
</GetFederationInformationRequestMessage>
</soap:Body>
</soap:Envelope>"
內容解密:
在上述程式碼中,我們首先使用熟悉的 shebang 行,接著是 get_domains 函式的開頭部分。這裡,我們從第一個命令列引數建立了一個域變數。接著,我們建立了 HTTP 要求的 XML 主體。這個主體包含了一個 SOAP 請求,其中包含了輸入的網域名稱 $1。
# 執行 HTTP POST 要求並儲存回應
response=$(curl -s -X POST -H "Content-type: text/xml; charset=utf-8" -H "User-agent: AutodiscoverClient" -d "$body" "https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc")
內容解密:
在這段程式碼中,我們使用 curl 執行 HTTP POST 要求,並將回應儲存在 response 變數中。body 變數包含了 SOAP 請求主體,並被擴充套件到 POST 資料中。這個請求被傳送到 Microsoft 365 的 Autodiscover 服務。
# 檢查回應是否為空
if [[ -z "$response" ]]; then
echo "[-] 無法執行要求。錯誤的網域名稱?"
exit 1
fi
內容解密:
這段程式碼檢查回應是否為空(-z 表示零長度),如果是則離開並顯示錯誤訊息。非零的離開碼表示程式因錯誤而終止。
# 解析 XML 回應以提取網域名稱並儲存結果
domains=$(echo "$response" | xmllint --xpath '//*[local-name()="Domain"]/text()' -)
# 檢查是否找到任何網域名稱
if [[ -z "$domains" ]]; then
echo "[-] 未找到任何網域名稱。"
exit 1
fi
# 輸出找到的網域名稱
echo -e "\n[+] 已找到網域名稱:"
echo "$domains" | tr ' ' '\n'
內容解密:
這段程式碼使用 xmllint 工具解析 XML 回應以提取網域名稱,並將結果儲存在 domains 變數中。如果未找到任何網域名稱,則離開並顯示錯誤訊息。最後,我們輸出找到的所有網域名稱,使用 tr 命令將空格替換為換行符。
# 提取租戶名稱
tenant=$(echo "$domains" | tr ' ' '\n' | grep "onmicrosoft.com" | head -n 1 | cut -d'.' -f1)
# 檢查是否找到租戶名稱
if [[ -z "$tenant" ]]; then
echo "[-] 未找到任何租戶。"
exit 1
fi
# 輸出找到的租戶名稱
echo -e "\n[+] 已找到租戶:\n${tenant}"
內容解密:
這段程式碼提取租戶名稱。首先,將 domains 變數中的空格替換為換行符,然後使用 grep 查詢包含 onmicrosoft.com 的行。使用 head -n 1 選取第一行資料,並使用 cut 命令在句點字元處分割資料,選取第一個欄位作為租戶名稱。
check_mdi "$tenant"
}
內容解密:
最後,我們呼叫 check_mdi 函式,並傳遞租戶名稱作為引數。
check_mdi() {
# 增加 MDI 域字尾至租戶名稱
tenant="$1.atp.azure.com"
# 檢查 MDI 執行個體是否存在於租戶域中
if dig "$tenant" +short; then
echo -e "\n[+] 已為 ${tenant} 找到 MDI 執行個體!\n"
else
echo -e "\n[-] 未為 ${tenant} 找到 MDI 執行個體\n"
fi
}
內容解密:
這段程式碼增加 MDI 域字尾至租戶名稱,然後使用 dig 命令檢查 MDI 執行個體是否存在於該租戶域中。如果找到了 MDI 執行個體,則顯示正面訊息;否則顯示負面訊息。
# 檢查命令列引數是否正確提供且第一個引數是否為 -d
if [[ $# -ne 2 || $1 != "-d" ]]; then
echo "Usage: $0 -d <domain>"
exit 1
fi
# 陳述式使用者輸入提供的引數設定 domain 型別變數。
domain=$2
# 呼叫 get_domains 函式提供的 domain。
get_domains "$domain"
內容解密:
這段程式碼檢查命令列引數是否正確提供且第一個引數是否為 -d。如果不正確則顯示使用方式並離開。然後將使用者輸入提供的引數設定給 domain 型別變數並呼叫 get_domains 函式傳遞該 domain。
自動化子域列舉使用 Bash
接下來玄貓將分享一些玄貓儲存在 .bashrc 檔案中的 Bash 函式。玄貓在外部測試時會使用這些函式來快速執行常見的偵察任務,並在埠和漏洞掃描之前進行操作。
mdi 函式
第一個函式命名為 mdi,我們已經在之前看到過它(ch08_check_mdi.sh)。我們只會包括從 ch08_check_mdi.sh 中更改過的一部分內容。範例程式碼可以在本章 GitHub 資料夾中的 ch08_mdi_function.sh 檔案中找到:
mdi() {
# This function takes a domain as input and checks MDI and returns domains using the same tenant.
while IFS= read -r line; do
body="<?xml version=\"1.0\" encoding=\"utf-8\"?>
內容解密:
我們首先宣告了一個命名為 mdi 的函式,然後將之前的所有內容放置於一個 while 裡面迴圈內部(while loop)來讀取標準輸入(stdin)。這是必需的一步以便讀取管道傳入資料(piped input),使我們能夠在函式之間進行資料管道(pipe data)。IFS= 指令保留換行符號(newline),當你輸入多行資料時會很有用。
rootdomain 函式
第二個函式命名為 rootdomain。此函式接受子域作為輸入並傳回根域。例如:如果你提供 www.example.com 作為輸入,輸出會是 example.com 。此函式用於從子域取得根域資料以便進一步傳遞給其他函式以尋找更多子域資料。
rootdomain() {
# This function takes a subdomain as input and returns the root domain.
while IFS= read -r line; do
echo "$line" | awk -F. '
內容解密:
這裡我們首先宣告了一個 rootdomain 函式並在其後方附註說明此函式目的、輸入與輸出內容說明。 接著while loop 陳述式開始一行一行讀取資料流(IFS= 設定內部欄位分隔符號「IFS」無)即保留前導及尾隨空白。 再來read -r 陳述式一行一行讀取標準輸入進入 line 型別變數。
利用 Awk 和指定分隔符號 . 在以上範例中即可達成從子網站 www.example.com 提取根網站 example.com 的需求。
```plantuml
@startuml
:www.example.com; --> :example;
:B; --> :com;
@enduml
此圖示說明瞭 rootdomain 函式如何運作:
從左至右看去由左向右:第A節點為原始輸入 www.example.com, A節點藉由 Awk 與 . 分隔符號進行分割後得到 B節點與 C節點, B節點代表根網站 example, C節點代表網站型別 com.
### 自動化子域列舉使用 Bash結束