個案

後門 Lambda 刪了 15 個月,它生出來的使用者還在

為了確認一把外洩的 AWS 金鑰有沒有被用過,我們順手做了一次持久化巡檢。資料沒被碰,卻在 IAM 裡撞見 2025 年那台自動販賣機留下的使用者:權限拿掉了一半、金鑰停了,但人從來沒刪。

30 秒看懂
  • 8/27 部署平台環境變數外洩後,一把 AWS 金鑰到 10/15 還能用。它在外洩期間能讀寫整個 DynamoDB,但稽核記錄與用量指標都證明:資料沒有被讀、也沒有被改。
  • 順手做持久化巡檢時,發現 2025 年 7 月後門 Lambda 建立的 IAM 使用者還在,15 個月來一直掛著寄信全權限。當年只拿掉管理員權限、停用金鑰,沒有刪人。
  • 另外查清了一件差點被當成入侵的事:10/12 有一個外部 AWS Organization 在帳號裡建了掛 AdministratorAccess 的角色。查證後是公司委託的 AWS 經銷商(帳單代理)對旗下帳號統一推送,屬正常;但這也代表有第三方長期持有完整管理權。

本來只想確認一把金鑰,結果撞見去年的人

10 月 15 日早上,我們還在處理同一波外洩的另一把 GCP 金鑰被盜用、專案被停權的事。那件事讓整張「外洩但仍有效」的清單重新排了優先順序,其中一列是一把 AWS access key:8 月 27 日部署平台環境變數外洩時它在範圍內,到這天早上還是 Active。

問題很單純:這把金鑰在外洩的 49 天裡有沒有被用過?要回答它,得先知道它當時能做什麼,再去 CloudTrail 與 CloudWatch 找有沒有人做過。答案是沒有,這部分可以證明到「資料沒被讀走、沒被改」。但查完之後我們沒停,因為這個帳號有前科:2025 年 7 月,它的 us-east-1 裡被植入過一台會自動生管理員的後門 Lambda。既然都進來了,就順手確認那件事是不是真的結束了。

它沒有結束。後門函式早就不在,但它建出來的那個 user- 加 8 碼 hex 的 IAM 使用者還在,15 個月來一直掛著 AmazonSESFullAccess。這篇要講的就是這個落差:**我們當年清掉的是「能力」,不是「身分」;而一個沒刪掉的身分,隨時可以被重新賦予能力。**時間一律為台灣時間(UTC+8),原始記錄另標 UTC。

一把金鑰、一個帳號、兩段歷史

這個 AWS 帳號是單一帳號,主要資源在 ap-northeast-1。DynamoDB 上有 46 張表,是早年一套物聯網服務的資料層;那套服務後來改走別的資料庫,這些表就一直留著。帳號擁有者靠主控台操作,鑑識則用 CloudShell 與本機 aws-cli。

外洩的那把金鑰屬於一個 IAM 使用者,建立日期是 2025-07-16,正好是後門 Lambda 事件應變的那一天。我們推測它是當時清理完、為了讓部署平台上的服務繼續連 DynamoDB 而新建的,所以它沒有直接政策,只靠一個資料庫群組拿權限。它的金鑰從建立到今天沒換過,同時放在部署平台的環境變數和開發者本機的 .env。

原本已有的防護是上次事件之後補的那一套:有 CloudTrail trail,但只記管理事件;沒有 DynamoDB data events;沒有對 IAM 高風險事件或 Organizations 變更的告警。下表是這次盤點牽涉到的兩組憑證。

leaked credentials這次盤點到的兩組 AWS 憑證
服務憑證類型用途存在形式放在哪權限範圍狀態
AWS IAM 使用者(部署平台連 DynamoDB 用)access key(access key ID + secret access key)部署平台上的服務讀寫 DynamoDB明文環境變數;本機 .env 另有一份部署平台環境變數(8/27 外洩範圍內)+開發者本機 .env外洩期間:資料庫群組(DynamoDB 全權限);10/15 14:51 移出群組後,IAM/S3/EC2 實測皆 AccessDenied2025-07-16 建立、455 天未輪替;10/15 仍 Active,權限已拔除,金鑰本身尚待停用
AWS IAM 使用者(2025-07 後門 Lambda 建立,user- 加 8 碼 hex)access key +(曾有的)主控台密碼攻擊者以程式或主控台操作整個帳號,並用 SES 寄信後門函式回傳的 JSON 明文,不落地2025-07 當時隨 API 回應交給呼叫者建立時:AdministratorAccess + IAMFullAccess + AmazonSESFullAccess;2026-10 時只剩 AmazonSESFullAccess金鑰 Inactive、最後使用 2025-07-16(STS)、無主控台密碼;10/15 刪除金鑰、卸政策、刪除使用者
憑證本身已遮蔽或失效;這裡只說明它是哪個服務、拿來做什麼、以什麼形式存放,讓你能對照自己的環境。

