Jenkins文件读取漏洞拾遗(CVE-2024-23897)

Jenkins 未授权文件读取漏洞(CVE-2024-23897)今天闹得沸沸扬扬,我也来简单分析一下这个漏洞,并看看这个文件读取如何利用。

首先说的是,由于Jenkins存在版本和插件差异,所以利用时可能也有不一样之处,本文内容不一定适用于所有Jenkins server。我们这里使用Vulhub的环境(2.441)来做分析和演示:https://github.com/vulhub/vulhub/tree/master/jenkins/CVE-2024-23897

0x01 漏洞原理

漏洞的原理比较简单,本文就不以跟代码记流水帐的形式分析代码了,文字描述一下。

Jenkins提供了一个命令行的接口,用户可以下载一个命令行客户端jenkins-cli.jar到本地,并调用该客户端来执行一些Jenkins的功能。本来是一个很常见的功能,但设计中神奇的是,用户使用jenkins-cli.jar时,命令行是传到服务端解析的,而不是在jenkins-cli.jar里解析。

这就导致了一个问题,因为Jenkins服务端解析命令行时使用了一个第三方库args4j,这个库实现了Linux中一个常见的功能——如果一个参数是以@开头,则会被自动认为是一个文件名,文件内容会被读取作为参数。

星球里以前讨论过类似的问题,比如curl参数注入中可以利用@来读取和上传文件:https://t.zsxq.com/nAyFuBi

PHP5.5以前,设置POST参数时,如果传入的参数值是@开头,则也会被认为是文件名,并读取该文件,造成文件读取漏洞。在PHP 5.5以后增加了CURLOPT_SAFE_UPLOAD选项并默认为true,用以杜绝这个问题:https://t.zsxq.com/YVbIYny

回到Jenkins,因为Jenkins是在服务端解析的命令行参数,又支持args4j的这个特性,所以造成文件读取漏洞。

要调试该漏洞,只需在Jenkins启动时设置一个环境变量“DEBUG=1”,即可开启JVM的远程调试端口。如果使用Vulhub环境,直接连接5005端口进行远程调试,方法曾在这个帖子里介绍过:https://t.zsxq.com/14N7DfqYr

0x02 Jenkins权限系统

正常情况下,cli这个功能的权限控制方案和Jenkins默认权限方案相同。

Jenkins有几种鉴权方案:

  • Anyone can do anything 没有任何权限认证,匿名用户即可登录后台执行Groovy脚本
  • Legacy Mode 旧鉴权模式
  • Logged-in users can do anything 任意登录的用户可以做任何事,这是默认的权限选项
  • Matrix-based security 细颗粒度权限控制,需要安装插件才支持这个鉴权模式

默认安装后是第3种方案,非常简单,没有垂直权限的区分,登录后的用户就是管理员,没有登录的用户就是匿名用户。

这种权限方案中,管理员可以设置“匿名用户可读”选项:

image.png

这个选项在后台的“Manage Jenkins” -> “Security” 中管理员可以将其开启或关闭,默认是关闭的。实际测试中,如果我们访问Jenkins首页,发现强制跳转到登录页面,或者啥功能都没有直接报权限错误,就说明关闭了“匿名用户读”选项。

如果Jenkins系统关闭了匿名用户可读功能,大部分的cli命令也就无法调用,会出现"ERROR: anonymous is missing the Overall/Read permission"的错误:

image.png

但有2个命令例外,就是help和who-am-i。这两个命令是无需任何权限的,所以可以用来读取文件,比如:

image.png

在Vulhub环境中,“匿名用户可读”是开启的,某些Jenkins版本默认安装时可能也是开启的,但通常管理员会关闭这个功能。另外,大部分企业的Jenkins会安装“Matrix-based security”这样的插件来管理权限,也会影响“匿名用户可读”选项的值。总而言之,这个选项的开关取决于管理员是否想让未登录用户看到一些Jenkins的任务。

总结一下就是:

  • 当Jenkins开启了“匿名用户可读”功能,大部分命令都可以被调用
  • 当Jenkins关闭了“匿名用户可读”功能,只有help和who-am-i命令可以被调用

经过@p1g3 的纠正和长亭的预警文章,其实Jenkins中不止只有help和who-am-i两个命令可以被匿名调用,还有下图中的6个写入相关命令。原因和命令使用的加载类有关,我分析代码的时候由于只看了CLICommand的代码所以忽略了:
image.png

0x03 如何读取想要的内容

前面说了,Jenkins文件读取漏洞的原理是args4j在解析命令行的时候会把@后面的字符作为文件名,并读取文件内容作为参数的值。

但是作为攻击者,我们必须想办法让Jenkins或args4j,将读到的文件内容返回给我们,才能最终达到任意文件读取的目的。好在,当我们调用命令行时,如果出错,args4j就会把错误返回给客户端,而错误信息中就包含文件的内容。

