OFERRY正在进入矩阵

黑客把入侵外包给 AI 代理:NorForge 从裸奔在公网的控制台里看到了全过程

10 月 19 日,挪威安全公司 NorForge 发了一篇 16 分钟读完的调查报告《Inside Cairn》,同一天路透社拿到了独家。故事的主线是:一个攻击者把入侵外包给了 AI 代理——对 1200 多个站点发起尝试,100 多家被确认攻破,成功个案平均耗时不到 23 小时,整个活动的 token 花费估算约 5000 美元。

但真正让这份报告值得读的,不是「AI 能打站」这个结论——那件事已经被说过很多遍了。是取证入口:研究者并不是靠流量分析或蜜罐抓到这条线的,而是发现这名攻击者把自己的 AI 代理控制台裸奔在公网 8000 端口上,面板连鉴权都没有,里面装着他所有客户被探测出来的凭据(官方定义合规的公司/客户一般叫目标)。安全公司连进去看到的东西,比攻击者自己想看到的还多。

先把数字账摆清楚#

先把可核的与不可核的分开。

NorForge 报告里明确写下的(一手口径):

  • 三个活动集群,合计 25 个暴露在公网的控制面板(AIA0001 十四个、AIA0002 十个、AIA0003 一个),端口统一是 8000。
  • 主集群 AIA0001 关联超过 1,100 个攻击项目,语言为中文,判定置信度「高」,被攻击站点集中在美国、加拿大、日本、澳大利亚四国的真实电商站(流量 20 万–150 万)。
  • 整体成功率约 10%;到達目标(成功个案)平均耗时略低于 23 小时;成功的项目平均产出约 140 条 fact。
  • 用 Trufflehog 扫描面板数据,产出「数以百计」的各类凭据,其中11% 被验证仍然有效。
  • 平均单个任务成本估算约 5 美元(含失败与提前终止的项目);有项目超过 1000 条 intent/fact,这类项目成本可能超过 100 美元。报告自己写明「不确定性很大,误差可能明显」。

路透社与后续报道补充的(第三方口径,未经官方确认):

  • 尝试站点数「超过 1,200」这个数字出现于后续报道;被攻破的公司被点名到 John Deere、SharkNinja、Cummins 等——这三家出现在第三方报道的标题里,NorForge 报告本身对受害者是脱敏的。
  • NorForge 的 Snorre Fagerland 对路透社给出的估算是总计约 5,000 美元 token、平均每次成功入侵约 50 美元。
  • 所有任务跑在一个名为 claudecode_deepseek-v4.1-flash 的 worker 下,NorForge 据此推测是 Claude Code 这类 harness 挂上了 DeepSeek 的低价模型。这条细节 Anthropic 与 DeepSeek 都没确认过,写进文章时要按「研究者推测」处理。
  • 发现时间线:Fagerland 大约在 10 月 2 日开始发现暴露的 Cairn 面板,到那时已认出约二十来个被用于恶意目的实例。

一条有意思的旁证:「10% 成功率」听起来很低,但把它和成本放在一起看就变味了。10% 乘上「每次尝试几美元」,等于单次成功入侵的边际成本压到了几十美元量级——这个数字足以解释为什么这类活动会变多:它不再需要人的时间,只需要电费和 token。

Cairn 是什么:把入侵拆成状态机#

Cairn 是一个开源的问题求解 harness(报告成文时它的代码在 GitHub 上公开可查)。它的设计不是「让模型自由发挥」,而是状态式的问题求解引擎:

  • 每个攻击任务是一个 project,带 origin(从哪里开始)和 goal(什么算完成)。
  • 所有中间结果放在一块黑板(blackboard)上,分两类:fact(已确认的事实,比如「这个域名是 Magento 2.4.9」)和 intent(打算做的事)。上面那个「平均 140 条 fact / 项目」就是这个黑板的规模指标。
  • 多个 agent 分工协作(bootstrap / reason / explore 这类角色),由 dispatcher 按优先级和并发上限派活。
  • hints 用来注入人工提示——这也是攻击者唯一需要出手的地方。

这带来一个很关键的副作用,NorForge 点出来了:黑板上的东西会被原样存在面板数据库里。也就是说,只要面板暴露,攻击者积累的所有情报——包括他在别处收割的、用于这批目标的凭据——就一起暴露了。对合法的红队来说这是噩梦(拿客户凭据测试,结果把凭据发到了公网上),对攻击者来说这是把柄。

任务长什么样#

NorForge 公开了一份「相当典型」的任务原文(中文原样保留,报告给了英译)。摘其要点:

