Skip to content
Exploit for CVE-2026

Exploit for CVE-2026

Sploitus • November 13, 2026

# CVE-2026-123039 — Check Point Gaia OS Dynamic CLI world-writable control socket allows a local user to execute commands as root

| **Component** | Check Point Gaia OS — Dynamic / Extended CLI daemon, `/usr/bin/dynamic_server` |

| **Affected** | Gaia OS R81, R81.10, R81.20, R82, R82.10, R82.20 — Quantum Security Gateways and Force firewalls, Security Management and Log Servers, Multi-Domain Security Management and Log Servers, Maestro / Scalable Platforms. Check Point Spark firewalls and Smart-1 Cloud are not affected. |

| **Fixed in** | The security hotfix for advisory `sk1000362`, delivered through the monthly Jumbo Hotfix Accumulator of the affected branches |

| **CWE** | CWE-732 (Incorrect Permission Assignment for Critical Resource) and CWE-862 (Missing Authorization) |

| **Impact** | Local privilege escalation to root: an arbitrary command string written into the control socket is executed with the privileges of the daemon |

| **CVSS** | 7.8 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H` |

| **Vector** | The local AF_UNIX control socket `/tmp/dynamic_socket`, published by the root daemon `dynamic_server` |

| **Trigger** | The command string an unprivileged local process writes into the socket, framed as `## ` |

| **Advisory** | Check Point `sk1000362` |

| **Mitigation** | `dbset process:dynamic_server off` followed by `dbset :save` in Expert mode |

| **Indicators** | `/var/log/dynamicCli.log` — the daemon records every request it accepts |

The Gaia OS Dynamic CLI (`sk144112`) maps expert-mode diagnostic commands into the

restricted `clish` shell. The mapping is served by `dynamic_server`, a daemon that runs

as root and owns the local control socket `/tmp/dynamic_socket`. On affected builds the

socket is created world writable and the daemon neither authenticates nor authorizes

the process that connects to it, so any authenticated local account — a service

account, a portal worker, an interactive shell user — writes a command string into the

socket and the daemon executes it as root. The attack is local by design and does not

need any credential beyond the ability to run a process on the appliance, which is why

it is consistently used as the second stage of the published campaigns that begin with

This repository contains a containerised laboratory that reproduces the escalation on

the affected behaviour, contrasts it with the patched behaviour, tests the published

mitigation and the operator-side hardening, and reports the result of a 0-day sweep

over the patched line. Every claim is backed by a capture in `evidence/`.

## 1. Scope and fidelity of this laboratory

Gaia OS is proprietary, licence-gated and not redistributable, the Dynamic CLI daemon

ships as an opaque binary inside the `cprx`/`cphaprep` packages, and the upstream fix

is a binary hotfix with no public source commit. Nothing of the vendor product is

redistributed here. What is reproducible in this laboratory is:

* **the vendor's own statement of the defect** — CWE-732 and CWE-862, the Dynamic CLI

daemon, the world-writable `/tmp/dynamic_socket`, the absence of caller

authentication, the local privilege escalation to root, CVSS 7.8, and the

affected/fixed version matrix — taken from advisory `sk1000362` and the CVE record;

* **the attack surface of the component** — a root daemon, an AF_UNIX control socket

in the shared `/tmp` namespace, the length-framed request format `## `,

the command string executed with the credentials of the daemon, and the request log

* **the security-relevant behaviour of the service** — a socket whose mode and peer

validation decide whether an unprivileged local process can reach the command

* **the mitigation surface** — the `dbset process:dynamic_server off` configuration

switch and its effect on the published socket.

Everything the laboratory asserts the *vendor product* is sourced from the

advisory and the CVE record; everything it asserts the *model* is labelled as

such. The model runs in the laboratory, and the vulnerable and the patched sides

differ in the authorization behaviour of the control socket alone — same base image,

same configuration, same service, same entrypoint, the variant selected by an

The lab image also carries the two partial fixes separately, so the two halves of the

| build | socket mode | peer validation | meaning |

| `dynamic_server.vuln` | `0777` | none | the affected behaviour (CVE-2026-123039) |