但这里有个问题是,如果我们使用@将文件内容加载成命令行参数,则会遇到一个棘手的问题——文件中的空白字符,会分割参数。比如,如果有一个文件内容有4行:

1
2
3
4

那么,命令行解析时会变成下面这个样子:

java -jar jenkins-cli.jar -s http://localhost:8080/ -http help 1 2 3 4

此时,1、2、3、4会变成help命令的4个参数。而解析命令行时通常只会取前面一些参数,报错也就只会把这些参数报出来,导致我们读取文件的时候只能读到文件的前几行。

比如,使用help命令,能读到文件的前2行:

image.png

使用keep-build命令,可以读取文件第3行:

image.png

但此时会有一个有趣的问题:如果一个文件只有一行(如/var/jenkins_home/secret.key 和 /var/jenkins_home/secrets/master.key),使用help读取就会出现“ERROR: You must authenticate to access this Jenkins.”的错误:

image.png

前面不是说调用help命令可以不需要权限吗?实际上,help命令会先检查命令的参数个数是否正确,后再检查权限,如果我们能让检查命令参数个数的步骤就出错,就能正常返回错误信息了。

所以,可以人为地给help命令后增加一个新参数,比如:

java -jar jenkins-cli.jar -s http://localhost:8080/ -http help 1 "@/var/jenkins_home/secret.key"

此时help后有两个参数,参数个数太多导致出错,第二个参数被爆出,就是我们要读的文件内容:

image.png

读一两行这种做法终究比较鸡肋,是否有命令可以读取完整文件内容呢?

其实,前面我也说了,如果Jenkins开启了“匿名用户可读”选项,则大部分命令都可以被调用,其中包括connect-node命令和reload-job命令。这俩命令可以用来读取文件全部内容:

image.png

0x04 了解Jenkins敏感文件

知道了如何读取文件,那么我们在实战中应该读取哪些文件呢?

如果部署Jenkins使用的是官方或Vulhub的Docker镜像,那么Jenkins的数据根目录是在/var/jenkins_home。但并不是所有人都会使用这个目录作为根目录,如果想读取Jenkins相关的文件,必须先知道Jenkins的根目录在哪里。

这时可以读取/proc/self/environ和/proc/self/cmdline,这两个文件中包含的是当前进程的环境变量和cmd启动命令行,其中就会包含Jenkins的根目录。

image.png

拿到了Jenkins的根目录,我们就可以来尝试读取下面这些敏感文件。

  • /var/jenkins_home/users/*/config.xml
  • /var/jenkins_home/secret.key
  • /var/jenkins_home/secrets/master.key
  • /var/jenkins_home/secrets/org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac

在后面对代码的分析中,我们能慢慢地了解这些文件的具体作用。

0x05 读取用户密码

默认未使用第三方登录的Jenkins中,用户相关信息是存储在文件中,而Session信息是存储在内存中。所以,在拥有文件读取漏洞后,首先想到的就是是否可以读取用户密码。

我们先读取/var/jenkins_home/users/users.xml,可以获取用户列表和每个用户信息所在的文件目录:

image.png

我画红框的就是admin用户所在目录,我们读取这个目录下的config文件:/var/jenkins_home/users/admin_2676914209327834400/config.xml

image.png

可见,我们拿到了三个重要信息:

  • 用户密码,使用JBCript哈希编码
  • 用户Token,使用sha256哈希编码
  • 用户种子,这个后面会用到

第一个,用户密码是以#jbcrypt前缀开头,但实际上调试可发现这就是一个BCrypt算法计算出来的hash值。我们将这个哈希值前面的#jbcrypt:去掉,并将第一个$2a改成$2y,就成为一个标准的bcrypt哈希值了。

由于我们已知管理员的密码是字符串“vulhub”,我们计算一下它是否能对应上这个bcrypt哈希值:

image.png

能够对应上,说明算法是bcrypt。对于这类hash值,我们只能硬跑破解,运气好可以跑出来明文。

第二个,用户Token。这个值并不是每个用户都有,通常只有用户生成了Token才会有。而且储存在文件中的Token是原始Token经过sha256后的结果,无法还原出原始Token,所以利用不了。

第三个,用户种子,这个值在Remember-Me Cookie中会用到。

那么,说到Remember-Me Cookie,Jenkins的Remember-Me Cookie是怎么工作的呢?

remember-me Cookie的生成与校验是在hudson.security.TokenBasedRememberMeServices2#makeTokenSignature函数中,这个函数代码很简单:

image.png

可以看见,由用户名、Token过期时间戳、userSeed、Key四个部分拼接组成的字符串,被使用Mac.mac函数签名以后,成为最后的Token。

这里就带来几个问题:

  • userSeed是什么?
  • Key是什么?
  • Mac.mac函数是什么算法?

我们在函数尾下断点,可以发现Key就是/var/jenkins_home/secret.key的内容:

image.png

这里的userSeed是前面读取的用户种子,Key是前面读取的secret.key文件的值,这两个值都可以通过漏洞读取到。

跟进Mac.mac函数后可以发现,最后实际上是使用了javax.crypto.Mac来计算hmac,hmac算法使用的salt来自于下面这个函数:

image.png

可见,这里首先从/var/jenkins_home/secrets/org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac文件中读取出一个密文,并使用AES解密,解密时使用的密钥来自于masterKey

这个masterKey的名字好像比较熟悉?确实就和前面读取的/var/jenkins_home/secrets/master.key有关。master.key文件的内容,使用Util.toAes128Key转换成适合AES128算法长度的Key,就是AES密钥:

image.png

所以,最后所有的敏感文件作用都屡清楚了:

  • /var/jenkins_home/users/*/config.xml 保存所有用户的信息,包括密码、种子、Token等
  • /var/jenkins_home/secret.key 保存Remember-Me Cookie中的一部分
  • /var/jenkins_home/secrets/master.key 作为AES解密密钥
  • /var/jenkins_home/secrets/org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac 作为计算hmac签名时的Key