这是四国(US/CA/JP/AU)已确认的真实购物电商站(流量 200k–1.5M)。 目标:找未认证 RCE(命令执行/代码注入/反序列化/文件上传 webshell/SQLi→RCE/SSTI/SSRF→RCE)。 优先:登录 SQLi、未认证端点、文件上传、反序列化、认证绕过、账户接管。 落地用 uploader shell(HTML 上传表单 + move_uploaded_file 保存),绝不落地命令执行型 webshell。 Goal met when whoami/id 输出确认命令执行。

这份文本里有三个值得单独拿出来讲的细节,它们说明这套东西已经工程化到什么程度:

一是目标经过预筛选。 上游喂料管线用 Playwright 自动验证过「这个站能不能下单」,再按流量排序投喂给 Cairn。攻击者省掉的第一步侦察,是用真浏览器跑出来的。

二是「绝不落地命令执行型 webshell」是一条刻意的策略。 命令执行型 webshell 的特征太明显(system/exec/eval),上传器型只有上传表单 + 重命名落盘,杀软与完整性检查都更难抓。攻击者的目标不是「马上拿 shell」,而是先拿到长期访问权——AIA0001 集群的行为特征被描述为「专门为了拿到访问权、以后再回来做更多事」。

三是喂料管线里有个低级 bug,反而暴露了关联性。 多个面板上的目标域名都被吃掉了一个开头的字母 w:Wholesale 变成 holesale、winter 变成 inter。原因是有一个共用的 domain 规范化脚本在去 www 时多删了一个字母。NorForge 说这反而让关联面板变得非常好认。这是本次事件里最适合拿来当反面教材的一条:你自动化管线里的每个 bug,都是别人归因你的线索。

取证窗口:他们看到了攻击者的工具箱#

有一次,其中一个 AIA0001 主机为了做带外(out-of-band)测试,在高位端口上开了一个 Web 服务器。就在这个短暂的窗口里,AI worker 的工作目录被看到了——于是有了这份工具清单:

类别看到的工具
浏览器自动化Playwright + Chromium(目录里是浏览器状态)
PHP 载荷PHPGGC(PHP Generic Gadget Chains,生成反序列化 gadget chain)
特定漏洞研究StyleSmuggler HTTP PoC,以及一个 Docker 化、按 revision 钉住的 Magento Open Source 2.4.9 验证实验室(对应 CVE-2026-75650)
侦察OWASP Amass、dnsrecon、ffuf、Nmap、rockyou 字典
运行环境Docker、PHP/Composer、Python 3、Node.js、Xvfb、tmux、OpenSSL
其他Vosk(离线语音识别)、Odoo 框架相关产物

两点值得注意:Magento 漏洞有专门的 Docker 实验室,说明攻击链是先在本地复现、再打真站的;以及工具清单里有识别语音的工具——安全圈对此的反应普遍是「这套工具箱不像只用来打电商站」。

还有一段 agent 任务更新被原文引用了:

本地环境是一个 Kali 容器,属于 tsec CTF/智能渗透测试竞赛框架……

NorForge 的判断是:这套 agent 环境是从一个更早的 CTF 竞赛配置里改出来的,「几乎可以确定是犯罪分子复用了既有工具,没有任何理由认为相关厂商参与其中」。这句话写得很克制——把一条容易被误读成「某厂商资助黑客」的证据,明确按「工具链复用」处理了。

防御侧今天就能做的四件事#

以下是基于这份报告的可核事实,能立刻落到自己环境里的动作。代码都是标准库,可以直接跑。

一、先查自己的「AI 工具面板」有没有裸奔#

这份报告最扎眼的一点是 25 个面板全在 8000 端口,而且没有鉴权。8000、8080、3000、8888 是各种 agent/编排框架(LangServe、FastAPI、Jupyter、Gradio、Open WebUI 一类)的默认端口。先扫自己的网段:

PYTHON
# scan_exposed_panels.py —— 只扫你自己拥有的主机,别扫别人的
import socket, ipaddress, concurrent.futures as cf

PORTS = [8000, 8080, 8888, 3000, 5000]  # 常见 Agent/编排框架默认端口
BANNERS = [(b"uvicorn", "FastAPI/uvicorn"), (b"gradio", "Gradio"),
           (b"jupyter", "Jupyter"), (b"Cairn", "Cairn 面板")]

