Author: Badcode and Longofo@Knownsec 404 Team
Date: 2020/02/09
Chinese Version: https://paper.seebug.org/1260/
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:
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.
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 toYumCapabilit
activationCondition
method:
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.
Pass the payload in Path of "createrepo"
.
You can see the execution result in the Status
column
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
.
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.
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.
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.
Now the incoming command has to meet three conditions
/createrepo
or /mergerepo
createrepo
or mergerepo
existsSeeing 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)
Pass the payload in Path of "createrepo"
.
View the process, notepad.exe
started
As you can see, the patch was successfully bypassed.
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:
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.
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
:
The front end is directly prohibited from modification, and the test is modified by capturing packets:
In YumCapabilityUpdateValidator.validate
breaks to:
It can be seen that this repair method can no longer be bypassed, unless the configuration file is overwritten by the file coverage, such as decompression and overwriting, but I was not found.
However, the place where Artifacts Upload can upload arbitrary files is still there. If the above situation appears in other places, it can still be used.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1261/