# Combining Bcrypt With Other Hash Functions [Link bài viết](https://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html) Ở đây mình sẽ ghi lại những ý mình hiểu theo link bài trên ### Password_hash() Giống như một bài CTF mình từng làm, `Bcrypt` có một đặc điểm khá hay: ``` Caution Using the PASSWORD_BCRYPT as the algorithm, will result in the password parameter being truncated to a maximum length of 72 bytes. ``` Tức là khi bạn nhập từ byte thứ 73 trở đi, nó sẽ bị cắt đoạn đó -> người ta nghĩ ra cách có thể sử dụng nhiều hơn 72 bytes -> lồng các hàm với nhau khi sử dụng `password_hash()`. ```php $hash = password_hash(hash_hmac("sha256", $found[0], $key, true), PASSWORD_BCRYPT); $hash = password_hash(sha1($_POST['c'], false), PASSWORD_BCRYPT); ``` Vậy lồng các hàm này sẽ mang đến mối nguy hiểm gì không ? Chúng ta cùng nhau phân tích tiếp. ### crypt.c Source của PHP là C, vì vậy có những hành vi của PHP sẽ bị ảnh hưởng bởi C. Đi sâu một chút vào hàm `crypt` tại [Link](https://heap.space/xref/PHP-7.4/ext/standard/crypt.c?r=565baf05) ```cpp PHPAPI zend_string *php_crypt(const char *password, const int pass_len, const char *salt, int salt_len, zend_bool quiet) ``` Đi đến đoạn trả về respone: ```cpp } else if ( salt[0] == '$' && salt[1] == '2' && salt[3] == '$') { char output[PHP_MAX_SALT_LEN + 1]; memset(output, 0, PHP_MAX_SALT_LEN + 1); crypt_res = php_crypt_blowfish_rn(password, salt, output, sizeof(output)); if (!crypt_res) { ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1); return NULL; } else { result = zend_string_init(output, strlen(output), 0); ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1); return result; } } ``` Như ở đoạn đầu, `password` là một `char *`, vì vậy `php_crypt_blowfish_rn()` sẽ không thể biết độ dài của đoạn input nhập vào được -> Vậy làm sao nó biết độ dài để xử lý được ? -> Đi sâu hơn vào hàm `php_crypt_blowfish_rn()` [Link](https://heap.space/xref/PHP-7.4/ext/standard/crypt_blowfish.c?r=8d3f8ca1) ```cpp #define BF_N 16 /* ...code... */ const char *ptr = key; /* ...code... */ for (i = 0; i < BF_N + 2; i++) { tmp[0] = tmp[1] = 0; for (j = 0; j < 4; j++) { tmp[0] <<= 8; tmp[0] |= (unsigned char)*ptr; /* correct */ tmp[1] <<= 8; tmp[1] |= (BF_word_signed)(signed char)*ptr; /* bug */ if (j) sign |= tmp[1] & 0x80; if (!*ptr) ptr = key; else ptr++; } diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */ expanded[i] = tmp[bug]; initial[i] = BF_init_state.P[i] ^ tmp[bug]; } ``` `password` lúc này sẽ là `key`, với `BF_N` là 16, vòng loop ngoài sẽ lặp 18, loop trong lặp 4 và tổng là 18*4=72 lần, nó sẽ đọc `password` bằng cách dịch con trỏ ```cpp if (!*ptr) ptr = key; else ptr++; ``` Nhưng có 1 điều cần chú ý là sẽ ra sao nếu password có kí tự null `\00` ? Khi đó nó sẽ được đặt về đầu chuỗi -> khi đó chuỗi `test\0abc` có thể coi như `test\0namtest\0namtest\0namtest\0nam` -> các chuỗi bắt đầu bằng `test\0` đều tương tự. Về cơ bản, nó bỏ qua mọi thứ sau byte null đầu tiên. [Xem thêm](https://3v4l.org/Y6onV) ### Kết hợp 2 điều trên Ví dụ: ```php $hash = '$2y$10$2ECy/U3F/NSvAjMcuBeI6uMDmJlI8t8ux0pXOAoajpv2hSH0veOMi' var_dump(password_verify("\0", $hash)) // bool(true) ``` ```php Những trường hợp này là tương đương nhau a\0bc a\0cd a\0ef ``` Chúng ta sẽ thường sử dụng `password_hash()` và sau đó là sử dụng `password_verify()` -> Trong trường hợp chúng ta lồng 2 hàm như đã nói, nếu có thể trong quá trình hàm thứ nhất tạo raw byte sẽ tồn tại `\0` thì sẽ dẫn tới quá trình xác thực không mong muốn -> Cách phòng ngừa: chuyển về dạng hex, base64,...