












2026年8月31日,一个疑似俄语攻击者用数百个AI代理(OpenAI Codex + DeepSeek驱动),在自建实验室里完成漏洞研究、武器化和大规模执行,打穿440台PaperCut打印管理服务器,12个组织被打到域控管理员,26秒内攻破11个组织。
传统攻击者在利用一个漏洞之前,要经历一个很长的手动流程:读CVE公告、理解漏洞原理、搭测试环境、写exploit代码、调试、扫描互联网上的目标、逐一执行、后渗透。每一步都需要人坐在键盘前操作,可能花几天到几周。
2026年8月31日发生的事情不一样。
一个疑似俄语攻击者(GreyNoise追踪到的IP是 45.142.193.132),用数百个AI代理组成一个"代理群"(agent swarm),把上面这套流程从头到尾自动化了。这些AI代理由 OpenAI Codex 编排框架驱动,底层用 DeepSeek 模型做推理。

GreyNoise 在2026年9月9日的报告《Agents Gone Wild》里描述了这个过程:
| 阶段 | 传统方式 | AI代理群方式 |
|---|---|---|
| 漏洞研究 | 人工读CVE、看Patch diff | AI代理自动分析漏洞公告和补丁 |
| Exploit开发 | 手写代码、反复调试 | AI代理在实验室环境中自动生成和测试exploit |
| 目标扫描 | 手动用Shodan/Netlas等工具 | AI代理自动调用Netlas.io构建目标清单 |
| 大规模利用 | 逐个目标手动执行 | 数百个代理并行执行exploit链 |
| 后渗透 | 手动操作Meterpreter/Mimikatz | AI代理自动收集凭证、横向移动 |

关键区别不是"AI让攻击变强了",而是"AI让攻击变快了"。GreyNoise给出了一个令人窒息的时间线:
空工作区到首次RCE(远程代码执行):不到4小时
首次域控管理员:再花2小时
全面启动后:26秒内攻破11个组织
最快一例:从初始访问到域控管理员,7分钟
攻击者串联了两个PaperCut NG/MF漏洞。PaperCut是一款打印管理软件,很多学校和企业用它来追踪打印任务、管理打印配额,部署量极大。
| CVE编号 | 类型 | CVSS | 核心问题 |
|---|---|---|---|
| CVE-2026-81578 | 认证绕过(不当访问控制) | 未公布 | Web管理界面中,未认证请求在后端访问控制检查完成前就触发了管理操作 |
| CVE-2026-82078 | 不安全动态类加载(CWE-470) | 9.4 | 数据库连接工具根据可配置的驱动名称实例化数据库驱动类,没有做白名单校验 |

攻击链的逻辑是这样的:
第一步,攻击者用 CVE-2026-81578 绕过认证。PaperCut的Web管理界面有一处设计缺陷:后端在检查"你有没有权限"之前,就先执行了部分管理操作。攻击者发一个精心构造的未认证请求,就能修改系统配置参数。
第二步,攻击者用 CVE-2026-82078 触发远程代码执行。PaperCut的数据库连接工具有一个功能:根据配置文件里写的"数据库驱动名称"来动态加载Java类。正常情况下这里填的是MySQL或PostgreSQL的驱动类名。但因为没有白名单校验,攻击者通过第一步修改了配置参数后,可以让PaperCut加载任意Java类——相当于在服务器上执行任意代码。
PaperCut在2026年8月27日发布了紧急补丁,但攻击者在8月31日就开始利用了,补丁到攻击仅隔4天。

