1<?php 2 3function my_mysqli_data_fields(): array 4{ 5 return [ 6 'intval' => [ 7 'type' => '03', 8 'charset' => '3f00', 9 'length' => '0b000000', 10 'flags' => '0110', 11 'decimal' => '00', 12 'query_data_packet_length' => '080000', 13 'query_data_value' => '023134', 14 'stmt_data_packet_length' => '0b0000', 15 'stmt_data_value' => '0e000000' 16 ], 17 'fltval' => [ 18 'type' => '04', 19 'charset' => '3f00', 20 'length' => '0c000000', 21 'flags' => '0110', 22 'decimal' => '1f', 23 'query_data_packet_length' => '090000', 24 'query_data_value' => '03322e33', 25 'stmt_data_packet_length' => '0b0000', 26 'stmt_data_value' => '33331340', 27 ], 28 'dblval' => [ 29 'type' => '05', 30 'charset' => '3f00', 31 'length' => '16000000', 32 'flags' => '0110', 33 'decimal' => '1f', 34 'query_data_packet_length' => '090000', 35 'query_data_value' => '03312e32', 36 'stmt_data_packet_length' => '0f0000', 37 'stmt_data_value' => '333333333333f33f' 38 ], 39 'datval' => [ 40 'type' => '0a', 41 'charset' => '3f00', 42 'length' => '0a000000', 43 'flags' => '8110', 44 'decimal' => '00', 45 'query_data_packet_length' => '100000', 46 'query_data_value' => '0a323031342d31322d3135', 47 'stmt_data_packet_length' => '0c0000', 48 'stmt_data_value' => '04de070c0f' 49 ], 50 'timval' => [ 51 'type' => '0b', 52 'charset' => '3f00', 53 'length' => '0a000000', 54 'flags' => '8110', 55 'decimal' => '00', 56 'query_data_packet_length' => '0e0000', 57 'query_data_value' => '0831333a30303a3032', 58 'stmt_data_packet_length' => '100000', 59 'stmt_data_value' => '080000000000150801' 60 ], 61 'dtival' => [ 62 'type' => '0c', 63 'charset' => '3f00', 64 'length' => '13000000', 65 'flags' => '8110', 66 'decimal' => '00', 67 'query_data_packet_length' => '190000', 68 'query_data_value' => '13323031342d31322d31362031333a30303a3031', 69 'stmt_data_packet_length' => '0f0000', 70 'stmt_data_value' => '07de070c100d0001' 71 ], 72 'bitval' => [ 73 'type' => '10', 74 'charset' => '3f00', 75 'length' => '40000000', 76 'flags' => '2110', 77 'decimal' => '00', 78 'query_data_packet_length' => '0e0000', 79 'query_data_value' => '080808080808080808', 80 'stmt_data_packet_length' => '100000', 81 'stmt_data_value' => '080808080808080808' 82 ], 83 'strval' => [ 84 'type' => 'fd', 85 'charset' => 'e000', 86 'length' => 'c8000000', 87 'flags' => '0110', 88 'decimal' => '00', 89 'query_data_packet_length' => '0a0000', 90 'query_data_value' => '0474657374', 91 'stmt_data_packet_length' => '0c0000', 92 'stmt_data_value' => '0474657374' 93 ], 94 ]; 95} 96 97function my_mysqli_data_field(string $field): array 98{ 99 $fields = my_mysqli_data_fields(); 100 if (!isset($fields[$field])) { 101 throw new Exception("Unknown field $field"); 102 } 103 return $fields[$field]; 104} 105 106 107 108class my_mysqli_fake_packet_item 109{ 110 public function __construct(public string|null $name, public string $value, public bool $is_hex = true) 111 { 112 } 113} 114 115class my_mysqli_fake_packet 116{ 117 private array $data = array(); 118 119 public function __get(string $name) 120 { 121 foreach ($this->data as $item) { 122 if ($item->name === $name) { 123 return $item->value; 124 } 125 } 126 return null; 127 } 128 129 public function __set(string $name, string|my_mysqli_fake_packet_item $value) 130 { 131 if ($value instanceof my_mysqli_fake_packet_item) { 132 if ($value->name === null) { 133 $value->name = $name; 134 } 135 } else { 136 $value = new my_mysqli_fake_packet_item($name, $value, true); 137 } 138 139 for ($i = 0; $i < count($this->data); $i++) { 140 if ($this->data[$i]->name === $name) { 141 $this->data[$i] = $value; 142 return; 143 } 144 } 145 146 $this->data[] = $value; 147 } 148 149 public function to_bytes(): string 150 { 151 $bytes = ''; 152 foreach ($this->data as $item) { 153 $bytes .= $item->is_hex ? hex2bin($item->value) : $item->value; 154 } 155 return $bytes; 156 } 157} 158 159class my_mysqli_fake_packet_generator 160{ 161 public static function create_packet_item(int|string $value, bool $is_hex = false, string $format = 'v'): my_mysqli_fake_packet_item 162 { 163 if (is_string($value)) { 164 $packed_value = $value; 165 } else { 166 $packed_value = pack($format, $value); 167 } 168 return new my_mysqli_fake_packet_item(null, $packed_value, $is_hex); 169 } 170 171 public function server_ok(): my_mysqli_fake_packet 172 { 173 $packet = new my_mysqli_fake_packet(); 174 $packet->packet_length = "070000"; 175 $packet->packet_number = "02"; 176 $packet->header = "00"; // OK 177 $packet->affected_rows = "00"; 178 $packet->last_insert_id = "00"; 179 $packet->server_status = "0200"; 180 $packet->warning_count = "0000"; 181 return $packet; 182 } 183 184 public function server_greetings(): my_mysqli_fake_packet 185 { 186 $packet = new my_mysqli_fake_packet(); 187 $packet->packet_length = "580000"; 188 $packet->packet_number = "00"; 189 $packet->proto_version = "0a"; 190 $packet->version = self::create_packet_item('5.5.5-10.5.18-MariaDB' . chr(0)); 191 $packet->thread_id = "03000000"; 192 $packet->salt = "473e3f6047257c67"; 193 $packet->filler = "00"; 194 $packet->server_capabilities = self::create_packet_item(0b1111011111111110); 195 $packet->server_character_set = "08"; 196 $packet->server_status = self::create_packet_item(0b000000000000010); 197 $packet->extended_server_capabilities = self::create_packet_item(0b1000000111111111); 198 $packet->auth_plugin = "15"; 199 $packet->unused = "000000000000"; 200 $packet->mariadb_extended_server_capabilities = self::create_packet_item(0b1111, false, 'V'); 201 $packet->mariadb_extended_server_capabilities_salt = "6c6b55463f49335f686c643100"; 202 $packet->mariadb_extended_server_capabilities_auth_plugin = self::create_packet_item('mysql_native_password'); 203 204 return $packet; 205 } 206 207 public function server_tabular_query_response(): array 208 { 209 $qr1 = new my_mysqli_fake_packet(); 210 $qr1->packet_length = "010000"; 211 $qr1->packet_number = "01"; 212 $qr1->field_count = "01"; 213 214 $qr2 = new my_mysqli_fake_packet(); 215 $qr2->packet_length = "190000"; 216 $qr2->packet_number = "02"; 217 $qr2->catalog_length_plus_name = "0164"; 218 $qr2->db_length_plus_name = "0164"; 219 $qr2->table_length_plus_name = "0164"; 220 $qr2->original_t = "0164"; 221 $qr2->name_length_plus_name = "0164"; 222 $qr2->original_n = "0164"; 223 $qr2->canary = "0c"; 224 $qr2->charset = "3f00"; 225 $qr2->length = "0b000000"; 226 $qr2->type = "03"; 227 $qr2->flags = "0350"; 228 $qr2->decimals = "000000"; 229 230 $qr3 = new my_mysqli_fake_packet(); 231 $qr3->full = "05000003fe00002200"; 232 233 $qr4 = new my_mysqli_fake_packet(); 234 $qr4->full = "0400000401350174"; 235 236 $qr5 = new my_mysqli_fake_packet(); 237 $qr5->full = "05000005fe00002200"; 238 239 return [$qr1, $qr2, $qr3, $qr4, $qr5]; 240 } 241 242 public function server_upsert_query_response(): array 243 { 244 $qr1 = new my_mysqli_fake_packet(); 245 $qr1->packet_length = "010000"; 246 $qr1->packet_number = "01"; 247 $qr1->field_count = "00"; // UPSERT 248 $qr1->affected_rows = "00"; 249 $qr1->affected_rows = "00"; 250 $qr1->last_insert_id = "00"; 251 $qr1->server_status = "0000"; 252 $qr1->warning_count = "0000"; 253 $qr1->len = "01"; 254 $qr1->filename = "65"; 255 $qr1->packet_length = sprintf("%02x0000", strlen($qr1->to_bytes())-4); 256 257 return [$qr1]; 258 } 259 260 public function server_stmt_prepare_response_start($num_field): my_mysqli_fake_packet 261 { 262 $pr1 = new my_mysqli_fake_packet(); 263 $pr1->packet_length = "0c0000"; 264 $pr1->packet_number = "01"; 265 $pr1->response_code = '00'; // OK 266 $pr1->statement_id = '01000000'; 267 $pr1->num_fields = $num_field; 268 $pr1->num_params = '0000'; 269 $pr1->filler = '00'; 270 $pr1->warnings = '0000'; 271 272 return $pr1; 273 } 274 275 public function server_stmt_prepare_response_end($packer_number): my_mysqli_fake_packet 276 { 277 $pr3 = new my_mysqli_fake_packet(); 278 $pr3->packet_length = "050000"; 279 $pr3->packet_number = $packer_number; 280 $pr3->packet_type = 'fe'; // EOF 281 $pr3->warnings = '0000'; 282 $pr3->server_status = '0200'; 283 284 return $pr3; 285 } 286 287 public function server_stmt_prepare_items_response(): array 288 { 289 $pr1 = $this->server_stmt_prepare_response_start('0100'); 290 291 $pr2 = new my_mysqli_fake_packet(); 292 $pr2->packet_length = "300000"; 293 $pr2->packet_number = "02"; 294 $pr2->catalogue_len = '03'; 295 $pr2->catalogue = '646566'; // def 296 $pr2->db_len = '08'; 297 $pr2->db = '7068705f74657374'; // php_test 298 $pr2->table_len = '05'; 299 $pr2->table = '6974656d73'; // items 300 $pr2->orig_table_len = '05'; 301 $pr2->orig_table = '6974656d73'; // items 302 $pr2->name_len = '04'; 303 $pr2->name = '6974656d'; 304 $pr2->orig_name_len = '04'; 305 $pr2->orig_name = '6974656d'; 306 $pr2->something = '0c'; 307 $pr2->charset = 'e000'; 308 $pr2->length = 'c8000000'; 309 $pr2->field_type = 'fd'; // FIELD_TYPE_VAR_STRING 310 $pr2->flags = '0110'; 311 $pr2->decimal = '00'; 312 $pr2->padding = '0000'; 313 314 $pr3 = $this->server_stmt_prepare_response_end('03'); 315 316 return [$pr1, $pr2, $pr3]; 317 } 318 319 public function server_stmt_prepare_data_response_field($packet_number, $field_name): my_mysqli_fake_packet 320 { 321 if (strlen($field_name) != 6) { 322 throw new Exception("Invalid field length - only 6 is allowed"); 323 } 324 325 $field = my_mysqli_data_field($field_name); 326 327 $pr = new my_mysqli_fake_packet(); 328 $pr->packet_length = "320000"; 329 $pr->packet_number = $packet_number; 330 $pr->catalogue_len = '03'; 331 $pr->catalogue = bin2hex('def'); 332 $pr->db_len = '08'; 333 $pr->db = bin2hex('php_test'); 334 $pr->table_len = '04'; 335 $pr->table = bin2hex('data'); 336 $pr->orig_table_len = '04'; 337 $pr->orig_table = bin2hex('data'); 338 $pr->name_len = '06'; 339 $pr->name = bin2hex($field_name); 340 $pr->orig_name_len = '06'; 341 $pr->orig_name = bin2hex($field_name); 342 $pr->something = '0c'; 343 $pr->charset = $field['charset']; 344 $pr->length = $field['length']; 345 $pr->field_type = $field['type']; 346 $pr->flags = $field['flags']; 347 $pr->decimal = $field['decimal']; 348 $pr->padding = '0000'; 349 350 return $pr; 351 } 352 353 public function server_stmt_prepare_data_response(string $field_name): array 354 { 355 $pr1 = $this->server_stmt_prepare_response_start('0200'); 356 357 $pr2 = $this->server_stmt_prepare_data_response_field('02', 'strval'); 358 $pr3 = $this->server_stmt_prepare_data_response_field('03', $field_name); 359 360 $pr4 = $this->server_stmt_prepare_response_end('04'); 361 362 return [$pr1, $pr2, $pr3, $pr4]; 363 } 364 365 public function server_stmt_execute_items_response(): array 366 { 367 $pr1 = new my_mysqli_fake_packet(); 368 $pr1->packet_length = "010000"; 369 $pr1->packet_number = "01"; 370 $pr1->num_fields = '01'; 371 372 $pr2 = new my_mysqli_fake_packet(); 373 $pr2->packet_length = "300000"; 374 $pr2->packet_number = "02"; 375 $pr2->catalogue_len = '03'; 376 $pr2->catalogue = '646566'; // def 377 $pr2->db_len = '08'; 378 $pr2->db = '7068705f74657374'; // php_test 379 $pr2->table_len = '05'; 380 $pr2->table = '6974656d73'; // items 381 $pr2->orig_table_len = '05'; 382 $pr2->orig_table = '6974656d73'; // items 383 $pr2->name_len = '04'; 384 $pr2->name = '6974656d'; 385 $pr2->orig_name_len = '04'; 386 $pr2->orig_name = '6974656d'; 387 $pr2->something = '0c'; 388 $pr2->charset = 'e000'; 389 $pr2->length = 'c8000000'; 390 $pr2->field_type = 'fd'; // FIELD_TYPE_VAR_STRING 391 $pr2->flags = '0110'; 392 $pr2->decimal = '00'; 393 $pr2->padding = '0000'; 394 395 $pr3 = new my_mysqli_fake_packet(); 396 $pr3->packet_length = "050000"; 397 $pr3->packet_number = "03"; 398 $pr3->packet_type = 'fe'; // EOF 399 $pr3->warnings = '0000'; 400 $pr3->server_status = '2200'; 401 402 $pr4 = new my_mysqli_fake_packet(); 403 $pr4->packet_length = "070000"; 404 $pr4->packet_number = "04"; 405 $pr4->packet_type = '00'; // OK 406 $pr4->affected_rows = '00'; 407 $pr4->row_data_len = '04'; 408 $pr4->row_data = '74657374'; // item 409 410 $pr5 = new my_mysqli_fake_packet(); 411 $pr5->full = '05000005fe00002200'; 412 413 return [$pr1, $pr2, $pr3, $pr4, $pr5]; 414 } 415 416 private function server_execute_data_response_start(string $field_name): array 417 { 418 $pr1 = new my_mysqli_fake_packet(); 419 $pr1->packet_length = "010000"; 420 $pr1->packet_number = "01"; 421 $pr1->num_fields = '02'; 422 423 $pr2 = new my_mysqli_fake_packet(); 424 $pr2->packet_length = "320000"; 425 $pr2->packet_number = "02"; 426 $pr2->catalogue_len = '03'; 427 $pr2->catalogue = '646566'; // def 428 $pr2->db_len = '08'; 429 $pr2->db = '7068705f74657374'; // php_test 430 $pr2->table_len = '04'; 431 $pr2->table = bin2hex('data'); 432 $pr2->orig_table_len = '04'; 433 $pr2->orig_table = bin2hex('data'); 434 $pr2->name_len = '06'; 435 $pr2->name = bin2hex('strval'); 436 $pr2->orig_name_len = '06'; 437 $pr2->orig_name = bin2hex('strval'); 438 $pr2->something = '0c'; 439 $pr2->charset = 'e000'; 440 $pr2->length = 'c8000000'; 441 $pr2->field_type = 'fd'; // FIELD_TYPE_VAR_STRING 442 $pr2->flags = '0110'; 443 $pr2->decimal = '00'; 444 $pr2->padding = '0000'; 445 446 $field = my_mysqli_data_field($field_name); 447 448 $pr3 = new my_mysqli_fake_packet(); 449 $pr3->packet_length = "320000"; 450 $pr3->packet_number = "03"; 451 $pr3->catalogue_len = '03'; 452 $pr3->catalogue = '646566'; // def 453 $pr3->db_len = '08'; 454 $pr3->db = '7068705f74657374'; // php_test 455 $pr3->table_len = '04'; 456 $pr3->table = bin2hex('data'); 457 $pr3->orig_table_len = '04'; 458 $pr3->orig_table = bin2hex('data'); 459 $pr3->name_len = '06'; 460 $pr3->name = bin2hex($field_name); 461 $pr3->orig_name_len = '06'; 462 $pr3->orig_name = bin2hex($field_name); 463 $pr3->something = '0c'; 464 $pr3->charset = $field['charset']; 465 $pr3->length = $field['length']; 466 $pr3->field_type = $field['type']; 467 $pr3->flags = $field['flags']; 468 $pr3->decimal = $field['decimal']; 469 $pr3->padding = '0000'; 470 471 $pr4 = new my_mysqli_fake_packet(); 472 $pr4->packet_length = "050000"; 473 $pr4->packet_number = "04"; 474 $pr4->packet_type = 'fe'; // EOF 475 $pr4->warnings = '0000'; 476 $pr4->server_status = '2200'; 477 478 return [$field, $pr1, $pr2, $pr3, $pr4]; 479 } 480 481 private function server_execute_data_response_end(): my_mysqli_fake_packet 482 { 483 $pr6 = new my_mysqli_fake_packet(); 484 $pr6->packet_length = '050000'; 485 $pr6->packet_number = "06"; 486 $pr6->packet_type = 'fe'; // EOF 487 $pr6->warnings = '0000'; 488 $pr6->server_status = '2200'; 489 490 return $pr6; 491 } 492 493 public function server_stmt_execute_data_response(string $field_name): array 494 { 495 [$field, $pr1, $pr2, $pr3, $pr4] = $this->server_execute_data_response_start($field_name); 496 497 $pr5 = new my_mysqli_fake_packet(); 498 $pr5->packet_length = $field['stmt_data_packet_length']; 499 $pr5->packet_number = "05"; 500 $pr5->packet_type = '00'; // OK 501 $pr5->affected_rows = '00'; 502 $pr5->row_field1_len = '04'; 503 $pr5->row_field1_data = '74657374'; // test 504 $pr5->row_field2 = $field['stmt_data_value']; 505 506 return [$pr1, $pr2, $pr3, $pr4, $pr5, $this->server_execute_data_response_end()]; 507 } 508 509 public function server_query_execute_data_response(string $field_name): array 510 { 511 [$field, $pr1, $pr2, $pr3, $pr4] = $this->server_execute_data_response_start($field_name); 512 513 $pr5 = new my_mysqli_fake_packet(); 514 $pr5->packet_length = $field['query_data_packet_length']; 515 $pr5->packet_number = "05"; 516 $pr5->row_field1_len = '04'; 517 $pr5->row_field1_data = '74657374'; // test 518 $pr5->row_field2 = $field['query_data_value']; 519 520 return [$pr1, $pr2, $pr3, $pr4, $pr5, $this->server_execute_data_response_end()]; 521 } 522} 523 524class my_mysqli_fake_server_conn 525{ 526 private $conn; 527 public $packet_generator; 528 529 public function __construct($socket) 530 { 531 $this->packet_generator = new my_mysqli_fake_packet_generator(); 532 $this->conn = stream_socket_accept($socket); 533 if ($this->conn) { 534 fprintf(STDERR, "[*] Connection established\n"); 535 } else { 536 fprintf(STDERR, "[*] Failed to establish connection\n"); 537 } 538 } 539 540 public function packets_to_bytes(array $packets): string 541 { 542 return implode('', array_map(fn($s) => $s->to_bytes(), $packets)); 543 } 544 545 public function send($payload, $message = null): void 546 { 547 if ($message) { 548 fprintf(STDERR, "[*] Sending - %s: %s\n", $message, bin2hex($payload)); 549 } 550 fwrite($this->conn, $payload); 551 } 552 553 public function read($bytes_len = 1024) 554 { 555 // wait 20ms to fill the buffer 556 usleep(20000); 557 $data = fread($this->conn, $bytes_len); 558 if ($data) { 559 fprintf(STDERR, "[*] Received: %s\n", bin2hex($data)); 560 } 561 } 562 563 public function close() 564 { 565 fclose($this->conn); 566 } 567 568 public function send_server_greetings() 569 { 570 $this->send($this->packet_generator->server_greetings()->to_bytes(), "Server Greeting"); 571 } 572 573 public function send_server_ok() 574 { 575 $this->send($this->packet_generator->server_ok()->to_bytes(), "Server OK"); 576 } 577 578 public function send_server_tabular_query_response(): void 579 { 580 $packets = $this->packet_generator->server_tabular_query_response(); 581 $this->send($this->packets_to_bytes($packets), "Tabular response"); 582 } 583 584 public function send_server_stmt_prepare_items_response(): void 585 { 586 $packets = $this->packet_generator->server_stmt_prepare_items_response(); 587 $this->send($this->packets_to_bytes($packets), "Stmt prepare items"); 588 } 589 590 591 public function send_server_stmt_prepare_data_response(string $field_name): void 592 { 593 $packets = $this->packet_generator->server_stmt_prepare_data_response($field_name); 594 $this->send($this->packets_to_bytes($packets), "Stmt prepare data $field_name"); 595 } 596 597 public function send_server_stmt_execute_items_response(): void 598 { 599 $packets = $this->packet_generator->server_stmt_execute_items_response(); 600 $this->send($this->packets_to_bytes($packets), "Stmt execute items"); 601 } 602 603 public function send_server_stmt_execute_data_response(string $field_name): void 604 { 605 $packets = $this->packet_generator->server_stmt_execute_data_response($field_name); 606 $this->send($this->packets_to_bytes($packets), "Stmt execute data $field_name"); 607 } 608 609 public function send_server_query_execute_data_response(string $field_name): void 610 { 611 $packets = $this->packet_generator->server_query_execute_data_response($field_name); 612 $this->send($this->packets_to_bytes($packets), "Query execute data $field_name"); 613 } 614} 615 616class my_mysqli_fake_server_process 617{ 618 private int $port; 619 620 public function __construct(private $process, private array $pipes) {} 621 622 public function terminate(bool $wait = false): void 623 { 624 if ($wait) { 625 $this->wait(); 626 } 627 proc_terminate($this->process); 628 } 629 630 public function wait(): void 631 { 632 $line = fgets($this->pipes[1]); 633 if (preg_match('/\[\*\] Server started on \d+\.\d+\.\d+\.\d+:(\d+)/', $line, $matches)) { 634 $this->port = (int)$matches[1]; 635 } 636 echo $line; 637 } 638 639 public function getPort(): int 640 { 641 return $this->port ?? throw new RuntimeException("Port not set"); 642 } 643} 644 645function my_mysqli_test_tabular_response_def_over_read(my_mysqli_fake_server_conn $conn): void 646{ 647 $rh = $conn->packet_generator->server_tabular_query_response(); 648 649 // Length of the packet is modified to include the next added data 650 $rh[1]->packet_length = "1e0000"; 651 652 // We add a length field encoded on 4 bytes which evaluates to 65536. If the process crashes because 653 // the heap has been overread, lower this value. 654 $rh[1]->extra_def_size = "fd000001"; # 65536 655 656 // Filler 657 $rh[1]->extra_def_data = "aa"; 658 659 $trrh = $conn->packets_to_bytes($rh); 660 661 $conn->send_server_greetings(); 662 $conn->read(); 663 $conn->send_server_ok(); 664 $conn->read(); 665 $conn->send($trrh, "Malicious Tabular Response [Extract heap through buffer over-read]"); 666 $conn->read(65536); 667} 668 669function my_mysqli_test_upsert_response_filename_over_read(my_mysqli_fake_server_conn $conn): void 670{ 671 $rh = $conn->packet_generator->server_upsert_query_response(); 672 673 // Set extra length to overread 674 $rh[0]->len = "fa"; 675 676 $trrh = $conn->packets_to_bytes($rh); 677 678 $conn->send_server_greetings(); 679 $conn->read(); 680 $conn->send_server_ok(); 681 $conn->read(); 682 $conn->send($trrh, "Malicious Tabular Response [Extract heap through buffer over-read]"); 683 $conn->read(65536); 684} 685 686function my_mysqli_test_auth_response_message_over_read(my_mysqli_fake_server_conn $conn): void 687{ 688 $p = $conn->packet_generator->server_ok(); 689 $p->packet_length = "090000"; 690 $p->message_len = "fcff"; 691 692 $conn->send_server_greetings(); 693 $conn->read(); 694 $conn->send($p->to_bytes(), "Malicious OK Auth Response [Extract heap through buffer over-read]"); 695 $conn->read(); 696} 697 698function my_mysqli_test_stmt_response_row_over_read_string(my_mysqli_fake_server_conn $conn): void 699{ 700 $rh = $conn->packet_generator->server_stmt_execute_items_response(); 701 702 // Set extra length to overread 703 $rh[3]->row_data_len = "fa"; 704 705 $conn->send_server_greetings(); 706 $conn->read(); 707 $conn->send_server_ok(); 708 $conn->read(); 709 $conn->send_server_stmt_prepare_items_response(); 710 $conn->read(); 711 $conn->send($conn->packets_to_bytes($rh), "Malicious Stmt Response for items [Extract heap through buffer over-read]"); 712 $conn->read(65536); 713} 714 715function my_mysqli_test_stmt_response_row_over_read_two_fields( 716 my_mysqli_fake_server_conn $conn, 717 string $field_name, 718 string $row_field1_len = '06' 719): void { 720 $rh = $conn->packet_generator->server_stmt_execute_data_response($field_name); 721 722 // Set extra length to overread by two bytes 723 $rh[4]->row_field1_len = $row_field1_len; 724 725 $conn->send_server_greetings(); 726 $conn->read(); 727 $conn->send_server_ok(); 728 $conn->read(); 729 $conn->send_server_stmt_prepare_data_response($field_name); 730 $conn->read(); 731 $conn->send( 732 $conn->packets_to_bytes($rh), 733 "Malicious Stmt Response for data $field_name [Extract heap through buffer over-read]" 734 ); 735 $conn->read(65536); 736} 737 738function my_mysqli_test_stmt_response_row_over_read_int(my_mysqli_fake_server_conn $conn): void 739{ 740 my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'intval'); 741} 742 743function my_mysqli_test_stmt_response_row_over_read_float(my_mysqli_fake_server_conn $conn): void 744{ 745 my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'fltval'); 746} 747 748function my_mysqli_test_stmt_response_row_over_read_double(my_mysqli_fake_server_conn $conn): void 749{ 750 my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'dblval'); 751} 752 753function my_mysqli_test_stmt_response_row_over_read_date(my_mysqli_fake_server_conn $conn): void 754{ 755 my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'datval'); 756} 757 758function my_mysqli_test_stmt_response_row_over_read_time(my_mysqli_fake_server_conn $conn): void 759{ 760 my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'timval', '0c'); 761} 762 763function my_mysqli_test_stmt_response_row_over_read_datetime(my_mysqli_fake_server_conn $conn): void 764{ 765 my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'dtival'); 766} 767 768function my_mysqli_test_stmt_response_row_no_space(my_mysqli_fake_server_conn $conn): void 769{ 770 my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'strval', '09'); 771} 772 773function my_mysqli_test_stmt_response_row_over_read_bit(my_mysqli_fake_server_conn $conn): void 774{ 775 my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'bitval'); 776} 777 778function my_mysqli_test_stmt_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void 779{ 780 $conn->send_server_greetings(); 781 $conn->read(); 782 $conn->send_server_ok(); 783 $conn->read(); 784 $field_names = array_keys(my_mysqli_data_fields()); 785 foreach ($field_names as $field_name) { 786 $conn->send_server_stmt_prepare_data_response($field_name); 787 $conn->read(65536); 788 $conn->send_server_stmt_execute_data_response($field_name); 789 $conn->read(65536); 790 } 791} 792 793function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server_conn $conn): void 794{ 795 $rh = $conn->packet_generator->server_query_execute_data_response('strval'); 796 797 // Set extra length to overread by two bytes 798 $rh[4]->row_field2 = 'fefefefefe'; 799 800 $conn->send_server_greetings(); 801 $conn->read(); 802 $conn->send_server_ok(); 803 $conn->read(); 804 $conn->send($conn->packets_to_bytes($rh), "Malicious Query Response for data strval field [length overflow]"); 805 $conn->read(65536); 806} 807 808function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void 809{ 810 $conn->send_server_greetings(); 811 $conn->read(); 812 $conn->send_server_ok(); 813 $conn->read(); 814 $field_names = array_keys(my_mysqli_data_fields()); 815 foreach ($field_names as $field_name) { 816 $conn->send_server_query_execute_data_response($field_name); 817 $conn->read(); 818 } 819} 820 821function run_fake_server(string $test_function, int|string $port = 0): int 822{ 823 $host = '127.0.0.1'; 824 825 $socket = @stream_socket_server("tcp://$host:$port", $errno, $errstr); 826 if (!$socket) { 827 die("Failed to create socket: $errstr ($errno)\n"); 828 } 829 if (intval($port) === 0) { 830 $address = stream_socket_get_name($socket, false); 831 list($host, $port) = explode(":", $address); 832 } 833 834 echo "[*] Server started on $host:$port\n"; 835 836 try { 837 $conn = new my_mysqli_fake_server_conn($socket); 838 $test_function_name = 'my_mysqli_test_' . $test_function; 839 call_user_func($test_function_name, $conn); 840 $conn->close(); 841 } catch (Exception $e) { 842 fprintf(STDERR, "[!] Exception: " . $e->getMessage() . "\n"); 843 } 844 845 fclose($socket); 846 847 echo "[*] Server finished\n"; 848} 849 850 851function run_fake_server_in_background($test_function, $port = 0): my_mysqli_fake_server_process 852{ 853 $command = [PHP_BINARY, '-n', __FILE__, 'mysqli_fake_server', $test_function, $port]; 854 855 $descriptorspec = array( 856 0 => array("pipe", "r"), 857 1 => array("pipe", "w"), 858 2 => STDERR, 859 ); 860 861 $process = proc_open($command, $descriptorspec, $pipes); 862 863 if (is_resource($process)) { 864 return new my_mysqli_fake_server_process($process, $pipes); 865 } else { 866 throw new Exception("Failed to start server process"); 867 } 868} 869 870if (isset($argv) && $argc > 2 && $argv[1] == 'mysqli_fake_server') { 871 run_fake_server($argv[2], $argv[3] ?? 0); 872} 873