第一列是這次的起點,第二列是這次的意外。兩個使用者是同一天建立的,一個是我們為了收拾殘局建的,一個是攻擊者留下的殘局本身。

10 月 15 日這一天

這條時間線沒有攻擊者在裡面,全是我們自己和帳號擁有者的動作。要看的是每一步怎麼把下一個問題帶出來。

  1. 2025-07-16 18:28
    後門 Lambda 建立 user- 加 8 碼 hex 的使用者,掛上三個高危受管政策
    IAM get-user 的 CreateDate(10:28 UTC);前案的函式程式碼。
  2. 2025-07-16
    建立部署平台用的 IAM 使用者與 access key
    IAM 使用者與金鑰的 CreateDate。
  3. 2026-08-27
    部署平台環境變數外洩,這把 access key 在範圍內
    平台通知;環境變數快照中的 key ID。
  4. 2026-10-12 12:09
    帳號所屬 Organization(公司委託的 AWS 經銷商)的管理帳號遠端啟用 Organizations 服務角色,並以 StackSets 推送掛 AdministratorAccess 的執行角色;同時段 Trusted Advisor 自動掃描
    CloudTrail 管理事件(04:09 UTC)與角色信任政策;同公司另一個帳號在同一分鐘被建立同名角色;擁有者確認經銷商委託。
  5. 2026-10-15 上午
    確認 access key 仍 Active,用金鑰本身探權限:IAM、S3、EC2 全 AccessDenied
    aws-cli 實測,記下鑑識端 IP 與時間。
  6. 2026-10-15 14:51
    帳號擁有者把這個使用者移出資料庫群組
    CloudTrail RemoveUserFromGroup。
  7. 2026-10-15 下午
    CloudTrail 90 天管理事件與 CloudWatch 容量指標交叉比對,確認外洩期間無存取
    lookup-events 兩個區域;CloudShell 用 boto3 get_metric_data 掃全部表。
  8. 2026-10-15 下午
    44 張休眠表建 on-demand backup,核對後關刪除保護並刪表;保留 2 張仍在用的
    list-backups 全部 AVAILABLE,名稱與待刪清單逐一比對。
  9. 2026-10-15 傍晚
    持久化巡檢:17 個區域 Lambda 為 0;從高危政策反查到後門使用者,刪除
    list-entities-for-policy;get-user 回 NoSuchEntity。

整天最重要的轉折在倒數第一列:我們本來在查資料有沒有被讀,最後刪掉的是一個去年就該刪的使用者。

這次沒有攻擊鏈,只有殘留物

到這裡,其實已經足夠理解整件事。如果你關心的是管理與制度問題,可以直接往〈刪掉函式不等於刪掉身分〉繼續閱讀。如果你想知道我們怎麼在沒開 data events 的情況下,證明 DynamoDB 沒被讀,下面開始進入技術鑑識。

先記住一件事就好:這次沒有找到任何攻擊者的動作。下面的攻擊鏈是拿來對照「如果有人用了這把金鑰,每一步會留下什麼」,多數格子是空的,空的理由各不相同。

發生了什麼 · 8/27 → 10/15· MITRE ATT&CK T1078.004
外洩的 access key 整段期間都有效,但 90 天管理事件裡沒有任何非我們的呼叫。持有者可能還沒試,或試了別的區域。
當時留下的訊號
CloudTrail 只有鑑識端自己的 7 筆事件。
能擋下它的控制
已知外洩的金鑰當天停用;長期金鑰設輪替期限。