| `dynamic_server.fixed` | `0600` | `SO_PEERCRED`, uid 0 only | the patched behaviour |

| `dynamic_server.perm` | `0600` | none | access control only |

| `dynamic_server.peer` | `0777` | `SO_PEERCRED`, uid 0 only | validation only |

The daemon creates its control socket and then decides who may use it. The affected

behaviour creates the socket world writable and never asks the kernel who the caller

361: if (bind(srv, (struct sockaddr *)&addr, sizeof(addr)) framed = 0;

The frame header is a convenience for the client, never a control. The authorization

decision does not depend on it, which is why the unframed legacy form reaches the

command execution path on the affected build just as the framed form does

On the affected build the PoC runs as uid 1000 and the command it sends produces a

file owned by uid 0 in the shared `/tmp` namespace

[exploit] socket : mode=0777 owner=root group=root

[exploit] marker : /tmp/gaia_lpe/pwned.txt owner=0 mode=0644

[exploit] marker data : uid=0 user=root | marker=created

[exploit] RESULT : VULNERABLE — uid 1000 (gaia) caused a command to run as uid 0 and produced a root owned file

The proof is read from the attacker side: the marker is owned by uid 0 although the

| Lab image | `cve-2026-123039:vuln` — Debian GNU/Linux 12 (bookworm), built from `lab/Containerfile` |

| PoC image | `cve-2026-123039-poc:local` — `python:3.12-alpine`, unprivileged uid 1000 |

| Network | `cve123039lab` (Podman bridge) |

| Shared namespace | Podman volume `cve123039lab-tmp`, mounted at `/tmp` in both the appliance and the PoC runner |

| Bounds | `--cpus 2 --memory 4g` on every container |

| Unprivileged account | `gaia`, uid 1000, gid 1000 |

| Privileged account | `root`, uid 0 — the identity of the daemon |

| Daemon log | `/var/log/dynamicCli.log`, mode `0640`, owned by root |

| Supervisor log | `/var/log/dynamic_server.supervisor.log` |

The shared volume is what makes the topology of the real appliance: on Gaia, the

daemon and every local account live in the same `/tmp`, so a socket file published

there is reachable from an unprivileged process. In the laboratory the volume is

mounted at `/tmp` in the appliance container and in the PoC runner container, so the

runner reaches the very same socket inode as a local attacker would. The PoC is

additionally executed inside the appliance container as `gaia`

(`podman exec --user 1000:1000`), which removes every doubt the topology.

The supervisor keeps the daemon running according to the local configuration, records

the exit status of a daemon that dies, and releases the socket when the daemon stops.

| `dynamic_server.vuln` | `-O2 -DGAIA_VARIANT=0` | the affected behaviour |

| `dynamic_server.fixed` | `-O2 -DGAIA_VARIANT=1` | the patched behaviour |

| `dynamic_server.perm` | `-O2 -DGAIA_VARIANT=2` | access control only |

| `dynamic_server.peer` | `-O2 -DGAIA_VARIANT=3` | validation only |

| `dynamic_server.asan` | `-O1 -fsanitize=address,undefined -fno-omit-frame-pointer` | the daemon under the sanitizers |

| `fuzz_frame` | `-O1 -fsanitize=address,undefined -fno-omit-frame-pointer` | the frame parser harness |

The image digests of the binaries and of the images are recorded in

make prepare # build the appliance image and the PoC runner image

make up # start the appliance with the affected behaviour

make check # classify the affected and the patched build

make exploit # run the full chain against both

make probe # exercise the sibling request forms

make mitigation # test the published mitigation and the socket hardening

make paths # the reproduction path matrix

make differential # everything above, with the evidence captured

make down # remove the containers

Every target is bounded and isolated: one Podman network per laboratory, one volume

for the shared `/tmp`, `--cpus 2 --memory 4g` on every container.

The laboratory can also be driven directly:

./run-lab.sh up 1 # start the appliance with the patched daemon

./run-lab.sh check # classify both sides and capture the evidence

./run-lab.sh differential # the whole differential

`poc/poc.py` is a standard-library-only Python program. It models the unprivileged

local attacker and refuses to run as root unless `--allow-root` is given.

| `check` | classify the target without changing any state: it reports the socket mode, whether the request handler is reachable and whether a command runs with root privileges |

| `exploit` | run the full chain: a command is sent that creates a root owned marker file, and the marker is verified from the attacker side |

| `probe` | exercise the companion primitives and the sibling request forms (framed, unframed, length mismatches, embedded NUL, two frames in one write) and report the privileged path |

| `mitigation` | verify that the published mitigation is in effect: the socket absent, the connection refused or the request refused all count as mitigated |

# against the laboratory, from the runner container

podman run --rm --network cve123039lab --user 1000:1000 \

-v cve123039lab-tmp:/tmp cve-2026-123039-poc:local exploit

# on the appliance itself, as the unprivileged account

podman exec --user 1000:1000 cve123039-gaia python3 /opt/poc/poc.py exploit

# against a real appliance, as the local account that holds the session

python3 poc/poc.py exploit --socket /tmp/dynamic_socket

| `0` | safe — the caller cannot reach command execution as root, or the mitigation is in effect |

| `1` | vulnerable — the caller reached command execution with root privileges |

| `2` | target unreachable — no socket published, no listener, or a timeout |

| `3` | primitive confirmed, gadget blocked — the socket is still world writable (CWE-732 still present) but the daemon refuses the request |

| Unprivileged local user writes a command frame into `/tmp/dynamic_socket` | reproduced on the affected build, blocked on the patched build | `evidence/check-vulnerable-run.txt`, `evidence/exploit-vulnerable-run.txt`, `evidence/check-fixed-run.txt`, `evidence/exploit-fixed-run.txt` |

| The same chain from a separate container that shares the appliance `/tmp` | reproduced / blocked | `evidence/exploit-runner-vulnerable-run.txt` |

| The same chain executed in situ on the appliance host as `gaia` | reproduced / blocked | `evidence/exploit-vulnerable-run.txt` |

| Unframed legacy request | reaches command execution on the affected build, blocked on the patched build | `evidence/probe-vulnerable-run.txt`, `evidence/probe-fixed-run.txt` |

| Length-prefixed request without the separating space | reaches command execution on the affected build | `evidence/probe-vulnerable-run.txt` |

| Length mismatch between the frame header and the payload | truncated or rejected, never an execution primitive | `evidence/probe-vulnerable-run.txt` |

| Embedded NUL in the command string | rejected by the parser | `evidence/probe-vulnerable-run.txt` |

| Two frames in one write | the first frame executes on the affected build | `evidence/probe-vulnerable-run.txt` |

| Root caller (the legitimate Dynamic CLI client) | served on every build — the fix must not break the privileged path | `evidence/0day-sweep.txt`, `evidence/path-matrix-run.txt` |

| The daemon publishes its socket at another path | still world writable, still vulnerable | `evidence/path-matrix-run.txt` |

| The daemon is stopped | no socket published, the PoC reports the target as unreachable | `evidence/path-matrix-run.txt` |

| The caller is already root | no escalation | `evidence/path-matrix-run.txt` |

| side | socket mode | `check` | `exploit` | evidence |

| affected (`vuln`) | `0777` | exit `1` — vulnerable | exit `1` — root owned marker created | `evidence/check-vulnerable-run.txt`, `evidence/exploit-vulnerable-run.txt` |

| patched (`fixed`) | `0600` | exit `0` — the connection is refused with `EACCES` | exit `0` — no marker | `evidence/check-fixed-run.txt`, `evidence/exploit-fixed-run.txt` |

| access control only (`perm`) | `0600` | exit `0` | — | `evidence/check-perm-run.txt` |

| validation only (`peer`) | `0777` | exit `3` — the socket is still world writable, the daemon refuses the request | — | `evidence/check-peer-run.txt` |

The differential the laboratory asserts is the pair of runs against the same PoC:

* the affected build reports `VULNERABLE — uid 1000 (gaia) reached command execution

with uid 0` and produces a root owned marker file;

* the patched build reports `safe — access control refuses the connection` and

`sk1000362` publishes the interim mitigation for the case that the hotfix cannot be

Measured on the affected build (`evidence/mitigation-dbsetserver-off-run.txt`): the

daemon stops, the socket is withdrawn, and the PoC reports `mitigated — no control

socket is published`. Reverting the configuration restores the escalation

(`evidence/mitigation-dbsetserver-reverted-run.txt`). The mitigation is effective and

reversible, at the price of the Dynamic CLI commands of `sk144112` failing while it is

### 6.2 Operator-side hardening of the socket mode

Restricting the socket mode by hand blocks the escalation as well, but the daemon

recreates the socket at startup and the hardening is silently lost on the

restart of the daemon (`evidence/mitigation-socketmode-run.txt`):

[mitigation] RESULT : mitigated — the socket refuses the connection

[mitigation] RESULT : NOT mitigated — the daemon still executes commands as uid 0

The published `dbset` mitigation is the only one of the two that survives a restart,

because it is the configuration the daemon itself obeys.

The flaw fires when all of the following hold:

* the Dynamic CLI daemon `dynamic_server` runs — it is enabled by default in the

appliance configuration, `process:dynamic_server` is `on`;

* the control socket is published at `/tmp/dynamic_socket` in a `/tmp` that is

world writable, which is the case of the appliance `/tmp` (`1777`);

* the attacker holds a local execution context on the appliance — an authenticated

account, a service account, or a compromised web portal worker. Any process that

can create a file in `/tmp` can reach the socket;

* the daemon runs with root privileges, which is its normal configuration.

Nothing of the chain requires a Gaia credential, a `clish` session or expert mode.

The published campaigns combine CVE-2026-123039 with a remote entry point, but the

escalation itself is local and unauthenticated on the appliance.

## 8. Additional findings (0-day sweep on the patched line)

The sweep runs against the patched build and asks one question: does any path remain

open that lets an unprivileged local process reach command execution? The full

transcript is `evidence/0day-sweep.txt`; the same sweep against the affected build is

| 1 | the unprivileged account connects to the patched socket | refused with `EACCES` — the socket is `0600` |

| 2 | a symlink pre-created at the socket path, to redirect the daemon's `bind()` | rejected — the daemon removes the stale entry before it binds, and the published socket is a socket again (`socket path: socket mode=600 owner=0:0`) |

| 3 | a regular file pre-created at the socket path | rejected — same mechanism, the daemon binds over it |

| 4 | write access to the daemon log `/var/log/dynamicCli.log` | refused — the log is `0640` and owned by root; no low privilege write path into the log exists |

| 5 | removal of the root owned socket by an unprivileged account | refused — the appliance `/tmp` is sticky, so only the owner of the entry may remove it |

| 6 | the privileged path of the Dynamic CLI | intact — a root client is still served on the patched build, the fix does not break the feature |

| 7 | the request framing parser under AddressSanitizer and UndefinedBehaviorSanitizer | `fuzz_frame: 200012 parser invocations, 0 defects` — the length field, the truncation, the oversized and NUL bearing frames are all handled without an out of bounds access or an undefined operation |

The sweep is a property of the laboratory, not of the vendor binary. `SO_PEERCRED`

cannot be forged by a client, and the sticky `/tmp` blocks the removal of the socket

entry, so neither of the two bypasses of the patched line has a route in the model; a

vendor implementation that resolves its socket path differently, or that accepts the

command string from a second channel, would have to be examined separately on the

poc/poc.py the proof of concept

poc/Containerfile the PoC runner image

lab/src/dynamic_server.c the Dynamic CLI daemon model

lab/src/dynframe.c the request framing parser

lab/src/dynframe.h the framing contract

lab/src/fuzz_frame.c the sanitizer harness for the framing parser

lab/src/supervisor.sh the process supervisor of the appliance

lab/src/dbset.sh the local configuration tool

lab/src/entrypoint.sh the appliance entry point

lab/src/sweep.sh the 0-day sweep of the patched build

evidence/ the captured runs of the differential

* Check Point advisory `sk1000362` — the Dynamic CLI local privilege escalation

* Check Point `sk144112` — the Dynamic / Extended CLI commands

* Check Point Quantum Security Gateway and Security Management documentation

* CVE-2026-109618 / CVE-2026-109619 — the remote entry points the published campaigns