Author: Badcode and Longofo@Knownsec 404 Team
Date: 2020/02/09
Chinese Version: https://paper.seebug.org/1260/

### Foreword

At the beginning of September 2019, we responded to the Nexus Repository Manager 2.x command injection vulnerability (CVE-2019-5475). The general reason and steps for recurrence are on Hackerone. It was announced that after emergency response to this vulnerability, we analyzed the patch to fix the vulnerability and found that the repair was incomplete and could still be bypassed. This article records two bypasses of the vulnerability. Although the fix version was released twice early, the official second update announcement is too slow https://support.sonatype.com/hc/en-us/articles/360033490774, so now we post this article.

The timeline：

• CVE-2019-5475（2019-08-09）
• Bypassed for the first time, CVE-2019-15588 (2019-10-28)
• Bypassed for the second time, CVE was not assigned, and the bulletin impact version was updated (2020-3-25)

Note: The original vulnerability analysis, the first bypass analysis, and the second bypass analysis were mainly written by Badcode, the second bypass analysis+, and the latest version analysis was mainly added by Longofo.

### Original vulnerability analysis

#### Vulnerability analysis

The code analyzed below is based on version 2.14.9-01.

The vulnerability is in the Yum Repository plugin, when configuring Yum's createrepo or mergerepo

The code level will jump toYumCapabilitactivationConditionmethod:

The value set in Path of "createrepo" above will be obtained through getConfig().getCreaterepoPath(). After obtaining this value, call the this.validate() method on Path of "createrepo". The value set in will be obtained through getConfig().getCreaterepoPath(). After obtaining this value, call the this.validate() method

The path passed in is user-controllable, and then the path splicing --version is then passed to the commandLineExecutor.exec() method, which looks like a method of executing commands, and this is also the case. Follow up the exec method of the CommandLineExecutor class

Parse the command before executing the command. CommandLine.parse() will use spaces as separators to obtain executable files and parameters. Eventually, the call to Runtime.getRuntime().exec() executed the command. For example, the command passed by the user is cmd.exe /c whoami, and finally the method to getRuntime().exec() is Runtime.getRuntime().exec({"cmd.exe","/c" ,"whoami"}). So the principle of the vulnerability is also very simple, that is, when the createrepo or mergerepo path is set, the path can be specified by the user, the --version string is spliced halfway, and finally it is executed at getRuntime.exec() Order.

#### Vulnerability reproduction

Pass the payload in Path of "createrepo".

You can see the execution result in the Status column

### Bypass analysis for the first time

#### First patch analysis

The official patch has changed a few places, the key point is here

It is common practice to filter commands before executing them. A new getCleanCommand() method has been added to filter commands.

allowedExecutables is a HashSet with only two values, createrepo and mergerepo. First determine whether the command passed in by the user is in allowedExecutables, if so, directly splice params ie --version and return directly. Then determine the path of the command passed in by the user. If it starts with the working directory of nexus (applicationDirectories.getWorkDirectory().getAbsolutePath()), return null directly. Continue to judge, if the file name is not in allowedExecutables then return null, that is, this command needs to end with /createrepo or /mergerepo. After passing the judgment, the absolute path of the file is concatenated and returned by --version.

#### First patch bypass

To be honest, at the first glance at this patch, I felt that there was a high probability that it would be around.

The incoming command only needs to meet two conditions, not beginning with nexus' working directory, and ending with /createrepo or /mergerepo.

Seeing the getCleanCommand() method in the patch, new File(command) is the key, and new File() is to create a new File instance by converting the given pathname string into an abstract pathname. It is worth noting that spaces can be used in the path string, which is

String f = "/etc/passwd /shadow";
File file = new File(f);

This is legal, and the value obtained by calling file.getName() is shadow. Combined with this feature, you can bypass the judgment in the patch.

String cmd = "/bin/bash -c whoami /createrepo";
File file = new File(cmd);
System.out.println(file.getName());
System.out.println(file.getAbsolutePath());

operation result

It can be seen that the value of file.getName() is exactly createrepo, which satisfies the judgment.

#### Bypassing the test for the first time

##### Test environment
• 2.14.14-01 version
• Linux
##### Test procedure

Pass the payload in Path of "createrepo".

Check the execution result in the Status column

As you can see, the patch was successfully bypassed.

Under the Windows environment, it is a little troublesome. There is no way to execute commands in the form of cmd.exe /c whoami, because cmd.exe /c whoami becomes cmd.exe \c whoami after new File() , which cannot be executed later. You can directly execute the exe. Note that --version will also be spliced later, so many commands cannot be executed, but there is still a way to make use of the ability to execute any exe to carry out subsequent attacks.

### Second bypass analysis

#### Second patch analysis

After I submitted the above bypass method, the official fixed this bypass method, see the official patch