def probe(ip, port, timeout=1.5):
    try:
        with socket.create_connection((ip, port), timeout=timeout) as s:
            s.sendall(b"GET / HTTP/1.0\r\nHost: %b\r\n\r\n" % ip.encode())
            data = s.recv(2048)
    except OSError:
        return None
    hit = [name for sig, name in BANNERS if sig.lower() in data.lower()]
    return {"target": f"{ip}:{port}", "server": data.decode("latin-1", "ignore").splitlines()[0] if data else "",
            "matches": hit, "looks_like_panel": bool(hit)}

def scan(cidr):
    ips = [str(i) for i in ipaddress.ip_network(cidr).hosts()]
    out = []
    with cf.ThreadPoolExecutor(max_workers=64) as ex:
        futs = [ex.submit(probe, ip, p) for ip in ips for p in PORTS]
        for f in cf.as_completed(futs):
            r = f.result()
            if r and r["looks_like_panel"]:
                out.append(r)
    return out

if __name__ == "__main__":
    import json
    for row in scan("10.0.0.0/24"):      # 换成你自己的网段
        print(json.dumps(row, ensure_ascii=False))
READ / EXECUTE / UNDERSTANDUTF-8

拿到清单后的动作比扫描更重要:默认绑 127.0.0.1 而不是 0.0.0.0,对外只开在有鉴权的反代后面。这份报告里所有攻击者的便宜,都来自「默认监听所有网卡 + 没有鉴权」这一个组合。

二、把「上传器型 webshell」变成可检测对象#

报告里那份任务原文明确写了落地形态:HTML 上传表单 + move_uploaded_file,不带命令执行函数。针对这一类,签名检测是无效的——它本来就没有特征函数。有效的是完整性基线 + 上传目录纪律:

PYTHON
# upload_watch.py —— 上传目录的基线比对 + 上传器形态启发式
import hashlib, pathlib, json, re, sys

SUSPECT = re.compile(r"(move_uploaded_file|\$_(?:FILES|POST)\s*\[|<input[^>]+type=[\"']file[\"'])", re.I)
WEB_EXT = {".php", ".php3", ".php5", ".phtml", ".jsp", ".jspx", ".asp", ".aspx", ".war", ".cgi", ".pl"}
UPLOAD_DIR = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "uploads")
BASE = pathlib.Path("upload_baseline.json")

def snapshot(root):
    out = {}
    for p in root.rglob("*"):
        if p.is_file():
            out[str(p.relative_to(root))] = hashlib.sha256(p.read_bytes()).hexdigest()
    return out

now, base = snapshot(UPLOAD_DIR), (json.loads(BASE.read_text()) if BASE.exists() else {})

for rel, digest in now.items():
    new = base.get(rel) != digest
    if not new:
        continue
    f = UPLOAD_DIR / rel
    flags = []
    if f.suffix.lower() in WEB_EXT:
        flags.append("可执行后缀")
    try:
        body = f.read_text("utf-8", "ignore")[:8192]
        if SUSPECT.search(body):
            flags.append("上传器形态代码")
    except OSError:
        pass
    print(f"[新增/变更] {rel} {'| ' + ' '.join(flags) if flags else ''}")

BASE.write_text(json.dumps(now, indent=1))
READ / EXECUTE / UNDERSTANDUTF-8

两个工程要点:上传目录必须禁止脚本执行(Nginx 里对上传路径直接 location ~* /uploads/.*\.(php|phtml)$ { deny all; },或让上传落盘到另一个域名的裸对象存储),以及别把上传目录放在 webroot 下——报告里 AIA0002 集群的任务原文就写着「藏到主目录之外的深层目录里,名字起得像正常文件,比如 confirm.php」,说明攻击者是按这个假设找位置的。

三、从访问日志里抓「代理脚印」#

AI 代理和爬虫不同:它的请求密度、UA、以及它验证目标的方式都很特别。报告给了一条现成的特征——目标是用 Playwright 真浏览器跑过的。

PYTHON
# hunt_agent_traffic.py —— 从 nginx access log 里找 Playwright/无头浏览器与扫描行为
import re, sys, collections, datetime

LINE = re.compile(r'(?P<ip>\S+) .*?"(?P<req>[^"]*)" (?P<st>\d{3}) .*?"(?P<ua>[^"]*)"')
HEADLESS = re.compile(r"(HeadlessChrome|Playwright|puppeteer|Xvfb|Chrome/\d+.*Headless)", re.I)
SCANNER = re.compile(r"(ffuf|nmap|nikto|sqlmap|amass|masscan)", re.I)

