Dissecting and MITMing Duo Device Health App

Chaim Sanders
7 min readSep 7, 2022

--

Every week, almost without fail, I come across one thing that confuses, entertains, or most commonly infuriates me. I’ve decided to keep a record of my adventures.

I must admit, last week’s blog post was a bit — blasé — for me. The real reason I wrote it was to lay the foundation to talk about ‘Zero Trust Applications’ like #okta Verify and #cisco #duo Device Health. So now, we’re gonna talk a bit more about Duo Device Health in quite a bit of detail

The Background

First and foremost, a little quibble: ‘policy enforcement’ versus ‘zero trust’. To me, Zero Trust is an architecture decision that necessitates policy enforcement on a frequently basis. Therefore, even though I’ve referred to these tools generally as Zero Trust Applications, going forward we can be more precise and call them policy enforcement tools.

So for those who stay current, in my last post, I outlined a new method for detecting reverse proxy based phishing. This approach worked by ensuring that the request inbound from a policy enforcement application came from the same IP as the authentication request.

This week we’ll show how these applications can be dissected and how, with Duo at least, these applications can be spoofed to work around my detection methodology and of course bypass security checks in general (uh oh).

The Problem

While both your browser and the policy enforcement application reside on the same host, there is a trust boundary between them. If there is no additional shared secret between the policy enforcement application and the policy enforcement engine to validate the user/device, then the target is open to being MITM’d. By default, Duo Device Health does not require this shared secret.

Assuming the above is true (it is), we should be able to make our own Duo Device Health client. The only thing that would be (and is) required is knowing the URL and the format of the request sent by the client to Duo. With this info in mind, an attacker can strip the Duo Device Health request from the reverse proxied JavaScript and pass their own, independent, spoofed response from the proxy machine to bypass my detection methodology.

Duo Device Health is a standard Swift application on macOS. This means we have several options for reverse engineering it. I tend to be more networking inclined, so my first stop was to figure out how IPC worked. A simple command such as the following will provide back the ports the service is listening on (we could also use the browser to inspect this):

sudo lsof -i -P | grep LISTEN | grep :$PORT

Duo generally seemed to use 53100, but other ports were possible given the status of that port. From there, our trusty friend Wireshark was able to fess out the request (HTTP) from Duo MFA check to the policy enforcement app (URLDecoded for viewing):

http://127.0.0.1:53100/report?txid=3483a784-3903-427e-88f6-81479af25da9&eh_service_url=https://2.endpointhealth.duosecurity.com/v1/healthapp/device/health?_req_trace_group=2220ed89df93f872be9b2371_26aa93004201d9a7609ad0ba&desktop_session_key=mfa-DA6O76PIKRG6QMJY3LW9-DU1EWDWA4VW0QTDS8J9L&_=1662145322968

If you’re like me, the eh_service_url, made you excited. I thought, perhaps I could just modify the URL and Duo Device Health app would provide its super-secret content to an arbitrary location. Given that this request is in plain HTTP, we can just use Burp to see if we get lucky.

Modifying the eh_service_url parameter ended up in a HTTP/1.1 400 Bad Request. Drat. Oh well, I guess we have to whip out Hopper. A quick string search for the domain leads us to a regex:

^https:\/\/.*[.]duosecurity[.]com$

This ends up relying on Swift’s URL() (Authors note: This may prove to be an area for a future vulnerability) and didn’t provide us a quick win.

Our second approach would require more fully reverse engineering the application (which I was hoping to avoid) or using library interposition. I tend to be lazy and library interposition can be quick and on MacOS interposition is easy. In this case, we know that ultimately most network code is gonna use write(). I borrowed this general interposition approach from [1].

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#define INTERPOSE(_replacement, _replacee) \
__attribute__((used)) static struct { \
const void* replacement; \
const void* replacee; \
} _interpose_##_replacee __attribute__ ((section("__DATA, __interpose"))) = { \
(const void*) (unsigned long) &_replacement, \
(const void*) (unsigned long) &_replacee \
};
ssize_t my_write(int fd, const void *buf, size_t count){
ssize_t returned = write(fd, buf, count);
printf("\n-------WRITE:\n%s\n-----------\n",buf);
return (returned);
}
INTERPOSE(my_write, write);