GreyNoise把整个攻击生命周期分为5个阶段。下面逐个拆解。
AI代理自动分析CVE公告和补丁差异,理解两个漏洞的原理和利用条件。这一步不需要人工介入,AI代理读公开信息就能完成。
攻击者搭了一个自建实验室环境,里面包含一个有漏洞的PaperCut安装和一个Active Directory服务器。AI代理在这个环境里反复测试exploit链,确认能用之后才去碰真实目标。
这一步值得注意:攻击者不是盲打。他在实验室里验证了完整的攻击链,包括从RCE到域控的横向移动路径。
AI代理调用 Netlas.io(一个互联网扫描服务)搜索公网暴露的PaperCut实例,自动构建目标清单。扫描结果涵盖了48个国家的大量教育机构。
数百个AI代理同时执行exploit链,对目标清单中的服务器逐个攻击。这就是"26秒11个组织"的来源:当全部代理同时启动时,攻击速度远超人工操作的能力。
Arctic Wolf 的报告描述了后渗透阶段的活动:
| 活动类型 | 具体操作 |
|---|---|
| 凭证收集 | 转储LSASS进程内存、收集Windows注册表hive(SYSTEM和SECURITY),base64编码后通过HTTP外传 |
| 横向移动 | 使用pass-the-hash攻击域控 |
| 持久化 | 部署Metasploit/Meterpreter相关Java payload |
| 信息搜集 | 执行命令识别主机、用户、进程和敏感配置数据 |

GreyNoise观察到,在打到域控管理员的12个组织中,攻击者走了三条不同的路径。这三条路径揭示了一个关键事实:PaperCut服务器往往运行在域成员机器上,且经常以高权限服务账号运行。
| 路径 | 前提条件 | 攻击手法 | 难度 |
|---|---|---|---|
| 路径A | PaperCut运行在域成员服务器上 | 转储LSASS进程内存和注册表密钥(SYSTEM/SECURITY hive),恢复特权凭证哈希,用pass-the-hash认证到域控 | 中等 |
| 路径B | 域内存在未修复的noPac漏洞 | 利用noPac漏洞直接提升权限 | 低(只需漏洞存在) |
| 路径C | PaperCut以域管服务账号运行 | 直接把自己的账户加入Domain Admins组 | 极低 |
路径C是最可怕的。如果PaperCut的服务账号本身就有域管权限,攻击者拿到RCE后一步就能拿到整个域的控制权,不需要任何横向移动技术。Silverfort在分析中指出,这就是为什么最快一例只花了7分钟——从初始访问到域控,中间没有任何"技术含量",纯粹是因为权限配置太松。

把所有数字放在一起看,整个攻击的漏斗是这样的:
| 层级 | 数量 | 占比 | 含义 |
|---|---|---|---|
| 扫描到的PaperCut实例 | 大量 | 100% | Netlas.io枚举到的公网暴露实例 |
| 成功攻破(RCE) | 440 | - | 完成认证绕过+类加载RCE |
| 凭证被收割 | 280 | 63.6% | 攻击者从中提取了可用凭证 |
| 系统/域密获取 | 147 | 33.4% | 获取了操作系统级或域级密钥 |
| 域控管理员 | 12 | 2.7% | 拿到整个域的控制权 |
395个组织、48个国家受影响。主要目标是教育行业,占比约一半。受影响最多的国家是美国、英国、法国、西班牙、加拿大、比利时、葡萄牙、澳大利亚、德国和瑞士。
台湾地区的数据是12/11/10/0——12个实例被攻破,11个凭证被收割,10个系统密钥被获取,0个被打到域管。说明当地的基础硬化做得相对到位。
另外值得一提的是:GreyNoise提到有一个案例被Cloudflare WAF挡住了。这证明即使不做深度防御,基本的WAF规则对已知的攻击模式仍然有效。