hits = collections.Counter()
for line in open(sys.argv[1], errors="ignore"):
    m = LINE.search(line)
    if not m:
        continue
    ua = m.group("ua") or ""
    if HEADLESS.search(ua) or SCANNER.search(ua):
        # 按「IP + 小时」聚合:短时间内密集探测才是信号,偶发一次不算
        hits[(m.group("ip"), 1)] += 1

for (ip, _), n in hits.most_common(50):
    if n >= 30:
        print(f"{ip}\t{n} 次无头/扫描器请求 —— 建议核对是否为自动化验收,否则封禁并查来源")
READ / EXECUTE / UNDERSTANDUTF-8

这份代码不抓攻击,它抓的是**「值得看一眼的异常」**。把它接进日志管道,比事后翻日志有效得多——报告里 AIA0001 到被发现时已经跑了 1,100 多个项目。

四、别忘了最便宜的那件事:凭据卫生#

报告里 Trufflehog 扫出来「数以百计」的凭据、11% 验证有效。这个比例之所以高,是因为面板里存的是攻击者为了打这批目标而专门准备的凭据——它们本来就是活的。自己这边要做的是反过来:确认没有任何一份凭据躺在没有鉴权的服务里。最容易被忽略的三处是:面板/编排工具的数据库、CI 的构建日志与环境变量转储、以及测试环境里从生产复制过来的配置。

SHELL
# 用官方镜像扫一遍你自己的仓库与部署目录(不联网、只读)
docker run --rm -v "$PWD:/p" trufflesecurity/trufflehog:latest filesystem /p --only-verified
READ / EXECUTE / UNDERSTANDUTF-8

--only-verified 是关键:它会真的去尝试用这个凭据认证,只报验证有效的,正好对上报告里那个 11% 的口径。

别把这篇读成机器人大末日#

有三件事必须说清楚,否则这类新闻很容易被读成「AI 已经能自主攻破一切」。

第一,成功率是约 10%。 也就是说绝大多数项目并没有达成目标。报告自己的措辞是「很多剩余任务达成了部分目标,或者收集到了可用于后续尝试的信息」——这更像是「把人工侦察与试探的成本压到接近零」,而不是「自动化的完胜」。

第二,攻击者的 opsec 差得离谱。 面板无鉴权、SSH 登录凭据写在任务里、域名规范化脚本有 bug、工具目录在带外测试时被开在公网端口上。报告结论段写得很直白:「在追求快速结果的过程中,很多人忘了基本的安全预防措施,不但把自己的行动安全搞砸了,还让攻击面比原本的意图大得多。」

第三,模型与 harness 都在快速变化。 报告里那句「这只是当下这个时间点的快照」值得抄一遍:这套数字的保质期可能只有几个月。

真正需要记住的推论是成本结构变了:一次入侵尝试从「人的几小时」变成了「几美元」。这不会让所有系统立刻被打穿,但它会改变谁的资产值得被试探——报告里 AIA0001 的挑选标准写得很清楚:四国、真实电商站、流量 20 万以上。它会先来试探那些「看起来像有钱」的站。

总结#

这份报告里最有用的一句话,是它对攻击者自己的评价:「不论这些操作者掌握多少攻击技术,保护自己的基础设施显然不在他们的强项里。」

对我们这边,可执行的顺序是:

  1. 收口:所有 agent/编排/面板类服务默认绑本机,对外必须有鉴权——报告里 25 个面板、统一 8000 端口、零鉴权,这一条就能挡住同类事故的大头。
  2. 上传纪律:上传目录移出 webroot、禁止脚本执行、做完整性基线;对「上传器型 webshell」不要指望签名,指望基线。
  3. 日志里带上代理视角:无头浏览器身份与高频探测是最早的信号,比事后做取证便宜。
  4. 凭据卫生:用 --only-verified 扫,把「躺着的活凭据」清零;攻击者是因为把凭据存在了裸奔面板上才被抓到的,同样的错误在防守方这边一样致命。

最后提醒一句事实边界:本文引用的 claudecode_deepseek-v4.1-flash 这个 worker 名、以及「约 50 美元一次成功入侵」「1,200 多个站点」这些数字,分别来自研究者的推测与第三方报道口径,厂商没有确认,NorForge 也明确标注了不确定性。可以据此调风险策略,但别把这些数字当成可以引用的定论。

参考与致谢#

黑客把入侵外包给 AI 代理:NorForge 从裸奔在公网的控制台里看到了全过程

https://www.oferry.com/posts/a483/
作者
摆渡
发布于
许可协议
CC BY-NC-SA 4.0

除另有声明外,本文采用上述许可协议。转载或改编请注明作者与原文地址,并遵守许可条款。