从选中目标到拿到特权访问不到三小时:Storm-0940 的 Agent 化攻击改变了什么

·阅读约13分钟·Evergreen Tools Team

2026 年 10 月 21 日,Microsoft 威胁情报团队公开了 Storm-0940 的行动:这是一个自 2021 年起活跃、被归因为国家背景的攻击组织,它使用名为 ARTEX 的开源 Agent 化渗透测试框架自动化攻击,目标横跨航空航天与国防、汽车、化工、能源、金融服务、政府、法律、制造、媒体、军事、半导体、软件以及智库。真正该让人停下来的是一句时间描述:在多起案例中,从选定目标到拿到云基础设施特权访问不足三小时。Microsoft 同时判断该组织对近期披露的 ConnectWise 多租户应用供应链入侵负责。这不是一个「漏洞更聪明」的故事,而是一个「侦察与撞库循环不再需要人睡觉」的故事。

从选中目标到拿到特权访问不到三小时:Storm-0940 的 Agent 化攻击

从选中目标到拿到特权访问不到三小时:Storm-0940 的

一、Microsoft 到底说了什么

这个组织的历史其实很常规:先用密码喷洒和攻击面向公网的应用拿到初始访问,再用被入侵账号横向移动,然后在云环境里通过给已有 OAuth 应用添加凭据来维持持久化。最后这一步最该记住,因为它把一个应用变成了一直有效的身份——密码改了它还在。新东西是自动化那一层。ARTEX 被描述为一个开源渗透测试框架,能让任意 AI 模型驱动攻击工作流,本身是一个用 Go 写的多 Agent LLM 系统。Storm-0940 用它做扫描侦察、密码喷洒,以及在 ConnectWise 事件的下游受害者上滥用 OAuth 应用。Microsoft 明确说明:这些活动并非源自 Microsoft 自身系统、产品或服务的入侵或漏洞。

# Inventory pass 1: applications that gained credentials recently.
# Read-only. App registration needs Directory.Read.All.
import json, urllib.request, datetime as dt

TOKEN = "<graph token from az account get-access-token --resource https://graph.microsoft.com>"
HDR = {"Authorization": "Bearer " + TOKEN}

def get(url):
    req = urllib.request.Request(url, headers=HDR)
    return json.load(urllib.request.urlopen(req, timeout=60))

cutoff = dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=30)
apps = get("https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,passwordCredentials,keyCredentials")

for a in apps["value"]:
    for cred in a.get("passwordCredentials") or []:
        start = cred.get("startDateTime")
        if not start:
            continue
        when = dt.datetime.fromisoformat(start.replace("Z", "+00:00"))
        if when >= cutoff:
            print("NEW SECRET |", a["displayName"], "|", a["appId"],
                  "| added", start, "| expires", cred.get("endDateTime"))
            print("            hint:", (cred.get("hint") or "")[:4] + "...")

# A secret added in the last 30 days with no change ticket is the cheapest
# signal in this whole report. Chase that number until it is boring.

二、Agent 改变的是算式,不只是速度

Microsoft 自己的表述很准确:Agent 不受影响人类的那些因素约束,可以 7×24 小时工作,周末和假期照跑,而且能同时尝试多条入侵路径。把这个放到一次常规红队行动里算一遍。人类操作者选一条路、验证、卡住就退。二十路并行不是「强二十倍」,而是换了一种统计结果——攻击者只需要一条路成功,防守方需要把所有路都堵住。这就是为什么「不到三小时」不是宣传数字,而是当入侵里最贵的那部分(耐心的串行试探)变成并行且免费之后,自然会出现的数字。

# Inventory pass 2: service principal sign-ins, oldest-touched first.
# AuditLog.Read.All required. /auditLogs is beta-only on Graph.
import json, urllib.request, collections

TOKEN = "<graph token>"
HDR = {"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"}

url = ("https://graph.microsoft.com/beta/auditLogs/signIns"
       "?$filter=signInEventTypes/any(t:t eq 'servicePrincipal')"
       "&$top=500")

req = urllib.request.Request(url, headers=HDR)
rows = json.load(urllib.request.urlopen(req, timeout=90))["value"]

