xref: /PHP-8.4/ext/mysqli/tests/fake_server.inc (revision 39c292b1)
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