Running this is simple:

$ clang  -arch arm64e -dynamiclib inject.c -o inject.dylib
$ DYLD_INSERT_LIBRARIES=inject.dylib /Applications/Duo\ Device\ Health.app/Contents/MacOS/Duo\ Device\ Health

This yielded some information (authors note: running read() interposition will show information on what files are being read to perform some of the checks) but no sign of the HTTP request(s). Oh well, back to Hopper.

A quick scan through the available procedures shows us what our issue was. AlamoFire is an included library. This library is commonly used for handling HTTP requests.

One of the nicer parts of Mach-O packed Swift applications is that these libraries can be swapped out. Opening up Duo Device Health.app, we can find the info.plist (/Applications/Duo Device Health.app/Contents/Frameworks/Alamofire.framework/Resources/Info.plist) for AlamoFire and we find that its using quite an outdated version (3 years out of date, oh dear).

Downloading and compiling that archaic version with XCode is pretty straight forward. Now all that was left was patching our desired functions. Fortunately, Hopper also provided us with a list of functions that we know are going to be called by Duo Device Health.

We have a pretty good idea that data is being sent, so it must be encoded somehow, JSON is a good guess, especially given we see some JSON encoding functions in use. I started by modifying all the encode functions, since they’ll have body access, starting with JSONEncoding.encode(). This turned out to be a good guess, as I was greeted pretty quickly with the body of the content (cut down for view-ability)

Now that I have the format of the request, I am able to successfully extract the txid and eh_service_url from the initial Duo request and provide a valid spoofed response to Duo without the client ever being the wiser. Please note that some values are boolean (firewall status, etc.) and some have only fixed values (Security Agent, etc.)

The Solution

I’ve made a Duo Device Health app impersonator in Python as a proof of concept. You can check it out at https://github.com/csanders-git/device-health-app-python.

By default Duo Device Health provides no authentication of the endpoint nor does it provide authentication of the data sent (it doesn’t even require HTTPS). Duo does support, trusted endpoints, which can be enabled, however it has some major shortcomings.

Duo trusted endpoints was previously designed to use certificates. This was a rather clumsy implementation as important aspects such as revocation were lacking. The issues were further compounded by two aspects: browser support, and Duo MFA implementation limitations.

In order to work around browser support issues Duo switched from certificates to using unique, sorta-non-guessable secrets (TM?). Unfortunately, as this secret is generated based on information on the endpoint, once compromised, a machine must carefully be reprovisioned as the secrets are linked to hardware. The following are the sources of the unique IDs used. This is the ONLY source of shared secrets currently not deprecated, so failure to use trusted endpoints AND enforce it for all services makes Duo Device Health insecure.

MacOS:
ioreg -d2 -c IOPlatformExpertDevice | awk -F\" '/IOPlatformUUID/{print $(NF-1)}'
Windows:
Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Cryptography -Name "MachineGuid"

This leads to our other problem. Duo really wants users to leverage their SSO platform, but most users will find that they have an existing SSO platform already in use (i’m being charitable here). This typically means that Duo will provide MFA in front of that entire platform (Okta for instance). This is a problem as Okta may protect many hundreds or thousands of apps. Any of those applications that don’t leverage a full JavaScript stack during login will fail all trusted endpoint checks and therefore force trusted endpoint checks to be turned off completely. Said another way, using trusted endpoint protections in front of a generic SSO provider is typically not feasible.

There is one other option, Duo Network Gateway. This is a self-hosted reverse proxy that needs to be put in front of each specific application route and will provide an additional auth check (yuck). Generally, this design limitation is a problem for Duo and one of the reasons they should be frightened by Okta OIE with Okta Verify and device policy.

In summary we showed that Duo Device Health is generally spoofable and supports insecure communication. We also discussed that the only method to prevent spoofing is trusted endpoint protections which generally can’t be applied in many/most situations. Oy.

Authors Note: There are actually some useful results of this work, for instance the native Duo Device Health app doesn’t support Windows Server, but extending my Python PoC I was quickly able to provide that capability, so, there’s that.

--

--