new article

This commit is contained in:
Daniele Linguaglossa 2026-07-27 11:20:23 +02:00
parent 7c01ed1054
commit 398ae5fc33
11 changed files with 137 additions and 1 deletions

15
.gitattributes vendored Normal file
View File

@ -0,0 +1,15 @@
# Force LF line endings in the working tree for everyone, always.
# This project embeds text resources into an ELF shared object and builds
# on Linux — CRLF must never leak into any of it.
* text=auto eol=lf
# Opaque files: never touch, never warn
*.jpg binary
*.jpeg binary
*.png binary
*.gif binary
*.webp binary
*.ico binary
*.o binary
*.so binary
*.min.js -text

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ src/challenge.c
res/pages/index.md res/pages/index.md
tools/bin/* tools/bin/*
src/include/* src/include/*
design-previews
.argus/

View File

@ -0,0 +1,113 @@
---
title: Virtual Camera, Real SYSTEM
description: How a fake webcam gave me real SYSTEM - same plantable CrossDevice DLL as calif.io's Dark Elevator, brand new ride through the Windows virtual camera infrastructure, still working on fully patched Windows 11
author: DZONERZY
date: Monday, 27 July, 2026
unlisted: true
---
# AI 1 - Humans 0
![Screenshot of the calif.io Dark Elevator blog post](../assets/calif-dark-elevator.jpg)
A few weeks ago the folks at **calif.io** started dropping a bunch of really cool writeups about vulnerabilities they found *with the help of AI*. Their latest one, [Dark Elevator](https://blog.calif.io/p/dark-elevator-windows-install-service), is a beautiful Windows 11 LPE: a normal user gets an interactive `NT AUTHORITY\SYSTEM` shell, no admin, no UAC, no reboot, the full package. Microsoft fixed it on July 14th as **CVE-2026-50343**.
Now, huge respect, 'cause that's a clean logic-flaw chain and a great read. But it also left me with that itchy feeling every offensive researcher knows... if the AI found one bug in that area, what else is lying around?
So I did what any reasonable person would do on a weekend, I opened a disassembler and went down the rabbit hole. Spoiler: I came back with my own ride to SYSTEM, on **fully patched** Windows 11 24H2 and 25H2. Same plantable DLL at the heart of it, completely different elevator.
This is the story.
# Their bug, in 30 seconds
![Regedit showing the CrossDevice CLSID InprocServer32 pointing to C:\ProgramData](../assets/crossdevice-inproc-dll.jpg){class="zoomable"}
All credits to calif.io for the original find, but let me do a quick recap so we're on the same page. Their exploit chained two logic flaws:
1. **A writable plugin map.** InstallService (the SYSTEM service behind app installs) picks a COM class to load by reading a registry key, `StaticPluginMap`, that any normal user can write. Write your own CLSID there and the SYSTEM service will `CoCreateInstance` whatever you point at.
2. **A user-plantable COM server.** Windows ships a CrossDevice streaming component, CLSID `{E9F83CF2-E0C0-4CA7-AF01-E90C70BEF496}`, whose `InprocServer32` points to `C:\ProgramData\CrossDevice\CrossDevice.Streaming.Source.dll`. The registration lives in `HKLM` so you can't touch it... but the DLL it points to **doesn't exist**, and `C:\ProgramData` is a place where any standard user can create directories and files.
Plant the DLL, poke the service, and your code runs inside a SYSTEM `svchost.exe`. Elegant, deterministic, no memory corruption. Chef's kiss 😀.
# The real treasure was the DLL
![Test-Path showing the CrossDevice DLL is missing](../assets/dll-missing.jpg){class="zoomable"}
Reading their post, the thing that kept my attention wasn't InstallService, but that weird stale COM registration: a **machine-wide** HKLM registration, protected from modification, pointing to a file that doesn't exist in a directory any user can create. If that's not a bug, it's at least a very generous *gift*.
Microsoft's fix for CVE-2026-50343 took care of the InstallService side of the story. But that CrossDevice registration is still there, still pointing to a missing DLL in a path any user can create. Fully updated system, mind you.
So I asked myself, InstallService can't be the *only* privileged process willing to load that class, right? Who else wants my DLL? I went hunting for another elevator.
# Say cheese!
![Decompiler view of VerifyCOMServerByClsid in mfsensorgroup.dll](../assets/mfsensorgroup-ida.jpg){class="zoomable"}
Turns out Windows has a whole infrastructure for **virtual cameras** (the thing apps use to expose fake webcams), and it comes with a public API, `MFCreateVirtualCamera`, that lets you create a camera backed by a COM class of your choice. Nice feature, eh? And guess which CLSID gets *special treatment* in there?
While reversing `mfsensorgroup.dll` something caught my attention 🥁 ... a function called `VerifyCOMServerByClsid`, that compares the requested class against a small internal allowlist... and guess what, our beloved CrossDevice CLSID is on it. A matching class is accepted through a special-case path instead of getting the normal COM-server-path validation. The protected registration stays usable even though it resolves to an absent DLL in a user-creatable location. On the analyzed image (file version `10.0.26100.8737`, SHA-256 `F6C21838...5761671`, for the nerds keeping score) the CrossDevice entry sits at `0x1800827F8`.
![GUID in mfsensorgroup.dll](../assets/mfsensorgroup-guid-ida.jpg){class="zoomable"}
At this point the plan writes itself:
```cpp
MFCreateVirtualCamera(
MFVirtualCameraType_SoftwareCameraSource,
MFVirtualCameraLifetime_Session,
MFVirtualCameraAccess_CurrentUser,
L"CrossDevice",
L"{E9F83CF2-E0C0-4CA7-AF01-E90C70BEF496}",
nullptr,
0,
&camera);
```
Create the camera with the CrossDevice CLSID, call `IMFVirtualCamera::Start`, then `GetMediaSource`... and the **Windows Frame Server** - a service running as `NT AUTHORITY\LOCAL SERVICE` - happily loads *your* media source.
Now, LOCAL SERVICE is not SYSTEM, I know, but stay with me for a second.
# The media source from hell
But of course there's a catch, 'cause there's always a catch. You can't just drop a DLL with a class factory and call it a day: the Frame Server expects a *functional* Media Foundation source before it completes the activation path. Streams, descriptors, the whole ceremony.
So a good chunk of this project was writing a complete, valid `IMFMediaSource` implementation (`SimpleMediaSource.cpp`, `SimpleMediaStream.cpp`, friends...) just to convince the service to keep us around long enough to have fun. The final PoC embeds the whole DLL as an `RCDATA` resource inside a single `exploit.exe`, plants it with `CREATE_NEW` (it politely refuses to overwrite anything that isn't its own), and activates the camera through a child process.
# It's potato time
![Console showing the exploit output OK SYSTEM cmd.exe](../assets/exploit-run.png){class="zoomable"}
And here's why LOCAL SERVICE is more than enough: the Frame Server process token is **unrestricted**, not AppContainer, and has **`SeImpersonatePrivilege`** enabled. If you've been in the Windows trenches for a while, your mouth is already watering: that's potato country.
The payload gates itself on the host identity (it only proceeds if the process token is `S-1-5-19`, LOCAL SERVICE), then runs the well-known EFSR coercion pattern - the same family as [CoercedPotato](https://github.com/hackvens/CoercedPotato):
1. Create a randomized named pipe, `\\.\pipe\<UUID>\pipe\srvsvc`.
2. Call `EfsRpcOpenFileRaw` over the local EFSR RPC endpoint (`ncalrpc`) with a path that resolves to our pipe.
3. The EFSR service connects to us. We call `ImpersonateNamedPipeClient`, open the thread token, and check it against `WinLocalSystemSid`.
4. Only if the captured token is **verified** as `NT AUTHORITY\SYSTEM` (`S-1-5-18`) do we continue - no blind trust, this is a civilized exploit.
5. `DuplicateTokenEx` to get a primary token, `TokenSessionId` to pin it to the active interactive session, `winsta0\default`, and `CreateProcessAsUserW` for good old `cmd.exe`.
And there it is, an interactive **SYSTEM shell** popping up in your own user session. No admin, no UAC, nothing.
# So, how bad is it
Standard user to SYSTEM, which means game over: bypass UAC and local ACLs, read other users' data, install persistent privileged stuff, own the box. Suggested severity **High**, CVSS 3.1 **7.8** (`AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`).
Confirmed on fully serviced builds:
| Operating system | Build | Result |
|---|---:|---|
| Windows 11 Pro 24H2 x64 | 26100.8894 | Confirmed - full chain from medium-integrity standard user to SYSTEM |
| Windows 11 Pro 25H2 x64 | 26200.8875 | Confirmed - same standalone exploit from a freshly created non-admin account |
This is *not* a claim that every edition is affected: Windows 10, Home, Enterprise and 26H1 weren't tested.
Worth noting: the CrossDevice/Media Foundation area already received security servicing ([CVE-2025-24076](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-24076), [CVE-2026-21508](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-21508)), and calif.io's path was fixed as CVE-2026-50343. This chain was reproduced on builds **newer than all of those fixes**. The primitive that refuses to die is the stale system-wide registration pointing to an absent file in a user-creatable `C:\ProgramData` subtree - plus a virtual-camera activation path that loads a valid attacker source into the privileged Frame Server.
# How to fix it
The root cause is really three trust decisions stacking up, and breaking any of them breaks the chain:
1. Remove the stale CrossDevice COM registration when the component isn't installed.
2. Put required in-process servers in directories protected from standard-user creation and modification.
3. Reject virtual-camera media sources whose resolved COM server is missing, user-controlled, unsigned, or outside an approved protected location - no special-casing for the CrossDevice CLSID.
4. Stop loading untrusted media sources in processes that hold `SeImpersonatePrivilege` (a restricted host would have made the potato stage a lot harder).
5. When servicing, clean up vulnerable registrations on *upgraded* systems too, not just new installs.
Huge kudos to calif.io for the original Dark Elevator research and for sharing it, the best thing about this community is that one good writeup sparks the next one. The AI found its bug, I found mine with a mix of old-fashioned work and agentic research, staring at a disassembler until the bytes confessed (ok, mostly the old-fashioned way).
> Have fun and happy hacking!

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
res/assets/dll-missing.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
res/assets/exploit-run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@ -28,6 +28,9 @@ $if(description)$
<meta name="description" content="$description$" /> <meta name="description" content="$description$" />
<meta property="og:description" content="$description$" /> <meta property="og:description" content="$description$" />
<meta name="twitter:description" content="$description$" /> <meta name="twitter:description" content="$description$" />
$endif$
$if(unlisted)$
<meta name="robots" content="noindex" />
$endif$ $endif$
<style> <style>
$styles.html()$ $styles.html()$

View File

@ -1,5 +1,4 @@
# IndexGen is the index generator for the static website. # IndexGen is the index generator for the static website.
# DO NOT MODIFY THIS FILE!
import argparse import argparse
import datetime import datetime
import jinja2 import jinja2
@ -33,6 +32,10 @@ def find_articles(resources) -> list:
article_data = yaml.load_all(file, Loader=yaml.SafeLoader) article_data = yaml.load_all(file, Loader=yaml.SafeLoader)
# get only first element out of the generator # get only first element out of the generator
article_data = next(article_data) article_data = next(article_data)
# articles marked "unlisted: true" are built and deployed at
# their URL but never shown on the index page
if article_data.get('unlisted'):
continue
# append link to the article # append link to the article
article_data['link'] = f"/articles/{os.path.basename(article).replace('.md', '.html')}" article_data['link'] = f"/articles/{os.path.basename(article).replace('.md', '.html')}"
articles.append(article_data) articles.append(article_data)