# 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
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