七格裡只有兩格有實際內容:一格是外洩的路徑,一格是去年留下的人。其餘標成缺口的格子,都是有證據證明「沒發生」;第四格那個看起來最像提權的 Admin 角色,查證後是經銷商的正常作業。

證明沒被讀,比證明被讀難

判斷一把外洩金鑰的風險,第一步是問「它能做什麼」。我們用金鑰本身打了一輪 IAM、S3 ListBuckets、EC2,全部 AccessDenied;主控台上它既沒有直接政策,也不在任何群組。如果停在這裡,結論會是「這把金鑰沒有權限,風險很低」。

這個結論是錯的。帳號擁有者提醒:它「以前」有 DynamoDB 全權限。CloudTrail 一查,擁有者就在當天下午 14:51 把它移出了資料庫群組——我們探權限的時候,看到的是剛被拔掉之後的樣子。外洩的 49 天裡,它一直能讀寫整個 DynamoDB。

接著是第二個陷阱。主控台上這把金鑰的「最後使用」顯示兩小時前,看起來很像有人剛用過。

排除自己之後,管理事件就很乾淨了。

但管理事件只記「對表做了什麼」,不記「從表讀了什麼」。GetItem、Query、Scan、PutItem 屬於 data events,這個 trail 沒開。

那個替代證據是 CloudWatch 的容量指標。DynamoDB 的每一次讀都會消耗讀取容量(RCU),Scan 更是整張表的量;就算表是 on-demand 模式,ConsumedReadCapacityUnits 也照樣記。只要容量是 0,就不可能有人讀過。

深入閱讀在 CloudShell 一次掃完所有表的容量指標boto3 get_metric_data,一次最多 500 條 query
CloudShell · 46 張表 08-01 至今的讀寫容量READ-ONLY
import boto3, datetime as dt
ddb = boto3.client("dynamodb", region_name="ap-northeast-1")
cw  = boto3.client("cloudwatch", region_name="ap-northeast-1")
tables = [t for p in ddb.get_paginator("list_tables").paginate() for t in p["TableNames"]]

qs = []
for i, t in enumerate(tables):
  for j, m in enumerate(["ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits"]):
      qs.append({"Id": f"q{i}_{j}", "Label": f"{t}|{m}",
                 "MetricStat": {"Metric": {"Namespace": "AWS/DynamoDB", "MetricName": m,
                                "Dimensions": [{"Name": "TableName", "Value": t}]},
                                "Period": 86400, "Stat": "Sum"}})

res = cw.get_metric_data(MetricDataQueries=qs,
      StartTime=dt.datetime(2026, 8, 1), EndTime=dt.datetime.utcnow())
for r in res["MetricDataResults"]:
  print(r["Label"], sum(r["Values"]))

# 輸出:每張表的讀、寫總和都是 0.0
# Scan、DeleteItem 用 SuccessfulRequestLatency 的 SampleCount 加 Operation 維度另查,同樣為 0

同一招查 ReturnedItemCount 與 SuccessfulRequestLatency(依 Operation 分組),可以分辨 Scan、Query、GetItem 各自的次數。注意 CloudWatch 指標的保留期依解析度遞減,日粒度的 Sum 可以回看一年以上,近兩週的細粒度資料則最完整。

證明完資料沒事,我們回頭看這個帳號本身。2025 年那台後門 Lambda 的程式碼很清楚:建一個 user- 加 8 碼 hex 的使用者,掛 AdministratorAccess、IAMFullAccess、AmazonSESFullAccess。17 個區域的 Lambda 都是 0,函式確實不在了。可是它的產物呢?

第一次用迴圈列使用者時,我們沒看到它。不是它不在,是迴圈印出的行數超過終端機一頁,前面幾行捲上去了,我們只看了後半段。改用「從政策反查」才抓到。

從高危政策反查掛載對象READ-ONLY
$ aws iam list-entities-for-policy \
  --policy-arn arn:aws:iam::aws:policy/AmazonSESFullAccess \
  --query 'PolicyUsers[].UserName' --output text
user-xxxxxxxx                   # ← 名稱模式和 2025 年後門函式產生的一模一樣

$ aws iam get-user --user-name user-xxxxxxxx --query 'User.CreateDate'
"2025-07-16T10:28:...Z"