seen = collections.Counter()
ipmap = collections.defaultdict(set)
for r in rows:
    name = (r.get("appDisplayName") or "unknown")
    seen[name] += 1
    ip = r.get("ipAddress") or "-"
    ipmap[name].add(ip)

for name, n in seen.most_common(25):
    print(f"{n:5d}  {name:<40}  ips={len(ipmap[name])}")
    if len(ipmap[name]) > 3:
        print("       ^ many source IPs for a non-interactive identity")

# Before you accept this output, check who owns each app. The finding is not
# the volume, it is the app nobody can name an owner for.

三、对 SaaS 真正致命的枢轴:一个已获授权的应用

ConnectWise 案里的下游机制,是多数团队没有建模过的那块。Storm-0940 滥用一个已被入侵的应用,通过「已经获得授权」的应用向 Microsoft 365 客户环境完成认证,因此不需要任何交互式用户登录。在多个环境中,Microsoft 观察到经由 Microsoft Graph 的 AI 加速后续活动:侦察、修改应用凭据、尝试修改权限以扩大或持久化访问。作为防守方再读一遍这份清单:里面没有一条是恶意软件检测。全是身份与配置,全都落在你的监控本来被调成忽略的那片正常 API 表面里,而且是由一个合法持有这些权限的实体执行的。

# Inventory pass 3: grants that carry application roles, not user scopes.
# Application permissions act without a signed-in human. That is the pivot.
import json, urllib.request

TOKEN = "<graph token>"
HDR = {"Authorization": "Bearer " + TOKEN}

def get(u):
    return json.load(urllib.request.urlopen(urllib.request.Request(u, headers=HDR), timeout=60))

grants = get("https://graph.microsoft.com/v1.0/oauth2PermissionGrants?$top=200")
high = []
for g in grants["value"]:
    scopes = (g.get("scope") or "").split()
    if not scopes:
        # Empty scope means application-level roles, the dangerous case.
        high.append((g.get("clientId"), g.get("resourceId"), "<app roles>"))
    else:
        risky = {"Mail.ReadWrite", "Files.ReadWrite.All", "Directory.ReadWrite.All",
                 "Sites.FullControl.All", "RoleManagement.ReadWrite.Directory"}
        if risky & set(scopes):
            high.append((g.get("clientId"), g.get("resourceId"), " ".join(sorted(risky & set(scopes)))))

for c, r, s in high:
    print("GRANT", c, "->", r, "|", s)
print("total grants:", len(grants["value"]), "flagged:", len(high))

# Every flagged row needs a name attached. If nobody can name it, remove it.
# That is a shorter conversation than an incident review.

四、该查什么,按回报排序

Microsoft 的建议很短,值得逐字执行:审查服务主体(service principal)登录、调查非预期的应用凭据或权限变更、遵循 ConnectWise 发布的处置指引、保存证据。顺序很重要。先看服务主体登录,因为那里是「没有人参与」的活动会显形的地方,而多数团队从没认真看过这份日志。第二看凭据与权限的差分,因为给 OAuth 应用加密钥正是攻击者持久化的方式,一个背后没有工单的新密钥,是你能拿到的最干净的单点信号。第三才是保存证据,而这一条恰恰是压力之下最容易被跳过的那步;把审计线索一起删掉的处置,也删掉了你判断「哪些租户被碰过」的能力。

// Defender XDR hunting: Graph activity that looks like post-compromise work.
// Run this before you need it, so the baseline exists.
AuditLogs
| where OperationName in ("Add service principal credentials.",
                          "Update application - Certificates and secrets management",
                          "Add delegated permission grant.",
                          "Add app role assignment to service principal.")
| extend Actor = tostring(InitiatedBy.user.userPrincipalName),
         App   = tostring(TargetResources[0].displayName)
| summarize Count = count(), FirstSeen = min(TimeGenerated),
            Actors = make_set(Actor, 5) by App
| where Count > 0
| order by FirstSeen desc

// Pair it with sign-ins where the app authenticated without a user:
// AADServicePrincipalSignInLogs
// | where ResultType == 0
// | summarize signins=count(), ips=dcount(IPAddress) by AppDisplayName
// | where ips > 3
// The pairing is the point: a credential change followed by new-IP
// authentication from that app is the sequence, not either event alone.

五、在下一次读报告之前,先查自己的租户