以下脚本面向授权运维人员,用于自查环境中是否存在本次攻击的暴露面。
一句话讲清楚:扫描指定IP范围,检查PaperCut默认端口(9191/9192)的Web管理界面是否对公网开放。公网可达是本次攻击的入口条件。
"""
PaperCut管理接口公网暴露检测
扫描指定IP范围的9191/9192端口,检查PaperCut Web管理界面是否开放
"""
import socket
import urllib.request
import urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed
PAPERCUT_PORTS = [9191, 9192]
TIMEOUT = 5
SIGNATURES = ["papercut", "PaperCut", "print management", "app?service=page/"]
def check_host(ip: str, port: int) -> dict:
result = {"ip": ip, "port": port, "open": False, "is_papercut": False, "status": ""}
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(TIMEOUT)
ret = sock.connect_ex((ip, port))
sock.close()
if ret != 0:
return result
result["open"] = True
url = f"http://{ip}:{port}/"
req = urllib.request.Request(url, headers={"User-Agent": "PaperCut-Audit/1.0"})
resp = urllib.request.urlopen(req, timeout=TIMEOUT)
body = resp.read(4096).decode("utf-8", errors="ignore").lower()
result["status"] = str(resp.status)
for sig in SIGNATURES:
if sig.lower() in body:
result["is_papercut"] = True
break
except urllib.error.HTTPError as e:
result["status"] = str(e.code)
try:
body = e.read(4096).decode("utf-8", errors="ignore").lower()
for sig in SIGNATURES:
if sig.lower() in body:
result["is_papercut"] = True
break
except Exception:
pass
except Exception as e:
result["status"] = str(e)
return result
def scan_range(ip_list: list) -> list:
results = []
targets = [(ip, port) for ip in ip_list for port in PAPERCUT_PORTS]
with ThreadPoolExecutor(max_workers=50) as pool:
futures = {pool.submit(check_host, ip, port): (ip, port) for ip, port in targets}
for f in as_completed(futures):
r = f.result()
if r["open"]:
results.append(r)
return results
if __name__ == "__main__":
# 替换为你的授权IP范围
test_ips = ["127.0.0.1"]
print("[*] PaperCut管理接口暴露检测")
print(f"[*] 扫描目标: {len(test_ips)} 个IP, 端口 {PAPERCUT_PORTS}")
results = scan_range(test_ips)
exposed = [r for r in results if r["is_papercut"]]
print(f"[+] 开放端口的主机: {len(results)}")
print(f"[+] 确认为PaperCut的: {len(exposed)}")
for r in exposed:
print(f" [!] {r['ip']}:{r['port']} - PaperCut管理界面公网可达")
if not exposed:
print(" [OK] 未发现公网暴露的PaperCut管理界面")
一句话讲清楚:通过HTTP请求获取PaperCut的版本信息,判断是否运行在有漏洞的版本上。CVE-2026-81578和CVE-2026-82078影响24.1.9之前和25.0.12之前的版本。
"""
PaperCut版本漏洞检测
通过Web管理界面的HTTP响应头和页面内容提取版本信息
受影响版本: PaperCut NG/MF < 24.1.9 或 < 25.0.12
"""
import urllib.request
import urllib.error
import re
import sys
# 已修复版本(根据补丁公告)
PATCHED_VERSIONS = {
"24.1.9": True,
"25.0.12": True,
}
def parse_version(version_str: str) -> tuple:
"""将版本字符串解析为可比较的元组"""
parts = re.findall(r"\d+", version_str)
return tuple(int(p) for p in parts) if parts else (0,)
def is_vulnerable(version_str: str) -> bool:
"""判断版本是否受漏洞影响"""
v = parse_version(version_str)
# 24.1.9之前的版本受影响
if v < (24, 1, 9):
return True
# 25.0.x系列中,25.0.12之前的版本受影响
if (25, 0) <= v < (25, 0, 12):
return True
return False
def get_papercut_version(url: str) -> str:
"""从PaperCut管理界面提取版本信息"""
headers = {"User-Agent": "PaperCut-Version-Audit/1.0"}
try:
req = urllib.request.Request(url, headers=headers)
resp = urllib.request.urlopen(req, timeout=10)
body = resp.read(8192).decode("utf-8", errors="ignore")
# PaperCut版本通常出现在页面底部或meta标签中
patterns = [
r'(?:version|ver)[:\s]*([\d.]+)',
r'build\s*#?\s*([\d.]+)',
r'PaperCut\s+(?:NG|MF)\s+([\d.]+)',
r'"version":\s*"([\d.]+)"',
]
for pattern in patterns:
match = re.search(pattern, body, re.IGNORECASE)
if match:
return match.group(1)
# 检查HTTP响应头
server = resp.headers.get("Server", "")
match = re.search(r'([\d.]+)', server)
if match:
return match.group(1)
return "unknown"
except urllib.error.HTTPError as e:
return f"HTTP {e.code}"
except Exception as e:
return f"error: {e}"
def audit_papercut(url: str) -> dict:
"""完整审计单个PaperCut实例"""
version = get_papercut_version(url)
result = {
"url": url,
"version": version,
"vulnerable": False,
"cves": [],
}
if version not in ("unknown",) and not version.startswith("HTTP") and not version.startswith("error"):
if is_vulnerable(version):
result["vulnerable"] = True
result["cves"] = ["CVE-2026-81578", "CVE-2026-82078"]
return result
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:9191"
print(f"[*] PaperCut版本漏洞检测")
print(f"[*] 目标: {target}")
result = audit_papercut(target)
print(f"[+] 版本: {result['version']}")
if result["vulnerable"]:
print(f" [!] 受影响CVE: {', '.join(result['cves'])}")
print(f" [!] 建议: 立即升级到 24.1.9+ 或 25.0.12+")
elif result["version"] == "unknown":
print(f" [?] 无法确定版本,建议手动检查")
else:
print(f" [OK] 版本已修复")
一句话讲清楚:检查本地PaperCut服务进程的运行账户,判断它是否以域管理员或高权限域服务账号运行。这是路径C(直接加人到Domain Admins)的前提条件。
"""
PaperCut服务运行权限检测
检查PaperCut服务进程是否以高权限域账号运行
路径C的风险前提: PaperCut以域管服务账号运行 → RCE后一步拿域控
"""
import subprocess
import re
import sys
PAPERCUT_PROCESS_NAMES = [
"pc-app.exe",
"pc-print-provider.exe",
"pc-web-print.exe",
"PaperCut.exe",
"papercut",
]
def get_service_accounts() -> list:
"""获取所有服务的运行账户(Windows)"""
results = []
try:
cmd = 'sc query type= service state= all'
output = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=30
)
services = output.stdout.split("\n\n")
for svc in services:
name_match = re.search(r'SERVICE_NAME:\s*(\S+)', svc)
if not name_match:
continue
svc_name = name_match.group(1)
# 获取服务配置中的运行账户
qcmd = f'sc qc "{svc_name}"'
qoutput = subprocess.run(
qcmd, shell=True, capture_output=True, text=True, timeout=10
)
account_match = re.search(
r'SERVICE_START_NAME\s*:\s*(.+)', qoutput.stdout
)
if account_match:
account = account_match.group(1).strip()
# 检查是否PaperCut相关服务
if any(pn.lower() in svc_name.lower() for pn in PAPERCUT_PROCESS_NAMES):
results.append({
"service": svc_name,
"account": account,
"is_domain_admin": check_domain_admin(account),
})
except Exception as e:
print(f"[!] 获取服务信息失败: {e}")
return results
def check_domain_admin(account: str) -> bool:
"""检查账户是否为域管理员"""
high_priv_indicators = [
"Domain Admin",
"Enterprise Admin",
"ADMINISTRATOR",
"SYSTEM",
"LocalSystem",
]
account_upper = account.upper()
for indicator in high_priv_indicators:
if indicator.upper() in account_upper:
return True
# 检查是否是域账户(包含\且不是本地计算机名)
if "\\" in account:
domain = account.split("\\")[0]
user = account.split("\\")[1]
if domain.upper() not in ["NT AUTHORITY", "LOCALSYSTEM", "NT SERVICE"]:
# 域账户运行服务,需要进一步确认权限
return True # 标记为需要人工确认
return False
def check_process_accounts() -> list:
"""通过进程列表检查PaperCut进程的运行账户"""
results = []
try:
output = subprocess.run(
'tasklist /v /fo csv',
shell=True, capture_output=True, text=True, timeout=30
)
lines = output.stdout.strip().split("\n")
for line in lines:
for pn in PAPERCUT_PROCESS_NAMES:
if pn.lower() in line.lower():
# 解析CSV行
parts = line.strip('"').split('","')
if len(parts) >= 8:
proc_name = parts[0].strip('"')
user = parts[7].strip('"') if len(parts) > 7 else ""
results.append({
"process": proc_name,
"user": user,
"is_domain_admin": check_domain_admin(user),
})
break
except Exception as e:
print(f"[!] 获取进程信息失败: {e}")
return results
if __name__ == "__main__":
print("[*] PaperCut服务运行权限检测")
print("[*] 检查路径C风险: PaperCut是否以高权限域账号运行")
print()
print("[*] 检查服务账户...")
services = get_service_accounts()
if services:
for svc in services:
status = "[!]" if svc["is_domain_admin"] else "[OK]"
print(f" {status} 服务: {svc['service']}")
print(f" 账户: {svc['account']}")
if svc["is_domain_admin"]:
print(f" 风险: 高权限账户运行PaperCut (路径C)")
else:
print(" [?] 未找到PaperCut相关服务(可能未安装或名称不同)")
print()
print("[*] 检查进程账户...")
processes = check_process_accounts()
if processes:
for proc in processes:
status = "[!]" if proc["is_domain_admin"] else "[OK]"
print(f" {status} 进程: {proc['process']}")
print(f" 用户: {proc['user']}")
if proc["is_domain_admin"]:
print(f" 风险: 高权限账户运行PaperCut (路径C)")
else:
print(" [?] 未找到PaperCut相关进程")
print()
print("[*] 建议:")
print(" - PaperCut服务应以最小权限的本地服务账号运行")
print(" - 绝不要用域管理员账号运行PaperCut服务")
print(" - 如果PaperCut需要域访问,使用专门的低权限服务账号")
print(" - 限制PaperCut服务器的网络访问范围,不要暴露到公网")
GreyNoise的报告是单一来源的观测数据,未经第二方独立复验。攻击时间线和所有数字都来自GreyNoise的全球观测网格。
攻击者具体使用了哪些AI模型和编排框架,GreyNoise提到了OpenAI Codex和DeepSeek,但没公开技术细节(比如prompt设计、代理间通信协议、失败回退策略)。Blackpoint Cyber提到了"AI加速了exploit开发流程"但也没有给出代码层面的证据。
PaperCut漏洞本身的技术细节(CVE-2026-81578的具体HTTP请求结构、CVE-2026-82078的类加载payload构造)在公开报告中没有完全披露,本文基于Horizon3.ai和Huntress的技术分析做了合理推测,但不包含可直接利用的exploit代码。
12个被打到域管的组织的具体信息(行业、规模、AD配置)没有公开,无法做更细的归因分析。路径B(noPac漏洞)的前提是域内存在未修复的noPac,但这说明这些组织的AD维护本身就有问题。
"26秒11个组织"这个数字是攻击全面启动后的并行速度,不代表每个组织的完整渗透时间。完整渗透(从RCE到域管)最快也要7分钟。
本文没有覆盖防御方如何检测AI代理群攻击的实时行为模式(比如代理间的通信特征、并发请求模式),这需要更深入的网络流量分析。
这次攻击让我最在意的不是漏洞本身——PaperCut的漏洞链确实精巧,但认证绕过+类加载RCE的组合在Java Web应用中并不罕见。
让我在意的是AI代理群改变了攻击的经济学。
传统攻击者打一个目标要几个小时,打440个目标需要几周。AI代理群让这个时间压缩到了几个小时。攻击者不再需要坐在键盘前逐个操作,代理群可以并行执行、自动适应、自动回退。这意味着打440个目标的边际成本接近于零。
更让人不安的是路径C。12个组织被打到域管,其中一些是因为PaperCut以域管服务账号运行。这不是什么高级技术,这是配置问题。AI代理发现这个配置缺陷的速度,远快于运维团队修复它的速度。
但也不是没有好消息。Cloudflare WAF挡住了一例,台湾的基础硬化让12个被攻破的实例中0个被打到域管。这说明基础的防御措施——WAF规则、最小权限配置、限制公网访问——在AI时代依然有效。
AI加速了攻击,但没改变防御的基本原则。只是留给防守方的时间更少了。
本文基于GreyNoise于2026年9月9日发布的报告《Agents Gone Wild》及Blackpoint Cyber、Arctic Wolf的联合分析撰写。CVE技术细节参考Horizon3.ai和Huntress的公开研究。文中代码片段为防御性检测脚本,仅用于授权运维人员自查环境暴露面,不构成安全建议。攻击者IP(45.142.193.132)及入侵指标已在GreyNoise原报告中公开。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。