最後使用的服務是 STS,推測是攻擊者拿到金鑰後先打了一次身分確認;之後這把金鑰再也沒被用過。10/15 我們依序刪除它的 access key、卸下政策,再 delete-user,get-user 回 NoSuchEntity 才算收尾。

順著反查的思路,我們也把角色過了一遍:133 個角色,大多是 2024 年 Lambda 自動建立的執行角色。

同樣標推論的,是開頭那個使用者的來歷。

最後是一件寫在這裡、但還沒有答案的事。

六條證明、兩條推論、一個缺口。資料這一邊被證明得很乾淨,身分這一邊則挖出了一個去年的後門,以及一個我們沒意識到的第三方管理權。

刪掉函式不等於刪掉身分

接下來從技術回到管理與制度。你不需要記住上面的指標名稱,只要記住一件事:2025 年我們以為清完了,其實只清了一半。

表面上的問題是一個後門使用者活過了整整一年多。往下一層是技術:當年的處置把「危險」理解成「權限」,所以拿掉了 Admin 與 IAMFull、停用了金鑰,看起來它已經什麼都做不了。但 IAM 使用者是一個容器,權限和金鑰只是裝在裡面的東西;容器在,任何一個拿到 IAM 寫入權限的人,一行 AttachUserPolicy、一行 UpdateAccessKey 就能讓它復活,而且名字和建立時間都是舊的,混在清單裡更不顯眼。SES 全權限沒被一起拿掉,則說明了當時判斷「哪些權限高危」時漏了一個——寄信信譽正是那台販賣機最想要的東西。

往上追到管理,問題出在驗收方式。前案的處置紀錄寫的是「函式、執行角色與可疑使用者都清掉了」,但沒有一張清單逐一列出後門產生的每個 entity、每個 entity 的最終狀態、用什麼指令驗證。「清掉了」是一句話,不是一個可以被檢查的結果。開頭那把 access key 也是同一個形狀:權限現在是空的,但沒有人記得它曾經有什麼,差一點就用「現在」替「外洩期間」下了結論。

最後一層是制度,這層我只能寫成推測:這個帳號裡長期堆著不再使用的東西——休眠一年半的 DynamoDB、一百多個孤兒角色、一把建立後從沒換過的金鑰——而沒有任何機制要求定期把它們拿出來看。後門使用者能活這麼久,不是因為它藏得好,是因為這個帳號從來沒有人做過「列出所有身分,每一個都要說得出主人」這件事。

訊號一直都在,只是沒有人從政策那一端看

這次沒有任何告警響過,因為沒有設任何告警。後門使用者在 2025-07-16 被建立時,CreateUser、AttachUserPolicy、CreateAccessKey 三個事件連在一起出現;10/12 經銷商用 StackSets 建出一個信任外部帳號的 Admin 角色,也是 CloudTrail 裡清清楚楚的管理事件。這次它是合法的,但我們是做巡檢時才看到,而不是事件發生的當下;下一次推送的如果不是經銷商,我們一樣不會知道。這些都是最該接 EventBridge 的事件,一個都沒接。

另一半的原因是「看清單」這個動作本身不可靠。我們這次已經是帶著「要找後門使用者」的目的去列的,還是因為終端機捲動漏看了一次;平常沒有目的地掃過一份上百行的清單,漏看幾乎是必然。從 AmazonSESFullAccess、AdministratorAccess、IAMFullAccess 這幾個政策反查掛載對象,輸出只有寥寥幾行,每一行都必須說得出為什麼在那裡,這才是能重複執行的檢查。

10 月 15 日的處置

早上的第一個動作是確認金鑰還活著,然後用它自己探權限,同時把鑑識端的 IP 與時間記下來。下午帳號擁有者把它移出資料庫群組,等於先切斷了它對資料的存取,我們再用 CloudTrail 與 CloudWatch 兩條線確認外洩期間沒有人讀寫。

確認資料沒事之後,處理的是資料本身。除了 2 張仍在用的,其餘早已休眠,留著不會帶來任何價值,只會讓下一次外洩多一份可以被讀走的東西。我們先替這些表各建一份 on-demand backup,共約 210MB,等全部狀態變成 AVAILABLE、名稱和待刪清單逐一對上,才關掉刪除保護、刪表。on-demand backup 在刪表之後仍然存在,需要時可以還原成新表,所以這一步是可逆的。