这里面,前三个文件都可以直接读取,但读取第四个文件时我遇到了问题。

0x07 二进制文件读取问题

第四个文件是一个二进制文件,我们对文件读取的数据包抓包,可以拿到最原始的返回数据:

image.png

可见,这个返回数据里有大量的ef bf bd,这就是UTF-8字符集无法识别时的“占位符”。我曾在星球里分享过Burpsuite发包时遇到过类似问题:https://t.zsxq.com/NzfuJ2r,可以复习一下。

也就是说,读取二进制文件时会因为非UTF-8字符集的问题导致有大量字符被“占位符”替代,无法读到完整的hmac salt。

官方漏洞通告里也提到了这个问题:

For example, attempting to read random binary data using UTF-8, roughly half of all bytes will be replaced with a placeholder for an illegal value. For 32 byte random binary secrets, as commonly used in Jenkins for HMAC-SHA256, this would require attackers to correctly guess on average 16 bytes, which is infeasible. In contrast, with the encoding Windows-1252, only 5 out of 256 possible values are illegal and would be replaced with a placeholder.

在UTF-8字符集情况下,32字节长度的二进制字符可能有16个字节都是占位符;而如果是Windows-1252字符集,则256个字符中只有5个字符是占位符。

而正常来说,Linux系统下部署的Jenkins都是默认的UTF-8字符集,而部分Windows下部署的Jenkins可能会是其他字符集。如果按照16:32这个比例来看,当前环境是根本无法正确读到二进制文件的。

那么,是否有办法在客户端设置字符集呢?

我在抓cli请求包的时候,关注到协议中是存在一个字符集的设置的:

image.png

阅读代码可以发现,这个字符集可以通过Java客户端的file.encoding来配置。我尝试将其设置为UTF-16BE:

java -Dfile.encoding=UTF-16BE -jar jenkins-cli.jar -s http://localhost:8080/ -http who-am-i "@/var/jenkins_home/secrets/org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac"

此时再抓包查看返回:

image.png

可以发现,返回的错误信息确实变成UTF-16编码了,但其中仍然包含大量fd ff这样的占位符,并不能实现读取二进制文件内容的目标。

我尝试了Java中其他的编码,都是相同的问题。我们看下args4j中解析@符号的代码,可以发现其使用的是new BufferedReader(new FileReader(f))来读取文件:

image.png

查看FileReader函数的文档可以看出,如果其构造函数不传入第二个参数,会使用默认的charset打开文件。而默认charset就是当前服务端的file.encoding设置。因为这部分代码在args4j中,在读取文件的时候就已经使用UTF-8编码,导致二进制信息的丢失,后续客户端传入的charset是什么都不会影响返回结果中的占位符了。

那么实际上伪造remember-me这条路就死了,就像官方漏洞通告里说的,目标环境需要支持读取二进制文件(比如默认字符集是Windows-1252的Windows系统)。

0x08 总结

分析了这么多,大概核心的内容都已经说明了。总结一下,CVE-2024-23897漏洞的利用有下面两个比较核心的限制:

  • 是否开启“匿名用户可读”选项
  • 服务端字符集是否兼容读取二进制文件

第一个问题的结果会影响攻击者是否能够读取文件的全文,包括用户的密码等信息;第二个问题的结果影响攻击者是否能够伪造任意用户的remember-me Cookie。

当然,还是最开始说的,本文仅限于Vulhub环境中的Jenkins版本(2.441)。其他环境的Jenkins的相关细节会有许多不同,比如,旧版本Jenkins用户密码是加密而不是哈希,可以利用诸如jenkins-decrypt这样的工具来解密密码,还可以利用一些插件来进一步攻击。这部分细节就留给大家自行深入研究了。

赞赏

喜欢这篇文章?打赏1元

评论

Z 回复

时不时来这里转转,看看有没有新的文章发布。

phithon 回复

@Z 可以RSS订阅一下

qiang 回复

tql P神。

captcha