來到工具介紹的第三天,就稽核的實務工作中,市面上雖然有眾多現成掃描器與服務,但面對組織內部特殊需求、IoT 裝置等的非典型行為、以及稽核流程中的合規與可追溯性要求,自製工具能提供更高的彈性、可控性與可驗證性。本系列自製工具旨在填補標準工具與實際稽核需求之間的鴻溝,幫助稽核人員在現地勘查、證據蒐集與弱點驗證時,擁有一組輕量、可複製且容易整合的工具。
使用本系列工具時,依然秉持三個設計原則:第一是實用性,工具要能直接對應稽核檢查項目並產生可驗證的輸出;第二是可重現性,稽核結果需可被重複驗證與追溯;第三是守法與倫理,工具設計與使用流程應尊重授權範圍,避免未經授權的掃描或資料擷取,並在章節中明確說明合規與授權注意事項。
用途(Purpose)
產生要掃描 / 偵測的 IP 清單(單一 IP、IP 範圍、CIDR 列表或隨機化的目標集合),作為後續掃描(port scan、HTTP 掃描、SNMP 等)的輸入。
常見做法 / 範例
192.168.10.0/24
→ 列出 .1
~.254
風險/注意事項
避免將敏感或未被授權的外部 IP 列入掃描;清楚記錄授權範圍與白名單/黑名單條目。
下面是一個簡單的 Bash shell 腳本範例,能夠根據你輸入的 Class C 網路位址(預設子網路掩碼 255.255.255.0,CIDR /24)產生一個 TXT 檔案,裡面列出該網路內所有可用的 IP(從 1 到 254)。
你可以把腳本存成 generate_class_c.sh
,然後執行:
chmod +x generate_class_c.sh # 先把腳本設成可執行
./generate_class_c.sh 192.168.1.0 output.txt
執行後,output.txt
就會長成:
192.168.1.1
192.168.1.2
...
192.168.1.254
腳本:generate_class_c.sh
#!/usr/bin/env bash
# -------------------------------------------------------------
# generate_class_c.sh
# 用法: ./generate_class_c.sh <network_ip> <output_file>
# 例子: ./generate_class_c.sh 192.168.1.0 ips.txt
# 根據 <network_ip> 產生 Class C 範圍 (192.168.1.0/24),所有可用 IP(1 ~ 254)寫入 <output_file>
# -------------------------------------------------------------
# 1. 參數檢查
if [[ $# -ne 2 ]]; then
echo "用法: $0 <network_ip> <output_file>"
echo "範例: $0 192.168.1.0 ips.txt"
exit 1
fi
NETWORK_IP="$1"
OUTPUT_FILE="$2"
# 2. 驗證 IP 格式,檢查是否為四段式 IPv4
if ! [[ "$NETWORK_IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
echo "錯誤: 不是合法的 IPv4 位址: $NETWORK_IP"
exit 1
fi
# 每段 0-255
IFS='.' read -r o1 o2 o3 o4 <<< "$NETWORK_IP"
for octet in "$o1" "$o2" "$o3" "$o4"; do
if (( octet < 0 || octet > 255 )); then
echo "錯誤: IPv4 每段必須在 0~255 之間: $octet"
exit 1
fi
done
# 3. 取得網路位址(前 3 個 octet),保留前三段,最後一段視為 0
NETWORK_BASE="${o1}.${o2}.${o3}.0"
# 4. 產生 IP 列表並寫入檔案
echo "generate Class C 網路 ($NETWORK_BASE/24) 的 IP 列表,寫入 $OUTPUT_FILE ..."
# 使用 seq 產生 1~254
for host in $(seq 1 254); do
printf "%s.%d\n" "${NETWORK_BASE%.*}" "$host" >> "$OUTPUT_FILE"
done
echo "Finish..."
exit 0
用途
將目標 Domain 解析成對應的 IP(可含 A/AAAA/CNAME/多個 IP),用於建立實際掃描目標、辨識負載平衡/CDN、或發現舊有子域名指向。
常見做法 / 範例指令
dig
/nslookup
:dig +short example.com A
或 dig example.com ANY
domain, ip, timestamp
格式風險/注意事項
CDN / Anycast 可能導致多個不同的 IP;若要對後端主機做攻擊性檢測,需先確認授權(不要在未授權情況下對 CDN 節點做攻擊性測試)。
下面是一個 Bash 工具,利用 dig
(DNS 解析工具)把已知的域名轉成 IP。
腳本支援:
A
記錄(IPv4)前置條件:
dig
必須已安裝(大多數 Linux / macOS 都內建)腳本:domain_to_ip.sh
#!/usr/bin/env bash
# ------------------------------------------------------------
# domain_to_ip.sh
#
# 用法:
# ./domain_to_ip.sh domain1.com domain2.org ...
# ./domain_to_ip.sh -f domains.txt
# - -f <file> 每行一個域名的文字檔作為輸入
# - -o <file> 結果寫入 <file>(預設輸出到標準輸出)
# - -t <type> DNS 記錄類型,預設 A(可改成 AAAA、MX、TXT 等)
# ------------------------------------------------------------
set -euo pipefail
# 參數初始值
INPUT_FILE="" # -f
OUTPUT_FILE="" # -o
DNS_TYPE="A" # -t
usage() {
cat <<EOF
用法: $0 [選項] [域名…]
選項:
-f <file> 從 <file> 讀取域名(每行一個)
-o <file> 將結果寫到 <file>(若不指定則寫到標準輸出)
-t <type> DNS 記錄類型,預設 $DNS_TYPE(可輸入 AAAA、MX、TXT 等)
-h 顯示此說明
EOF
exit 1
}
# 解析參數
while getopts ":f:o:t:h" opt; do
case $opt in
f) INPUT_FILE="$OPTARG" ;;
o) OUTPUT_FILE="$OPTARG" ;;
t) DNS_TYPE="$OPTARG" ;;
h) usage ;;
*) echo "未知選項: -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND-1))
# 檢查 dig 是否安裝
command -v dig >/dev/null 2>&1 || { echo "錯誤: 未安裝 dig" >&2; exit 1; }
# 讀取domain列表
domains=()
if [[ -n "$INPUT_FILE" ]]; then
if [[ ! -f "$INPUT_FILE" ]]; then
echo "錯誤: 檔案不存在: $INPUT_FILE" >&2
exit 1
fi
while IFS= read -r line || [[ -n "$line" ]]; do # 跳過空行與註解
[[ -z "$line" ]] && continue
[[ "$line" =~ ^# ]] && continue
domains+=("$line")
done < "$INPUT_FILE"
else # 直接從指令列參數取得
if (( $# == 0 )); then
echo "錯誤: 未提供域名" >&2
usage
fi
domains+=("$@")
fi
# 輸出檔案
if [[ -n "$OUTPUT_FILE" ]]; then
exec 1>"$OUTPUT_FILE"
echo "=== DNS 解析結果寫入到 $OUTPUT_FILE ==="
fi
# 解析
for domain in "${domains[@]}"; do # 取得 IP(多行表示多個 A 記錄)
result=$(dig +short "$domain" "$DNS_TYPE" 2>/dev/null)
if [[ -z "$result" ]]; then
echo "$domain: 無法解析($DNS_TYPE)" >&2
continue
fi
# 印出結果:域名 -> IP(若有多個 IP,逐行列印)
echo "$domain ->"
echo "$result" | sed 's/^/ /'
done
exit 0
1. 存檔並授權
chmod +x domain_to_ip.sh
2. 直接列印多個域名
./domain_to_ip.sh google.com github.com example.org
結果類似:
google.com ->
142.250.65.78
github.com ->
140.82.114.4
example.org -> 無法解析(A)
3. 使用文字檔批次處理
domains.txt
(每行一個域名):
google.com
github.com
example.org
./domain_to_ip.sh -f domains.txt
4. 寫入檔案並指定 AAAA 記錄
./domain_to_ip.sh -f domains.txt -o results.txt -t AAAA
results.txt
會被覆寫為:
=== DNS 解析結果寫入到 results.txt ===
google.com ->
2607:f8b0:4005:80b::200e
github.com ->
2606:50c0:8000:10d::2003
example.org -> 無法解析(AAAA)
用途
對 HTTP(S) 服務進行高效能的可用性、回應碼、標頭、標題、內容字串、重導向、TLS 與主機資訊偵測;常作為目標存活性與資訊收集的第一線工具。
常見做法 / 範例命令(依照授權範圍使用)
httpx -l targets.txt -status-code -tech-detect -title -threads 50
httpx -l domains.txt -status-code | grep "200"
風險/注意事項
高併發或短時間大量請求可能造成目標服務不穩;必要時與目標協調低流量掃描或在離峰時段執行。
下面是一個 Python 3 腳本,使用 httpx(同步模式)來:
http://<IP>/
的 GET 請求(預設 80 端口)<title>
內容(若存在)前置條件
pip install httpx beautifulsoup4
執行方式
python check_http.py ips.txt > results.txt
ips.txt
:每行一個 IP,空行與 #
開頭的行會被忽略。results.txt
:輸出檔案結果
192.168.1.1 | 200 | My Web Page
10.0.0.5 | timeout
172.16.0.2 | 404 | Not Found
腳本:check_http.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
check_http.py,使用 httpx 逐一檢查 IP 是否提供 80 端口 HTTP
服務,並取得網站 title。
說明:
- 讀取「IP 清單文字檔」(每行一個 IP,空行/註解行會被忽略)
- 針對每個 IP 送出 http://<IP>/ 的 GET 請求
- 若成功 (status code 2xx/3xx) 取得 `<title>`,否則紀錄錯誤訊息
- 支援 `--concurrency N` 以加速多筆請求 (預設 N=10)
- 輸出格式: <IP> | <status or error> | <title or empty>
"""
import argparse
import re
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
try:
import httpx
except ImportError:
sys.exit("需安裝 httpx: pip install httpx")
try:
from bs4 import BeautifulSoup
except ImportError:
sys.exit("需安裝 beautifulsoup4: pip install beautifulsoup4")
# --------------------------- 常量 --------------------------- #
DEFAULT_TIMEOUT = 5.0 # 每個請求的逾時時間(秒)
DEFAULT_CONCURRENCY = 10 # 同時發送的請求數
# --------------------------- 工具 --------------------------- #
def read_ip_list(path: str):
"""
讀取 IP 清單檔案,回傳一個 list[str]。
空行與以 '#' 開頭的行將被忽略。
"""
ips = []
with open(path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
# 只保留 IP 形式,若包含冒號或其他不合法字元則忽略
if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', line):
ips.append(line)
else:
print(f"忽略非 IP 格式: {line}", file=sys.stderr)
return ips
def fetch_title(ip: str, client: httpx.Client, timeout):
"""
對 IP 送 GET 請求,返回 (status_code_or_error, title_or_empty)
"""
url = f"http://{ip}/"
try:
resp = client.get(url, timeout = timeout, follow_redirects=True)
status = resp.status_code
if 200 <= status < 400:
# 解析 HTML 取得 title
soup = BeautifulSoup(resp.text, 'html.parser')
title_tag = soup.title
if title_tag and title_tag.string:
title = title_tag.string.strip()
else:
title = ''
return status, title
else:
return status, ''
except httpx.RequestError as exc: # 連線失敗 / 逾時等
return f"error: {exc.__class__.__name__}", ''
except Exception as exc:
return f"error: {exc}", ''
def main():
parser = argparse.ArgumentParser(
description="檢查 IP 80 port 是否提供 HTTP 服務並取得網站 title",
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("ip_file", help="IP 清單文字檔案路徑")
parser.add_argument("--concurrency", "-c", type=int, default=DEFAULT_CONCURRENCY,
help="同時執行的請求數")
parser.add_argument("--timeout", "-t", type=float, default=DEFAULT_TIMEOUT,
help="單次請求逾時時間(秒)")
args = parser.parse_args()
ips = read_ip_list(args.ip_file)
if not ips:
sys.exit("沒有有效 IP")
print(f"檢查 {len(ips)} 個 IP({args.concurrency} 個併發)…")
# 重新設置全域逾時
results = []
# httpx 支援多執行緒(同步模式)不需要 async
with httpx.Client(http2=False, verify=False) as client:
with ThreadPoolExecutor(max_workers=args.concurrency) as executor:
# 建立 Future 對應
future_to_ip = {executor.submit(fetch_title, ip, client,
args.timeout): ip for ip in ips}
for future in as_completed(future_to_ip):
ip = future_to_ip[future]
try:
status, title = future.result()
except Exception as exc:
status, title = f"error: {exc}", ''
results.append((ip, status, title))
# 輸出
for ip, status, title in results:
print(f"{ip:<15} | {status:<8} | {title}")
if __name__ == "__main__":
main()
用途
讀取 SNMP(簡單網路管理協定)公開的資訊:系統描述、介面資訊、路由表、已安裝模組、甚至是明文社群字串(若為 v1/v2c)的公開設定。IoT 裝置經常忘記更改預設社群字串,使其成為重要監測目標。
常見做法 / 範例命令
snmpwalk -v2c -c public 192.168.1.100
snmpget -v2c -c public 192.168.1.100 SNMPv2-MIB::sysDescr.0
風險/注意事項
對有寫入權限的 SNMP 操作可造成破壞(例如改變設定),因此只執行讀取;如需寫入測試,必須有明確書面授權與 SOP。
前置條件
sudo apt-get install snmp # Debian/Ubuntu
brew install net-snmp # macOS
使用方式
# 輸入檔為每行一個 IP(空行/# 開頭的行會被忽略)
./check_snmp.sh -f ip_list.txt -o snmp_results.txt
-f
來源檔案,-o
輸出檔案(若不指定則寫到標準輸出)。其它可自訂的參數:
-c <community>
– community string(預設 public
)-v <version>
– SNMP 版本(預設 2c
)-t <timeout>
– SNMP 命令逾時(秒,預設 5)-r <retries>
– 重試次數(預設 1)-i <interval>
– 重試之間的間隔(秒,預設 0)輸出範例(寫入 snmp_results.txt
)
192.168.1.10 | success | SysDescr: Linux host 5.4.0-26-generic
10.0.0.5 | timeout |
172.16.0.2 | no-response |
腳本:check_snmp.sh
#!/usr/bin/env bash
# -------------------------------------------------------------
# check_snmp.sh
# 對 IP 清單發送 SNMPv2‑c public community string,只用一個 OID (sysDescr.0) 來檢查 SNMP 是否正常回應。
# 用法:
# ./check_snmp.sh [-f <file>] [-o <outfile>] [-c <community>]
# [-v <version>] [-t <timeout>] [-r <retries>]
# [-i <interval>] [<ip1> <ip2> ...]
# 例子:
# ./check_snmp.sh -f ip_list.txt -o results.txt
# -------------------------------------------------------------
set -euo pipefail
# --------- 預設參數 ----------
COMMUNITY="public"
VERSION="2c"
TIMEOUT=5 # 秒
RETRIES=1
INTERVAL=0 # 秒
IP_LIST=() # 從參數或檔案取得
OUTPUT_FILE="" # 若空則寫到 stdout
# --------- 輔助函式 ----------
usage() {
cat << EOF
用法: $0 [選項] [IP…]
選項:
-f <file> 從檔案讀取 IP(每行一個,空行/# 開頭行忽略)
-o <file> 將結果寫到 <file>(預設寫到標準輸出)
-c <community> community string(預設 ${COMMUNITY})
-v <version> SNMP 版本(2c / 3,預設 ${VERSION})
-t <timeout> SNMP 命令逾時(秒,預設 ${TIMEOUT})
-r <retries> SNMP 重試次數(預設 ${RETRIES})
-i <interval> 重試之間的秒數(預設 ${INTERVAL})
-h 顯示此說明
EOF
exit 1
}
# --------- 解析參數 ----------
while getopts ":f:o:c:v:t:r:i:h" opt; do
case $opt in
f) FILE="$OPTARG" ;;
o) OUTPUT_FILE="$OPTARG" ;;
c) COMMUNITY="$OPTARG" ;;
v) VERSION="$OPTARG" ;;
t) TIMEOUT="$OPTARG" ;;
r) RETRIES="$OPTARG" ;;
i) INTERVAL="$OPTARG" ;;
h) usage ;;
*) echo "未知選項: -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND-1))
# 參數指定 IP
if (( $# > 0 )); then
IP_LIST+=("$@")
fi
# 從檔案讀取
if [[ -n "${FILE:-}" ]]; then
if [[ ! -f "$FILE" ]]; then
echo "錯誤: 檔案不存在: $FILE" >&2
exit 1
fi
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}" # 去除註解
line="${line##*( )}" # 去除前導空白
line="${line%%*( )}" # 去除尾部空白
[[ -z "$line" ]] && continue
IP_LIST+=("$line")
done < "$FILE"
fi
if (( ${#IP_LIST[@]} == 0 )); then
echo "錯誤: 沒有有效的 IP 可測試。" >&2
usage
fi
# --------- 確認 snmpwalk 可用 ----------
if ! command -v snmpwalk >/dev/null 2>&1; then
echo "錯誤: 未安裝 snmpwalk (net-snmp)" >&2
exit 1
fi
# --------- 產生輸出 ----------
if [[ -n "$OUTPUT_FILE" ]]; then
: > "$OUTPUT_FILE" # 清空 / 建立檔案
exec 1>"$OUTPUT_FILE"
fi
echo "正在測試 ${#IP_LIST[@]} 個 IP (SNMPv${VERSION})…"
# --------- 逐個 IP ----------
for ip in "${IP_LIST[@]}"; do
# 只拿一個 OID (sysDescr.0) 來檢查 SNMP 是否正常
OID=".1.3.6.1.2.1.1.1.0"
# 執行 snmpwalk,捕捉 exit code
if snmpwalk -v"$VERSION" -c"$COMMUNITY" -t"$TIMEOUT" -r"$RETRIES" -u"$INTERVAL" "$ip" "$OID" >/dev/null 2>&1; then
# 取得回傳字串作為備註
RESPONSE=$(snmpwalk -v"$VERSION" -c"$COMMUNITY" -t"$TIMEOUT" -r"$RETRIES" -u"$INTERVAL" "$ip" "$OID" 2>/dev/null | tr
-d '\n')
echo "$ip | success | $RESPONSE"
else
# 透過 snmpwalk 的訊息判斷是 timeout / no-response
if snmpwalk -v"$VERSION" -c"$COMMUNITY" -t"$TIMEOUT" -r"$RETRIES" -u"$INTERVAL" "$ip" "$OID" 2>&1 | grep -q "Timeout";
then
echo "$ip | timeout |"
else
echo "$ip | no-response |"
fi
fi
done
echo "Test Finish..."
用途
對單頁應用(Single-Page Application, SPA)進行前端資源與 API 端點偵查:搜索 JavaScript 檔案、隱藏 API 路徑、靜態資源、前端路由、敏感字串(key/URL/註解)與認證邏輯弱點。
常見做法 / 範例步驟
network
面板或使用 wget/curl
追溯)。fetch
/axios
/XMLHttpRequest
相關字串與 API path。風險/注意事項
SPA 探查偏向資訊蒐集,若發現 API 必須再驗證其授權邏輯;不應透過前端漏洞執行未授權的行為。
用途
檢查 FTP(S) 服務的可用性、匿名登入允許性、預設帳密、目錄列舉、以及可讀/可寫權限;特別針對檔案伺服器或舊設備(有時仍使用 FTP)驗證配置安全性。
常見做法 / 範例指令
ftp 192.168.1.50
→ anonymous
curl --ftp-method nocwd ftp://user:pass@host/
或使用 nmap --script ftp-anon
檢測匿名登入風險/注意事項
FTP 明文傳輸容易導致認證/資料洩露;上傳功能若未控管會被用來放置惡意檔案。
前置條件
pip install tqdm
(tqdm
只為了美化進度條,可不安裝,腳本會自行降級到原始輸出)
使用方式
# 只列印結果到螢幕
python check_ftp_anonymous.py -f ip_list.txt
# 或者把結果寫進檔案
python check_ftp_anonymous.py -f ip_list.txt -o ftp_results.txt
-f
讀入 IP 清單(每行一個 IP,空行/# 開頭的行會被忽略)-o
輸出檔案(若不指定則寫到標準輸出)-p
指定 FTP 端口(預設 21)-t
每個連線逾時秒數(預設 5)-c
最大併發執行緒數(預設 20)
範例輸出
192.168.1.10 | success | Welcome to Anonymous FTP Server 10.0.0.5 | no-allow | Login failed: 530 Login incorrect. 172.16.0.2 | timeout |
腳本:check_ftp_anonymous.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
check_ftp_anonymous.py
掃描一份 IP 清單,判斷每台主機是否允許匿名 (anonymous) FTP 登入。
支援:
- 多執行緒(ThreadPoolExecutor)
- 自訂 FTP port
- 自訂連線逾時
- 將結果寫入檔案或直接輸出到標準輸出
tqdm: pip install tqdm # 進度條(可選,腳本會在無 tqdm 時退化)
"""
import argparse
import ftplib
import socket
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
# --------------------------------------------------------------------------- #
# 參數設定
# --------------------------------------------------------------------------- #
DEFAULT_PORT = 21
DEFAULT_TIMEOUT = 5.0 # 秒
DEFAULT_CONCURRENCY = 20
def read_ip_list(path: Path):
"""讀取 IP 清單,回傳 list[str]"""
ips = []
with path.open("r", encoding="utf-8") as f:
for line in f:
line = line.split("#", 1)[0].strip() # 切除註解、兩端空白
if not line:
continue
ips.append(line) # 只保留合法 IP 或域名
return ips
def test_ftp_anonymous(ip: str, port: int, timeout: float):
"""
試圖用匿名帳號連線 FTP。回傳 (status, message)
status:
success – 成功登入
no-allow – 登入失敗 (例如 530)
timeout – 連線逾時
error – 其它錯誤
"""
try:
with ftplib.FTP() as ftp:
ftp.connect(host=ip, port=port, timeout=timeout)
# 使用常見的匿名密碼格式
ftp.login(user="anonymous", passwd="anonymous@example.com")
# 取得歡迎字串,若無則留空
welcome = ftp.getwelcome() or ""
return ("success", welcome.strip())
except ftplib.error_perm as e:
# 530 之類的權限錯誤
return ("no-allow", str(e).strip())
except (socket.timeout, socket.timeout) as e:
return ("timeout", "")
except socket.error as e:
return ("error", f"socket error: {e}")
except Exception as e:
return ("error", f"unexpected error: {e}")
# 主程式
def main():
parser = argparse.ArgumentParser(
description="掃描 IP 清單,判斷是否允許匿名 FTP 登入",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-f",
"--file",
required=True,
type=Path,
help="IP 清單檔案 (每行一個 IP)",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default="",
help="結果輸出檔案 (若留空則輸出到 stdout)",
)
parser.add_argument(
"-p",
"--port",
type=int,
default=DEFAULT_PORT,
help="FTP 端口 (預設 21)",
)
parser.add_argument(
"-t",
"--timeout",
type=float,
default=DEFAULT_TIMEOUT,
help="連線逾時秒數",
)
parser.add_argument(
"-c",
"--concurrency",
type=int,
default=DEFAULT_CONCURRENCY,
help="最大併發執行緒數",
)
args = parser.parse_args()
ip_list = read_ip_list(args.file)
if not ip_list:
print("讀取到的 IP 清單為空,請確認檔案內容。", file=sys.stderr)
sys.exit(1)
# 進度條(tqdm)
try:
from tqdm import tqdm
def tqdm_wrap(iterable, **kwargs):
return tqdm(iterable, **kwargs)
progress_iter = tqdm_wrap
except ImportError: # 若無 tqdm,直接返回原始 iterator
def tqdm_wrap(iterable, **kwargs):
return iterable
progress_iter = tqdm_wrap
# 輸出處理
output_stream = sys.stdout
if args.output:
# 以覆寫方式開啟檔案
output_stream = open(args.output, "w", encoding="utf-8")
print(f"測試結果將寫入 {args.output}")
# 測試
print(f"正在測試 {len(ip_list)} 台主機 (FTP port={args.port}, timeout={args.timeout}s)...")
with ThreadPoolExecutor(max_workers=args.concurrency) as exc:
future_to_ip = {
exc.submit(test_ftp_anonymous, ip, args.port, args.timeout): ip
for ip in ip_list
}
for future in as_completed(future_to_ip):
ip = future_to_ip[future]
try:
status, msg = future.result()
except Exception as exc_e:
status, msg = ("error", f"future raised {exc_e}")
# 結果格式:IP | status | message
output_stream.write(f"{ip:<15} | {status:<9} | {msg}\n")
output_stream.flush()
if args.output:
output_stream.close()
print("完成。")
if __name__ == "__main__":
main()
用途
使用 Shodan 等互聯網資產搜尋引擎來找出公開暴露的設備、服務與端點(例如暴露的 SSH/FTP/SNMP 裝置、IoT 裝置、工業設備等),用於發現「已被公開索引」的目標與已知漏洞/暴露服務。
常見做法 / 範例
org:"Example Inc" port:161 product:netgear
(用於快速找出屬於某組織或特定服務的設備)風險/注意事項
Shodan 為公開資料來源,但不得做未經授權的入侵行為;Shodan 的資料可能有延遲或過期(需驗證)。
將一個「IP 清單」檔案送進 Shodan( https://shodan.io/ )的 API,取得每台機器的「已知資訊」(即 shodan.host(ip)
的完整 JSON),並把
結果寫成 CSV 或 JSON。
前置條件
shodan
套件:
pip install shodan
# 只顯示結果到終端
python shodan_query.py -f ip_list.txt
# 把結果寫進檔案
python shodan_query.py -f ip_list.txt -o shodan_results.csv -c 20
ip,status,bytes,data
192.168.1.10,success,152,{"ip_str":"192.168.1.10", ...}
10.0.0.5,no-allow,,{"error":"Not found"}
-o output.json
,腳本會寫入 JSON 陣列,方便處理。腳本:shodan_query.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
主要功能
* 讀取一個純文字檔,每行一個 IP (空行/以 # 為註解的行會被忽略)
* 使用 Shodan API key(透過參數 `-k` 或環境變數 `SHODAN_API_KEY`)
* 支援多執行緒(ThreadPoolExecutor)以縮短耗時
* 自訂輸出檔案(CSV/JSON)或直接寫到 stdout
* 內建錯誤處理、重試與速率限制提示
前置條件 pip install shodan tqdm
`shodan` : 官方 Shodan Python SDK
`tqdm` : 進度條(可選,無安裝時會自動退化為簡單迭代)
"""
import argparse
import json
import os
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
# ----------- 參數定義 ----------
DEFAULT_CONCURRENCY = 10
# ----------- 工具 ----------
def read_ip_list(file_path: Path): """讀取 IP 清單,每行一個 IP。# 為註解。"""
ips = []
with file_path.open("r", encoding="utf-8") as f:
for line in f:
line = line.split("#", 1)[0].strip() # 去除註解與兩端空白
if line:
ips.append(line)
return ips
def safe_str(json_obj):
"""將 dict/JSON 轉成可寫入 CSV 的字串"""
try:
return json.dumps(json_obj, separators=(",", ":"))
except Exception:
return "{}"
# ----------- main() ----------
def main():
parser = argparse.ArgumentParser(
description = "對 IP 清單逐筆查詢 Shodan,取得 host record",
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-f",
"--file",
required = True,
type = Path,
help = "IP 清單檔案 (每行一個 IP)",
)
parser.add_argument(
"-o",
"--output",
type = Path,
default = "",
help = "輸出檔案,若留空則寫入 stdout",
)
parser.add_argument(
"-k",
"--api-key",
default = os.getenv("SHODAN_API_KEY", ""),
help = "Shodan API key (可透過環境變數 SHODAN_API_KEY 取代)",
)
parser.add_argument(
"-c",
"--concurrency",
type = int,
default = DEFAULT_CONCURRENCY,
help = "併發執行緒數",
)
args = parser.parse_args()
if not args.api_key:
print("須提供 Shodan API key (使用 -k 或 SHODAN_API_KEY 環境變數)。", file = sys.stderr)
sys.exit(1)
try:
import shodan
except ImportError:
print(
"沒安裝 shodan 套件。請執行 `pip install shodan` 。",
file = sys.stderr,
)
sys.exit(1)
api = shodan.Shodan(args.api_key)
ips = read_ip_list(args.file)
if not ips:
print("讀取到的 IP 清單為空,請確認檔案內容。", file=sys.stderr)
sys.exit(1)
# ---------- 進度條 ----------
try:
from tqdm import tqdm
def wrap(iterable, **kwargs):
return tqdm(iterable, **kwargs)
pbar = wrap
except ImportError:
def wrap(iterable, **kwargs):
return iterable
pbar = wrap
# ---------- 輸出 ----------
if args.output:
out_file = args.output.open("w", encoding="utf-8", newline="")
print(f"輸出檔: {args.output}") # 寫入 CSV 標題
out_file.write("ip,status,bytes,json_data\n")
flush = out_file.flush
else:
out_file = sys.stdout
flush = out_file.flush
# ---------- 多執行緒 ----------
print(f"對 {len(ips)} 個 IP 進行 Shodan 查詢 (併發={args.concurrency})")
with ThreadPoolExecutor(max_workers=args.concurrency) as pool:
futures = {
pool.submit(api.host, ip): ip for ip in ips
}
for future in pbar(as_completed(futures), total=len(futures), unit="IP"):
ip = futures[future]
try:
result = future.result()
status = "success"
data_str = safe_str(result)
bytes_len = len(json.dumps(result, separators=(",", ":")))
except shodan.exception.APIError as e:
status = "api-error"
data_str = str(e)
bytes_len = 0
except Exception as e:
status = "error"
data_str = str(e)
bytes_len = 0
out_file.write(f"{ip},{status},{bytes_len},{data_str}\n")
flush()
if args.output:
out_file.close()
print("Finish...")
if __name__ == "__main__":
main()
用途
解析 DNS 記錄(A/AAAA/MX/TXT/CNAME/SRV 等),做子域名枚舉、反向 DNS 查詢(PTR),並檢查 DNS 設定(例如 zone transfer、錯誤劃分、公開的 TXT(可能含機密)等)。
常見做法 / 範例命令
dig @8.8.8.8 example.com MX +short
dig AXFR example.com @ns1.example.com
(若允許則為重大資訊洩露)dig -x 203.0.113.5 +short
風險/注意事項
DNS zone transfer 外洩會把整個 DNS 資訊(含內部主機)曝露;公開的 TXT 可能含 API key/驗證資訊。
前置條件
pip install dnspython tqdm
目的
主要參數說明
參數 | 作用 | 預設 |
---|---|---|
-f <file> |
IP 清單檔案 | 必填 |
-o <file> |
輸出檔案 | 空 → 標準輸出 |
-d <domain> |
測試用的域名(ANY/NS/AXFR) | example.com |
-t <type> |
DNS 解析類型 | A |
-c <threads> |
併發執行緒 | 20 |
--json |
輸出為 JSON | 否(預設 CSV) |
腳本:dns_resolver_scan.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
用 dnspython 逐筆對 IP 列表進行 DNS Resolver 測試,判斷
是否為「open resolver」以及是否具備 DNS 放大潛能。
支援 CSV / JSON 輸出,並可自訂測試域名與查詢類型。
# 只顯示結果
python dns_resolver_scan.py -f ip_list.txt
# 產生 CSV
python dns_resolver_scan.py -f ip_list.txt -o results.csv
# 產生 JSON
python dns_resolver_scan.py -f ip_list.txt -o results.json --json
"""
import argparse
import json
import os
import sys
import socket
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import dns.message
import dns.query
import dns.resolver
import dns.rdatatype
# 參數
DEFAULT_CONCURRENCY = 20
DEFAULT_DOMAIN = "example.com"
DEFAULT_QUERY_TYPE = "A"
DNS_PORT = 53
# 讀取 IP 列表
def read_ip_list(file_path: Path):
ips = []
with file_path.open("r", encoding="utf-8") as f:
for line in f:
line = line.split("#", 1)[0].strip()
if line:
ips.append(line)
return ips
def check_dns_resolver(ip: str, domain: str, query_type: str): # DNS 測試
"""
1. 先嘗試遞迴查詢 A/NS/ANY
2. 判斷是否允許遞迴
3. 如果允許,測試回應大小是否 >512 bytes(UDP 內部上限)
"""
result = {
"ip": ip,
"status": "unknown",
"recursion": False,
"response_bytes": 0,
"large_response": False,
"error": None,
}
# ---------- 準備查詢 ----------
try:
rdtype = dns.rdatatype.from_text(query_type.upper())
except Exception:
rdtype = dns.rdatatype.A # fallback
query = dns.message.make_query(domain, rdtype, use_edns=True)
# ---------- UDP ----------
try:
start = time.time()
resp = dns.query.udp(query, ip, port = DNS_PORT, timeout=5)
elapsed = time.time() - start
except socket.timeout:
result["error"] = f"UDP timeout ({elapsed:.2f}s)"
result["status"] = "timeout"
return result
except socket.gaierror as e:
result["error"] = f"socket.gaierror: {e}"
result["status"] = "error"
return result
except Exception as e:
result["error"] = f"query error: {e}"
result["status"] = "error"
return result
# ---------- 解析回應 ----------
try:
result["recursion"] = bool(resp.flags & dns.flags.RA) # Recursion Available
result["response_bytes"] = len(resp.to_wire())
result["large_response"] = result["response_bytes"] > 512
if result["recursion"] and result["large_response"]:
result["status"] = "open+amplifier"
elif result["recursion"]:
result["status"] = "open resolver"
else:
result["status"] = "restricted"
except Exception as e:
result["error"] = f"parse error: {e}"
result["status"] = "error"
return result
# ================ main ====================
def main():
parser = argparse.ArgumentParser(
description = "逐筆檢查 IP 是否為 Open DNS Resolver",
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-f",
"--file",
required = True,
type = Path,
help = "IP 清單檔案 (每行一個 IP)",
)
parser.add_argument(
"-o",
"--output",
type = Path,
default = "",
help = "輸出檔案 (CSV 或 JSON,若留空則寫入 stdout)",
)
parser.add_argument(
"-d",
"--domain",
default = DEFAULT_DOMAIN,
help = "測試域名(如 example.com)",
)
parser.add_argument(
"-t",
"--type",
default = DEFAULT_QUERY_TYPE,
help = "DNS 查詢類型 (A, NS, ANY 等)",
)
parser.add_argument(
"-c",
"--concurrency",
type=int,
default = DEFAULT_CONCURRENCY,
help = "最大執行緒數",
)
parser.add_argument(
"--json",
action = "store_true",
help = "輸出為 JSON 格式 (預設 CSV)",
)
args = parser.parse_args()
# 讀取 IP 列表
ips = read_ip_list(args.file)
if not ips:
print("IP 清單為空,請確認檔案內容。", file = sys.stderr)
sys.exit(1)
# ---------- 進度條 ----------
try:
from tqdm import tqdm
def wrap(iterable, **kwargs):
return tqdm(iterable, **kwargs)
pbar = wrap
except ImportError:
def wrap(iterable, **kwargs):
return iterable
pbar = wrap
# ---------- 輸出 ----------
if args.output:
out_path = args.output
out_path.parent.mkdir(parents = True, exist_ok = True)
if args.json:
out_file = out_path.open("w", encoding = "utf-8")
out_file.write("[\n")
first = True
else:
out_file = out_path.open("w", encoding = "utf-8", newline = "")
# CSV header
out_file.write("ip,status,recursion,bytes,large_response,error\n")
else:
out_file = sys.stdout
flush = out_file.flush
# ---------- 執行 ----------
print(f"對 {len(ips)} 個 IP 進行 DNS 測試 (併發={args.concurrency})")
with ThreadPoolExecutor(max_workers = args.concurrency) as pool:
futures = {
pool.submit(check_dns_resolver, ip, args.domain, args.type): ip
for ip in ips
}
for future in pbar(as_completed(futures), total=len(futures), unit="IP"):
ip = futures[future]
try:
res = future.result()
except Exception as e:
res = {
"ip": ip,
"status": "error",
"error": str(e),
}
# ---------- 輸出 ----------
if args.json:
if not first:
out_file.write(",\n")
json.dump(res, out_file, ensure_ascii = False, indent = 2)
first = False
else:
# CSV
line = (
f'{res["ip"]},{res["status"]},{int(res["recursion"])}'
f',{res["response_bytes"]},{int(res["large_response"])}'
f',"{res["error"] or ""}"\n'
)
out_file.write(line)
flush()
if args.output:
if args.json:
out_file.write("\n]\n")
out_file.close()
print("Finish...")
if __name__ == "__main__":
main()
結語
今天自製工具的開發與分享,目的不是取代市面上成熟的商業產品,而是在稽核過程中補足一些「小而精」的需求。透過簡單、透明且可控的程式碼,可以更靈活地定義、調整檢查範圍、輸出格式與整合方式,讓稽核團隊在有限的時間內快速取得可信的證據。
從 IP 產生器到 HTTP、SNMP、DNS 與 FTP 的基礎掃描,再到針對 SPA 的偵測與 Shodan 外部可見性驗證,這些工具構成了一套輕量化的檢查流程。它們的價值不是在技術實作本身,更在於提醒我們:資安稽核應該兼顧效率、可追溯性與合規性。未來若能持續擴充更多模組,或將這些工具整合到自動化的管線中,能進一步提升稽核作業的精確度與一致性。