傍晚做持久化巡檢:17 個區域逐一列 Lambda,確認後門函式不在;從三個高危政策反查,找到後門使用者後刪除其 access key、卸下政策、刪除使用者,最後以 get-user 回 NoSuchEntity 收尾。10/12 的 Admin 角色則和擁有者確認是經銷商的統一推送,並用同公司另一個帳號的同名角色交叉比對;孤兒角色當天只做到列出與記錄。開頭那把 access key 的權限已經被拔光,但金鑰本身還沒停用,這是當天沒做完的事。

這次做對的,和差一點做錯的

做對的部分,先是沒有停在「資料沒事」:如果查完 DynamoDB 就收工,後門使用者會繼續活下去。用 CloudWatch 容量指標補 data events 的缺口,讓「沒被讀」從一句推測變成可以證明的結論;刪表之前先備份、逐一比對名稱,讓清理保持可逆。從政策反查而不是看清單,則是這次真正抓到後門的那一步。

差一點做錯的有兩件,而且都是被別人或被第二次檢查救回來的。第一件是權限:我們探到 AccessDenied 時已經準備寫「這把金鑰沒權限」,是帳號擁有者一句「它以前有 DynamoDB」把結論拉回來。第二件是足跡:主控台的「兩小時前使用」差點被當成入侵跡象,是對過 sourceIPAddress 才發現是自己。兩件事的共同點是:當下看到的狀態,常常是被剛才的動作改過的狀態。

該改的則是 2025 年那次清理留下的習慣:以「能力」為單位收尾,而不是以「身分」為單位驗收。還有,那把 access key 到今天還沒停用。

之後我們改了什麼

當天做完的是:資料庫群組移除、44 張休眠表備份後刪除、後門使用者刪除,並把前案文末補上一段後續,指向這一篇。接下來幾天要做的是停用並刪除那把 access key,讓部署平台上仍連 DynamoDB 的服務改用新的、只限那 2 張表的憑證;以及請經銷商書面說明那個 AdministratorAccess 執行角色的用途,能縮到帳單與唯讀就縮,必要的高權限動作改成事先通知。

一個月內要補的是告警與驗收。EventBridge 接上 CreateUser、AttachUserPolicy、CreateAccessKey、Organizations 的 AcceptHandshake、帳號加入與服務角色啟用,以及 StackSets 建立或更新角色——經銷商的推送也要觸發,合法的動作同樣該被看見;DynamoDB 開 data events,至少對仍在用的表。所有事件處置的收尾,改成一張以 entity 為單位的清單:每個被建立或被碰過的使用者、金鑰、角色、函式各一列,寫下最終狀態與驗證指令,get 回 NoSuchEntity 才能打勾。

這一季要處理的是「沒人用的東西」:一百多個孤兒角色逐一確認後刪除;每季從高危政策反查一次掛載對象;長期金鑰設最長效期,超過就停。

下面這張成本表要先講清楚:這次沒有任何能證明的直接損失。資料沒被讀、帳單沒有異常;最後一列不是損失,而是一個長期存在、這次才被看見的第三方權限。

cost這次的帳:可證明的損失是零
項目用量單價期間US$NT$舉證
外洩期間 DynamoDB 被讀寫的量
沒有讀寫,就沒有被盜用的用量
46 張表的讀寫容量皆為 0依 RCU/WCU 計價08-01 → 10-150.000CloudWatch ConsumedRead/WriteCapacityUnits 每日加總
44 張休眠表的 on-demand backup
估:依公開牌價推算,實際以帳單為準
約 210MB(0.21 GB)估 US$0.114/GB-月每月,直到刪除備份0.021list-backups 的大小加總(可證);單價為牌價推估
刪表前的休眠表儲存
估:金額很小,真正的成本是它們在外洩期間是可被讀走的資料
約 0.21 GB估 US$0.285/GB-月每月(已停止)0.062以備份大小近似表大小;單價為牌價推估
後門使用者掛著 SES 全權限的 15 個月
估:金鑰最後使用在建立當天,推論期間沒有被拿來寄信
金鑰 Inactive、無主控台密碼—2025-07 → 2026-100.000IAM access key last used;SES 寄信統計未逐月匯出
處置人力
估:動作有紀錄,工時與日費為估算
權限確認、CloudTrail/CloudWatch 鑑識、備份刪表、持久化巡檢,約 1 人日顧問日費 NT$27,500(估)10/15859.3827,500處置紀錄可證動作;工時為估算
經銷商持有的完整管理權限(風險,非損失)
合法委託,不列入損失;經銷商服務費依合約,不在本表
一個掛 AdministratorAccess、信任經銷商管理帳號的 StackSets 執行角色—加入組織起(早於 90 天前)——CloudTrail 10/12 04:09 UTC 事件;同公司另一帳號同名角色;擁有者確認委託關係
合計(僅可證明列)0.000
匯率假設 1 USD ≈ NT$32。單價為公開牌價或帳單實付,來源:DynamoDB 用量取自 CloudWatch(08-01 至 10-15);備份與儲存單價以 AWS 公開牌價推估(ap-northeast-1,on-demand backup 約 US$0.114/GB-月、標準表儲存約 US$0.285/GB-月);人力日費沿用前篇假設 NT$27,500。「舉證」欄標明每筆損失的證據來源與等級( 可證明 推論 缺口)。

推估下來,這次的直接成本不到一美元的儲存費加上一天人力。但「便宜」只是因為這次運氣好:外洩的金鑰沒人用,後門使用者的金鑰是停著的。如果任何一個條件不成立,這張表就會長得完全不一樣。

帶得走的六件事

從攻擊者的角度看,那個沒被刪掉的使用者是一份免費的保險。他當年用販賣機生出它,領走金鑰,確認身分之後就沒再用。對方清掉了函式、拿掉了 Admin、停了金鑰,他什麼都不用做;只要將來任何一次外洩讓他拿到 IAM 寫入權限,他不必建新使用者、不必觸發 CreateUser 告警,只要把舊的那個重新接上政策、啟用金鑰。而那個舊使用者,已經在 SES 全權限下安靜地等了一年多。

lessons本案的 6 個教訓
  1. 01
    清理要以 entity 為單位驗收:刪掉函式、拿掉 Admin、停用金鑰都不等於刪掉後門使用者,get-user 回 NoSuchEntity 才算結束。
  2. 02
    判斷一把外洩金鑰的曝險,要看外洩期間它有什麼權限;現在探到的 AccessDenied,可能只是幾小時前剛被拔掉。
  3. 03
    鑑識會留下自己的足跡,動手前先記下自己的 IP 與時間,否則「最後使用」會把你自己誤判成攻擊者。
  4. 04
    沒開 DynamoDB data events 時,CloudWatch 的讀寫容量指標可以證明「有沒有被讀寫」;它答不出是誰,但足以排除資料外洩。
  5. 05
    休眠的表、孤兒角色、沒人用的金鑰都是攻擊面;先備份再刪,讓清理可逆,也讓下一次外洩少一份可以被拿走的東西。
  6. 06
    經銷商透過 Organizations/StackSets 推送的 Admin 角色是合法的,但等於第三方長期持有完整管理權;要求說明用途、縮到最小權限,並對這類事件設告警。

如果只想記十個詞,就記下面這十個;順序是照「讀完最該帶走」排的。

keywords重點學習:最需要學習的 10 個關鍵字10 個
  1. 01
    list-entities-for-policy
    這次抓到後門使用者的那一步。從高危政策反查掛載對象,輸出只有幾行,比肉眼掃上百行的使用者清單可靠得多。
  2. 02
    以 entity 為單位驗收
    2025 年清掉了權限和金鑰,卻留下了使用者;收尾清單要列每個使用者、金鑰、角色與函式的最終狀態。
  3. 03
    外洩期間的權限(point-in-time)
    這把金鑰今天 AccessDenied,外洩期間卻能讀寫整個 DynamoDB;曝險要用當時的權限算。
  4. 04
    DynamoDB data events
    GetItem、Scan、PutItem 只有它會記;沒開的話,CloudTrail 永遠答不出「誰讀了哪一筆」。
  5. 05
    ConsumedReadCapacityUnits
    沒有 data events 時的替代證據:Scan 一定耗 RCU,容量為 0 就能證明沒被讀。
  6. 06
    get_metric_data
    一次查數百條指標,在 CloudShell 裡幾秒鐘掃完幾十張表,不必逐張點主控台。
  7. 07
    sourceIPAddress/userAgent
    分辨「兩小時前使用」是攻擊者還是自己的依據;鑑識前先記下自己的值。
  8. 08
    AWS Organizations/StackSets
    經銷商用它對旗下帳號統一推送 Admin 角色;合法,但等於第三方長期持有完整管理權,必須有告警,也要說得出用途。
  9. 09
    on-demand backup
    讓「刪掉休眠表」變成可逆的動作;表刪了,備份還在,需要時能還原。
  10. 10
    孤兒角色(orphaned role)
    函式刪了、執行角色還在;它不會自己作亂,但會成為下一個人現成的權限容器。