Added a file.exists() method in the getCleanCommand() method to determine whether the file exists. The previous form of /bin/bash -c whoami /createrepo would definitely not work, because this file does not exist. So now there is another judgment, and the difficulty has increased. Is there no way to bypass it? No, it can still be bypassed.

#### Second patch bypass

Now the incoming command has to meet three conditions

• Does not start with nexus' working directory
• End with /createrepo or /mergerepo
• And this file createrepo or mergerepo exists

Seeing file.exists(), I remembered file_exists() in php. I also encountered this kind of judgment when I was doing php before. There is a system feature. In the Windows environment, directory jumps are allowed to jump to non-existing directories, while under Linux, you cannot jump to non-existing directories.

have a test

Linux

As you can see, file.exists() returned false

Windows

file.exists() returned true

Above we said new File(pathname), pathname is allowed with spaces. Using the features of the above WIndows environment, set cmd to C:\\Windows\\System32\\calc.exe \\..\\..\\win.ini

After the parse() method, finally getRuntime.exec({"C:\\Windows\\System32\\calc.exe","\\..\\..\\win.ini"}) , So that you can execute calc.

In the above test, "win.ini" is a file that does exist. Returning to the patch, you need to determine whether createrepo or mergerepo exists. First of all, from a functional point of view, the createrepo command is used to create a yum source (software repository), that is, to index many rpm packages stored in a specific local location, describe the dependency information required by each package, and form metadata. That is, this createrepo is unlikely to exist under Windows. If this does not exist, there is no way to judge. Since createrepo does not exist on the server, I will try to create one. I first tried to find an upload point and tried to upload a createrepo, but I didn't find a point where the name would remain unchanged after uploading. After uploading at Artifacts Upload, it becomes the name of the form Artifact-Version.Packaging. Artifact-Version.Packaging does not satisfy the second judgment and ends with createrepo.

At the beginning, when I saw file.exists(), I entered the mindset, thinking that it was judged that the file exists, but after reading the official documentation, I found that the file or directory exists. This is the second key point caused by this vulnerability. I can't create files, but I can create folders. When uploading Artifacts in Artifacts Upload, it can be defined by GAV Parameters.

When Group is set to test123, Artifact is set to test123, and Version is set to 1, when uploading Artifacts, the corresponding directory will be created in the server. The corresponding structure is as follows

If we set Group to createrepo, then the corresponding createrepo directory will be created.

Combine two features to test

String cmd = "C:\\Windows\\System32\\calc.exe \\..\\..\\..\\nexus\\sonatype-work\\nexus\\storage\\thirdparty\\createrepo";
File file = new File(cmd);
System.out.println(file.exists());
System.out.println(file.getName());
System.out.println(file.getAbsolutePath());

As you can see, file.exists() returned true, and file.getName() returned createrepo, both of which met the judgment.

Finally, in getRuntime(), it is probably

getRuntime.exec({"C:\Windows\System32\notepad.exe","\..\..\..\nexus\sonatype-work\nexus\storage\thirdparty\createrepo","--version"})

Can successfully execute notepad.exe. (The calc.exe demo cannot see the process, so replace it with Notepad.exe)

#### Second bypass test

##### Test environment
• 2.14.15-01 version
• Windows
##### Test procedure

Pass the payload in Path of "createrepo".

View the process, notepad.exe started

As you can see, the patch was successfully bypassed.

### Second bypass analysis+

After the second bypass analysis by @Badcode, you can see that you can successfully execute commands on the Windows system. But there is a big limitation:

1. nexus needs to be installed on the system disk
2. Some commands with parameters cannot be used

The above-mentioned "Artifacts Upload" upload location can upload any file, and the uploaded file name is obtained by splicing with custom parameters, so you can guess. Then you can upload any exe file you wrote.

#### Second bypass analysis + test

##### Test environment
• 2.14.15-01 version
• Windows
##### Test procedure

Navigate to Views/Repositories->Repositories->3rd party->Configuration, we can see the absolute path of default local storage location (the content uploaded later is also in this directory):

Navigate to Views/Repositories->Repositories->3rd party->Artifact Upload, we can upload malicious exe files:

The exe file will be renamed to createrepo-1.exe (spliced by custom parameters):

Also pass the payload into Path of "createrepo" (at this time, please note that the previous part starts with the nexus installation directory, which will be judged in the patch, so you can add ..\ at the top level or Get a false layer aaa\..\ etc.):

You can see that createrepo-1.exe has been executed:

After the second patch was bypassed, the official fixed it again. The official patch is as follows:

Removed the previous repair method and added the YumCapabilityUpdateValidator class. In validate, the obtained value and the value set in the properties are verified using absolutes for equal equality. This value can only be modified through sonatype-work/nexus/conf/capabilities.xml:

In YumCapabilityUpdateValidator.validate breaks to: