#!/usr/bin/env python3 """ upstream_frame_logger.py - Raw HTTP/2 Frame Logger for CVE-2026-42926 Complete schema with HTTP/2 preface handling and injection detection """ import socket import struct import json import sys import os import threading from datetime import datetime from typing import Optional, Dict, List, Tuple # HTTP/2 Frame Types FRAME_TYPES = { 0x00: 'DATA', 0x01: 'HEADERS', 0x02: 'PRIORITY', 0x03: 'RST_STREAM', 0x04: 'SETTINGS', 0x05: 'PUSH_PROMISE', 0x06: 'PING', 0x07: 'GOAWAY', 0x08: 'WINDOW_UPDATE', 0x09: 'CONTINUATION' } # HTTP/2 Connection Preface HTTP2_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' class HTTP2FrameLogger: def __init__(self, port: int = 8081, log_dir: str = "./upstream_logs"): self.port = port self.log_dir = log_dir os.makedirs(log_dir, exist_ok=True) self.frames: List[Dict] = [] self.frame_index = 0 self.connection_id = datetime.now().strftime('%Y%m%d_%H%M%S_%f') self.log_file = os.path.join(log_dir, f"frames_{self.connection_id}.json") self.lock = threading.Lock() self.raw_byte_offset = 0 self.connection_start = datetime.now().isoformat() self.preface_skipped = False def parse_frame_header(self, data: bytes) -> Optional[Dict]: """Parse 9-byte HTTP/2 frame header""" if len(data) < 9: return None # Length: 3 bytes, Type: 1 byte, Flags: 1 byte, Stream ID: 4 bytes length = struct.unpack('!I', b'\x00' + data[0:3])[0] frame_type = data[3] flags = data[4] stream_id = struct.unpack('!I', data[5:9])[0] & 0x7FFFFFFF return { 'length': length, 'type': FRAME_TYPES.get(frame_type, f'UNKNOWN({frame_type})'), 'type_code': frame_type, 'flags': flags, 'stream_id': stream_id, 'raw_header': data[0:9].hex(), 'raw_header_bytes': data[0:9] } def log_frame(self, frame_info: Dict, payload: bytes, byte_offset: int, is_injected: bool = False, injection_reason: str = None, run_id: str = None, crafted_frame_header_hex: str = None): """Log a complete frame with injection detection metadata""" with self.lock: self.frame_index += 1 frame_data = { 'frame_index': self.frame_index, 'timestamp': datetime.now().isoformat(), 'connection_id': self.connection_id, 'direction': 'nginx_to_upstream', 'stream_id': frame_info['stream_id'], 'frame': frame_info, 'payload_hex': payload.hex() if payload else None, 'payload_ascii': payload.decode('utf-8', errors='replace') if payload else None, 'payload_length': len(payload) if payload else 0, 'byte_offset': byte_offset, 'is_injected': is_injected, 'injection_reason': injection_reason, 'run_id': run_id, 'crafted_frame_header_hex': crafted_frame_header_hex, 'observed_frame_header_hex': frame_info['raw_header'], 'observed_frame_matches_crafted_frame': ( crafted_frame_header_hex == frame_info['raw_header'] if crafted_frame_header_hex else None ), 'completion_flag': True } self.frames.append(frame_data) print(f"[+] Frame #{self.frame_index}: {frame_info['type']} | " f"Stream: {frame_info['stream_id']} | Length: {frame_info['length']} | " f"Injected: {frame_data['is_injected']} | Reason: {injection_reason or 'N/A'}") def save_log(self): """Save all frames to JSON with complete metadata""" with self.lock: with open(self.log_file, 'w') as f: json.dump({ 'connection_id': self.connection_id, 'connection_start': self.connection_start, 'frame_count': len(self.frames), 'frames': self.frames, 'metadata': { 'logger_version': '6.0', 'cve': 'CVE-2026-42926', 'timestamp': datetime.now().isoformat(), 'preface_skipped': self.preface_skipped, 'schema': { 'connection_id': 'unique connection identifier', 'direction': 'nginx_to_upstream', 'stream_id': 'HTTP/2 stream identifier', 'frame_index': 'sequential frame number', 'byte_offset': 'raw byte offset in connection', 'is_injected': 'injection detected flag', 'injection_reason': 'reason for injection detection', 'run_id': 'lab run identifier for correlation', 'crafted_frame_header_hex': 'frame header sent in request body', 'observed_frame_header_hex': 'frame header received upstream', 'observed_frame_matches_crafted_frame': 'comparison result', 'completion_flag': 'frame fully parsed' } } }, f, indent=2) print(f"[+] Saved {len(self.frames)} frames to: {self.log_file}") return self.log_file class HTTP2UpstreamServer: def __init__(self, port: int = 8081, log_dir: str = "./upstream_logs"): self.port = port self.logger = HTTP2FrameLogger(port, log_dir) self.running = True self.connections = [] self.crafted_frame_header_hex = None def set_crafted_frame_header(self, hex_string: str): """Set the crafted frame header hex for comparison""" self.crafted_frame_header_hex = hex_string def handle_connection(self, client_socket: socket.socket, addr): """Handle raw HTTP/2 connection and log frames with injection detection""" client_socket.settimeout(30) buffer = b'' byte_offset = 0 try: while self.running: chunk = client_socket.recv(4096) if not chunk: break buffer += chunk # Skip HTTP/2 connection preface if present if not self.logger.preface_skipped and buffer.startswith(HTTP2_PREFACE): buffer = buffer[len(HTTP2_PREFACE):] byte_offset += len(HTTP2_PREFACE) self.logger.preface_skipped = True print(f"[+] HTTP/2 preface skipped ({len(HTTP2_PREFACE)} bytes)") # Parse frames from buffer while len(buffer) >= 9: frame_header = self.logger.parse_frame_header(buffer) if not frame_header: break # Extract payload payload_start = 9 payload_end = 9 + frame_header['length'] if len(buffer) < payload_end: break # Wait for more data payload = buffer[payload_start:payload_end] # Detect injection based on frame characteristics is_injected, injection_reason = self._detect_injection( frame_header, payload, byte_offset ) # Extract run_id from payload if present run_id = None if payload: try: payload_str = payload.decode('utf-8', errors='replace') if 'CVE2026_42926_' in payload_str: # Extract run_id from marker import re match = re.search(r'CVE2026_42926_[a-zA-Z0-9_]+', payload_str) if match: run_id = match.group(0) except: pass self.logger.log_frame( frame_header, payload, byte_offset, is_injected, injection_reason, run_id, self.crafted_frame_header_hex ) # Update byte offset byte_offset += 9 + frame_header['length'] # Remove processed frame from buffer buffer = buffer[payload_end:] except socket.timeout: print("[+] Connection timeout") except Exception as e: print(f"[!] Error: {e}") finally: client_socket.close() self.logger.save_log() def _detect_injection(self, frame_header: Dict, payload: bytes, byte_offset: int) -> Tuple[bool, Optional[str]]: """Detect if frame was injected from request body bytes""" # Condition 1: Frame header bytes match known injection pattern # Check for DATA frame with any length on stream 1 (client-initiated) if frame_header['type'] == 'DATA' and frame_header['stream_id'] == 1: # Check if payload contains CVE marker pattern if payload and b'CVE2026_42926_' in payload: return True, 'marker_found_in_data_frame_payload' # Condition 2: DATA frame with length 0 after large body (wrapped frame) if frame_header['type'] == 'DATA' and frame_header['length'] == 0: return True, 'zero_length_data_frame_after_large_body' # Condition 3: Unexpected frame type in request body path if frame_header['type'] in ['HEADERS', 'SETTINGS', 'PING'] and byte_offset > 1000: return True, 'unexpected_frame_type_in_body_path' return False, None def run(self): """Start the server""" server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('0.0.0.0', self.port)) server.listen(5) print(f"[+] HTTP/2 Frame Logger listening on port {self.port}") print(f"[+] Log directory: {self.logger.log_dir}") while self.running: client_socket, addr = server.accept() print(f"[+] Connection from {addr}") self.handle_connection(client_socket, addr) if __name__ == "__main__": port = int(sys.argv[1]) if len(sys.argv) > 1 else 8081 log_dir = sys.argv[2] if len(sys.argv) > 2 else "./upstream_logs" server = HTTP2UpstreamServer(port, log_dir) server.run()