如果回到 2025 年 7 月那一天

假設回到 2025 年 7 月,後門函式剛刪掉,我會把接下來的一小時這樣用。

前十五分鐘列出這次事件產生或碰過的每一個 entity:兩支函式、它們的執行角色、API Gateway、函式建立的每一個 user- 使用者與它們的金鑰、我們自己為了應變新建的使用者。一列一個,先不動手;這張表存成 entity-closeout-c3k.csv,之後每次事件收尾都沿用同一張表頭。

第二個十五分鐘逐列處置並驗證:後門產生的使用者不是停用,是刪除——先刪金鑰、卸政策、刪登入設定,再 delete-user,get-user 回 NoSuchEntity 才打勾;為應變新建的使用者,寫下它的用途、主人與預計輪替日。最後從 AdministratorAccess、IAMFullAccess、AmazonSESFullAccess 三個政策各反查一次,確認輸出裡每一行都說得出來歷。

剩下的半小時補告警:CreateUser、AttachUserPolicy、CreateAccessKey 與 Organizations 變更接上 EventBridge。如果那一小時有做,這次就不會在 IAM 裡遇到去年的人;而那把為了應變新建的金鑰,也會有一個輪替日,而不是放到 455 天。

這一小時的檢查部分,今天就可以在自己的帳號上做一次。下面五步全部唯讀,十五分鐘內做得完。

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

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

  1. 從三個高危政策反查掛載對象,而不是列使用者清單。
    for p in AdministratorAccess IAMFullAccess AmazonSESFullAccess; do echo "== $p"; aws iam list-entities-for-policy --policy-arn arn:aws:iam::aws:policy/$p --query '[PolicyUsers[].UserName,PolicyRoles[].RoleName,PolicyGroups[].GroupName]' --output text; done
    看到什麼代表什麼:每一行都要說得出主人與用途;名稱像 user- 加隨機字串、或你沒印象的使用者,先當後門殘留處理。
  2. 列出所有使用者的建立時間,對照你曾經處理過的事件日期。
    aws iam list-users --query 'Users[].[CreateDate,UserName]' --output text | sort
    看到什麼代表什麼:建立日期落在某次事件期間、卻沒有人能說出用途的使用者,就是那次清理沒收尾的地方。
  3. 確認 trail 有沒有記 DynamoDB data events。
    aws cloudtrail get-event-selectors --trail-name TRAIL_NAME
    看到什麼代表什麼:只有 ReadWriteType: All 的管理事件、沒有 AWS::DynamoDB::Table 的 data resource,代表出事時你答不出誰讀了哪一筆。
  4. 抽一張表,看最近 30 天的讀取容量。
    aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB --metric-name ConsumedReadCapacityUnits --dimensions Name=TableName,Value=TABLE_NAME --start-time $(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ) --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) --period 86400 --statistics Sum --output text
    看到什麼代表什麼:全是 0 代表這張表已經休眠:它不帶來價值,只等著被讀走,該排進備份後刪除的清單。
  5. 確認帳號是否屬於某個 Organization、管理帳號是誰。
    aws organizations describe-organization --query 'Organization.[Id,MasterAccountId]' --output text
    看到什麼代表什麼:有回傳就代表管理帳號能對你推送角色。說得出是哪家經銷商還不夠,接著用 list-entities-for-policy 看 AdministratorAccess 底下有沒有信任它的角色,並確認對方說明過用途。
分享
精選拆解直送信箱

每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。

延伸閱讀