下面这几段代码用最小读权限(Directory.Read.All,审计查询再加 AuditLog.Read.All)对 Microsoft Graph 执行,回答三个问题:哪些服务主体最近完成过认证、但好几个月没人动过;哪些应用在过去 30 天里被添加了凭据;哪些 OAuth 授权带的是高权限应用角色而不是委派的用户范围。它们都不是检测规则,而是清单陈述——这才是你真正需要的,因为攻击者的优势恰恰在于「你的清单和现实不一致,而且还没人发现」。只读执行,留下输出,每周对比一次。

# Give an agent its own identity, then prove the scope is small.
# Run against your own tenant; this is the pattern, not a magic command.
set -euo pipefail

APP_NAME="report-agent-$(date +%Y%m)"

# 1) A dedicated app registration, not a human's credentials
APP_ID=$(az ad app create --display-name "$APP_NAME" --query appId -o tsv)
az ad sp create --id "$APP_ID" >/dev/null

# 2) One resource, one role. Start with read-only Mail, not Mail.ReadWrite.
az ad app permission add --id "$APP_ID" \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope   # User.Read

# 3) A credential with a short life, tracked in your secret store
az ad app credential reset --id "$APP_ID" --years 1 --append \
  --display-name "rotation-$(date +%Y-%m)" --query endDateTime -o tsv

# 4) Prove it: list what this identity can actually reach
az ad app permission list --id "$APP_ID" -o table

# Write the owner and the rotation date into the app description. An identity
# with no owner is the finding, regardless of what else it can do.

六、如果你自己跑 Agent,你多了一块有工位号的攻击面

让人不舒服的对称之处在于:Agent 在你自己的系统里之所以有用,和它对入侵者之所以有价值,是同一组属性——长期有效的凭据、宽泛的权限范围、无需人工点击即可行动的能力、以及基本只记录成功的日志。由此有三条实践。第一,给每个 Agent 独立身份,而不是借用某个人的身份,这样吊销只需要一次操作。第二,把权限收敛到它真正调用的那些工具上,并把这份工具清单当作需要评审的产物,而不是任其漂移的配置。第三,每次工具调用都记录参数,因为当问题变成「这个 Agent 做了什么」时,工具调用日志能回答,而模型日志只能暗示。Microsoft 的报告最后给的是某个厂商产品的处置指引;更普适的教训更窄也更普世:身份、权限范围和调用日志,现在就是边界本身。

从选中目标到拿到特权访问不到三小时:Storm-0940 的 Agent 化攻击

从选中目标到拿到特权访问不到三小时:Storm-0940 的

从选中目标到拿到特权访问不到三小时:Storm-0940 的 Agent 化攻击

从选中目标到拿到特权访问不到三小时:Storm-0940 的

📌 常见问题 FAQ

ARTEX 是什么?

Microsoft 把 ARTEX 描述为一个开源渗透测试框架,可以让任意 AI 模型自动化多种攻击性安全工作流,本身是一个用 Go 写的多 Agent LLM 系统。Storm-0940 用它做扫描侦察、密码喷洒,以及对 ConnectWise 事件下游受害者的 OAuth 应用滥用。

入侵有多快?

按 Microsoft 的说法,在多起案例中从选定目标到取得云基础设施特权访问不足三小时。Agent 不受人类因素约束,可以持续运行并同时尝试多条入侵路径。

为什么 OAuth 应用滥用比初始访问更关键?

给已有 OAuth 应用添加凭据,等于给了攻击者一个在密码更换后仍能认证的身份;而且它可以借助「已经获得用户同意」的应用访问 Microsoft 365 资源,不需要任何交互式登录。

Microsoft 自身被入侵了吗?

没有。Microsoft 明确说明这些活动并非源自对 Microsoft 公司系统、产品或服务的入侵或漏洞。观察到的认证与后续活动发生在客户环境中、通过受影响的应用完成;出现此类活动本身并不等于客户数据已被访问或外泄。

管理员第一步应该做什么?

审查服务主体登录、调查非预期的应用凭据或权限变更、遵循 ConnectWise 发布的处置指引,并保存相关证据用于调查。Microsoft Defender XDR 提供了针对可疑 Microsoft Graph 活动的检测与狩猎查询。