TL博士
Maven Central 发布的一个工件 io.github.davidtimur:c2-lab 共发布了九个版本,其中八个版本在编译任何将 jar 包放置在注解处理器路径上的下游项目时,都会执行远程访问有效载荷。应用程序代码无需导入该 jar 包,源代码也无需引用它。 没有安装脚本,没有安装后等效脚本,也没有任何生命周期钩子供工具检查。
执行向量是 jar 包内的一个 46 字节的文件:一个 Java 服务提供程序注册信息,其中指定了一个实现该接口的类。 javax.annotation.processing.ProcessorJava编译器会自动发现此类注册信息。一旦发现, javac 实例化并运行该类是编译过程中的正常步骤。这意味着有效载荷的运行时环境是构建机器,而此时构建机器正在执行它存在的唯一任务。
在九个版本中,命令与控制通道重建了三次:首先是操作员提供的回调 URL,然后是通过 ngrok TCP 隧道建立的反向 shell,最后是 HTTP 轮询通道,其路径又更改了两次。最后一个版本安装了一个空操作的 TLS 信任管理器作为 JVM 默认设置。 在编译期间接受任何证书.
该工件在其自身的 POM 文件中包含“C2 Lab Payload”字样,并采用 MIT 许可证。在我们观察的整个期间,它都存在于 Maven Central 上。 此后,该内容及其所有内容均已被移除。 io.github.davidtimur 组。
剖析:编译器即执行引擎
Java 的注解处理框架的存在是为了让库能够在编译时生成代码——这是 Lombok、Dagger 以及众多 ORM 和序列化工具背后的机制。处理器会通过 jar 包内的一个纯文本文件来声明自身:
META-INF/services/javax.annotation.processing.Processor
受影响的每个版本中该文件的内容,逐字完整地记录如下:
io.github.davidtimur.c2lab.C2Processor
该文件在 1.0.1 到 1.0.8 版本中字节完全相同,md5 值也相同。 7d2a08a5c8869a47eea9fa62487dfbe4. 1.0.0 版本不包含此功能。
日期 Java语言 运行时,它会扫描注解处理器路径,查找并加载找到的服务条目。被编译的项目中无需提及处理器、进行任何注解或配置。只要存在于路径中即可。这正是 JavacDoor 与大多数工具所基于的供应链模式之间的区别所在:
| 模式 | 触发端口 | 可见 |
|---|---|---|
| npm install hook | npm install | scripts.postinstall 在清单中 |
| Python 导入时有效载荷 | 首先导入模块 | 源中的模块级语句 |
| JavacDoor | javac 在任何下游项目上 | 服务注册文件名 |
安装钩子是清单文件中的一项声明,而清单文件是用户首先会阅读的内容。导入时的有效负载至少存在于可读的源代码中。服务注册则不然:它只是一个文件名加上一行类名,而实际的行为却存在于另一个目录下的编译字节码中。
有效载荷类本身通过调用外部服务来执行主机侦察。已编译类的常量池包含 / bin / sh的, WHOAMI, uname -a, PWD 在 Unix 路径上 任务列表 在 Windows 路径上,以及 重定向错误流 将子进程的错误输出合并到捕获的数据流中。两个 JSON 模板将结果从主机传输出去——一个注册信标:
{"host":"%s","os":"%s","user":"%s","dir":"%s"}
由…… 主机 命令和 操作系统名称, user.name和 用户目录 系统属性和结果回调:
{"version":"%s","host":"%s","time":"%s","output":"%s"}
编译器输出中留下的进度标记异常坦诚: [C2] 编译时执行完成, [C2] 回调已发送 → HTTP , [C2] shell 已连接到 .
九个版本,三代C2
这些发布版本并非同一有效载荷的九个副本,而是一份迭代日志。按顺序阅读这些日志,可以看到通道在重建过程中,而传输向量保持不变。
| 发布 | 编译时自动执行 | 频道 |
|---|---|---|
1.0.0 | 否(无服务文件) | 仅限运营商提供的回调 URL |
1.0.1 | 是的(引入了向量) | 运营商提供的回调 URL |
1.0.2 | 含 | 反向 shell,ngrok TCP 隧道 |
1.0.3 | 含 | HTTP 通道,/register /cmd /out |
1.0.4 | 含 | HTTP 通道,/register /cmd /out |
1.0.5 | 含 | HTTP 通道,/register /poll /out |
1.0.6 | 含 | HTTP 通道,/register /poll /out |
1.0.7 | 含 | HTTP 通道,/register /cmd |
1.0.8 | 含 | 已禁用 HTTP 通道和 TLS 验证 |
表格中有三个细节值得详细阐述,因为每一个细节都会改变对工件集的解读方式。
向量到达的是 1.0.1,而不是 1.0.2。 1.0.0 版本保留了相同的侦察和回调逻辑,但没有服务文件,也没有注解处理导入;它仅在被调用时运行。从 1.0.1 版本开始,服务文件已添加,并且编译后的类也已导入。 javax.annotation.processing.SupportedSourceVersion任何对两个已发布版本(1.0.0 和 1.0.2)进行比较的评估都会正确地得出结论:某些东西发生了变化,但却错误地指出了变化的位置。
反向 shell 仅存在于一个版本中。 版本 1.0.2 包含 0.tcp.ngrok[.]io日志行 [C2] shell 已连接到 0.tcp.ngrok[.]io:19823一个互动横幅 c2壳以及帧终止符 __结尾__从 1.0.3 版本开始,不再包含上述任何内容,而是连接到 HTTPS 主机。如果仅列出已提交工单的版本,则下架请求会指出已失效的 TCP 端点,而忽略了后续六个版本中存在的有效 HTTP 通道。
最终版本取消了传输验证。 1.0.8 版本新增了一个实现该实现的类。 javax.net.ssl.X509TrustManager 其证书检查方法不起作用,一个始终为真的主机名验证器通过以下方式注册 设置默认主机名验证器,以及 信任所有 该例程会将两者安装为 JVM 的默认设置。其效果是,在编译的剩余时间内,JVM 会接受来自任何主机的任何证书——不仅适用于有效载荷自身的流量,也适用于之后构建过程中通过 TLS 进行的任何其他通信。
在所有六个使用该通道的版本中,HTTP 通道都由单个主机进行前端连接: tableful-fervor-crazed.ngrok-free[.]dev每个请求都带有标头 ngrok-skip-browser-warning这会抑制免费 ngrok 隧道提供给浏览器的插页。路径集在不同版本之间会发生变化。 /出去 在 1.0.7 版本中消失,并且 /命令 交替 /轮询 但主持人始终不变。
自我排除说明
从 1.0.1 版本开始,每个 POM 文件都为工件自身的构建设置了一个编译器参数:
-proc:无
该标志会禁用注释处理。它在这里的作用是预先处理。cise:当编译包含处理器的项目本身时,处理器不会运行。
这个标志的用途非常常规。一个包含注解处理器的项目通常需要在引导过程中避免将该处理器应用于自身,构建工具的文档也明确建议这样做。单独来看,它并不能证明任何事情。
结合处理器的运行机制来看,这描述了一种特定的不对称性:代码会在所有编译该工件的用户机器上执行,而不是在构建该工件的机器上执行。该标志出现在引入服务文件的同一版本(1.0.1)以及之后的每个版本中。“编译时执行开始的版本”和“本地关闭编译时执行的版本”之间的关联是工件集中最有用的分析信号,并且无需反编译任何内容即可在纯文本 POM 文件中看到它。
我们注意到这一影响,然后就此打住。没有任何证据表明设置该标志的原因。
还有一条元数据值得一提,主要是为了说明它的作用。POM 将项目命名为“C2 Lab Payload”,将其描述为“C2 lab payload artifact”,并使用 MIT 许可证。这种自我标注有时被用作证明某个软件包是研究成果的证据。cis与其说是实时威胁,不如说是实际存在的威胁。有时这种解读是正确的——一个声明为“金丝雀”但没有可访问基础设施的版本与此不同。但这并不适用于此。一个发布到公共代码库、任何使用者均可访问的功能性植入程序,其出站基础设施在九个版本中重建了三次,无论其元数据如何命名,它都是一个实际存在的功能。POM 文件中的名称不会改变针对该程序编译的机器上发生的情况。
构建机器的指标
如果构建主机针对此工件进行编译,则证据存在于构建日志和网络遥测中,而不是磁盘上的持久植入物中——有效载荷在编译器进程内运行并随其退出。
在 jar 或本地存储库缓存中
META-INF/services/javax.annotation.processing.Processor命名io.github.davidtimur.c2lab.C2Processor- 服务文件 md5
7d2a08a5c8869a47eea9fa62487dfbe4 - 下属类别
io/github/davidtimur/c2lab/:C2Processor,C2Task,Task,并且在 1.0.8 版本中,内部类Task$1
构建输出
[C2] compile-time execution complete[C2] callback sent → HTTP[C2] callback failed:[C2] shell connected to- 互动横幅
c2-shell; 帧终止符__END__
过程遥测
javac作为父母/bin/sh -c(Unix)或 Windows 命令解释器- 子命令
whoami,uname -a,pwd(Unix)或tasklist(Windows)由编译步骤控制
在网络遥测中
- 出站 TCP 到
0.tcp.ngrok[.]io:19823(版本 1.0.2) - HTTPS 到
tableful-fervor-crazed.ngrok-free[.]dev路径/register,/cmd,/poll,/out(版本 1.0.3 至 1.0.8) - 请求头
ngrok-skip-browser-warning: true - 请求体匹配
{"host":...,"os":...,"user":...,"dir":...}or{"version":...,"host":...,"time":...,"output":...}
在配置中
- 环境变量
CALLBACK,CALLBACK_URL系统属性callback.url
发布者元数据
- 团队
io.github.davidtimur出版商地址davudboi999@gmail[.]com签名密钥E520C345EF94423D
运行过 1.0.8 版本的构建需要额外检查一项。因为该版本默认将宽松的信任管理器设置为 JVM 全局设置,所以之后在同一 JVM 中建立的任何 TLS 连接(例如依赖解析、工件上传、部署步骤)都不会进行证书验证。因此,不应假定来自该时间段的流量已通过身份验证。
为什么编译后的产物需要不同的扫描方式
JavacDoor 是一个有用的测试用例,因为它同时推翻了两个常见的假设,而且这两个失败都不是某个特定供应商的工具所特有的。
第一个假设是危险代码会在清单文件中表明自身存在。 大量的供应链工具都是围绕生命周期组织的。 hooks因为对于 npm 和 PyPI 来说,这通常是操作发生的地方。JavacDoor 没有钩子。它的触发器是一个服务注册文件,该文件的名称是一个 Java 接口,内容是一个类名。要静态捕获它,你必须处理 META-INF/services/javax.annotation.processing.Processor 作为一个独立的执行入口点,它与……不相上下 安装后 脚本——然后沿着命名的类找到字节码。生态系统都有自己类似的自动发现机制,无论工具是否将其枚举为入口点,每个机制都是一个入口点。
第二个假设是字符串存在于源文件中。 对于一个 jar 文件,端点、shell 命令、JSON 模板和日志标记都位于常量池中。 。类 文件。使用 grep 命令搜索文本的工具找不到任何内容——并非因为字符串被混淆,而是因为它们位于结构化的二进制容器中,文本扫描无法解析。本文中的所有网络指标都来自常量池解析。其中一个是完整的 https:// URL 明文显示在类文件中;即使扫描 jar 包的可读内容,也无法找到它。这里不存在需要破解的编码,只需要读取容器格式即可。
这两个缺陷的形式相同:工件格式被视为一堆文件,而不是具有明确语义的结构。解决方法并不高明——解析容器,枚举生态系统自身的自动发现入口点,并追踪这些入口点直至编译后的代码。对于构建时向量,第三种检查方法成本低廉且诊断效果显著:比较工件对使用者所做的操作与其自身豁免的操作。如果一个工件注册了一个编译时处理器,同时又禁用了自身构建的编译时处理,那么仅仅两行纯文本 POM 就足以说明问题。
对于目前使用 Maven 制品的团队来说,以下三点是切实可行的:
- 将注解处理器路径视为执行边界。 依赖项会在你的构建过程中运行代码。如果构建不需要注解处理, -proc:无 在防御方面,它与本地显然一样有用;在它起作用的地方,显式地固定处理器集,而不是从编译类路径继承它。
- 记录编译器子进程。 在大多数项目中,编译步骤生成 shell 是异常的,很容易发出警报。
- 不要将元数据视为证词。 软件包名称或描述中的“Lab”、“test”、“payload”和“PoC”并非范围限制。可访问性和行为才是。
该文物及其所有组成部分均被移走。 Maven的 根据我们的报告,Central 已停止响应;现在工件路径和组路径均返回 404 错误,并且 Central 索引报告没有匹配的坐标。这导致此工件已关闭。但不会关闭向量,向量是 Java 编译器的一个已记录的特性,任何发布 jar 包的人都可以使用。
案例
本文未引用任何外部资料。所有结论均基于对已发布的九个 JAR 包的第一手静态分析,这些 JAR 包是从 Maven Central 仓库移除前获取的。分析过程中未执行任何来自这些 JAR 包的代码。







