11/08/2018, 20:48

Lỗ hổng bảo mật mới và nghiêm trọng của hầu hết phiên bản PHP (5/2015)

Lỗ hổng với mã CVE-2015-4024 được coi là nghiêm trọng khi cho phép attacker thực hiện tấn công từ chối dịch vụ (DoS) gây ra tổn thất lớn cho máy chủ chỉ bằng một đoạn mã nhỏ. Nếu một hệ thống botnet được trang bị sử dụng chiến lược này thì thiệt hại gây ra sẽ là khủng khiếp. CVE-2015-4024 ...

Lỗ hổng với mã CVE-2015-4024 được coi là nghiêm trọng khi cho phép attacker thực hiện tấn công từ chối dịch vụ (DoS) gây ra tổn thất lớn cho máy chủ chỉ bằng một đoạn mã nhỏ. Nếu một hệ thống botnet được trang bị sử dụng chiến lược này thì thiệt hại gây ra sẽ là khủng khiếp.

CVE-2015-4024 được báo cáo bởi một một nhà nghiên cứu an ninh tại Baidu, Trung Quốc, dựa vào lỗi trong đoạn logic phân tích HTTP header của PHP, qua đó nếu tạo ra một đoạn HTTP header đặc biệt thì có thể đẩy được CPU của server lên 100% khiến toàn bộ các chức năng khác trở nên tê liệt.

Nếu nhìn vào CHANGELOG của PHP gần đây và search CVE-2015-4024 thì sẽ thấy các dòng

Fixed bug #69364 (PHP Multipart/form-data remote dos Vulnerability). (CVE-2015-4024)

vào ngày 14/5/2015 ở các phiên bản

  • PHP 5.6.9
  • PHP 5.5.25
  • PHP 5.4.41

Điều này có nghĩa là PHP 5.6.9+, 5.5.25+, 5.4.41+ đã được vá, đồng nghĩa với chuyện toàn bộ các version

  • PHP 5.6.(<9)
  • PHP 5.5.(<25)
  • 5.4.(<41)
  • 5.3.*
  • 5.2.*

đều đang nằm trong vùng nguy hiểm. Nếu bạn đang chạy dịch vụ với dịch vụ sử dụng PHP, bạn nên kiểm tra version và vá lỗ hổng ngay bây giờ.

Hiện tại có 2 cách vá lỗ hổng tại thời điểm này

  • Nâng cấp version PHP lên các phiên bản đã được vá lỗi (đã liệt kê ở trên). Tuy nhiên nâng cấp version thường không đơn giản đối với các hệ thống lớn và đã xây dựng từ lâu do độ phức tạp của xử lý và tính lệ thuộc của source code.
  • Sử dụng bản patch. Cách này có vẻ thực tế hơn. Mình sẽ hướng dẫn cách dùng bản patch ở phần tiếp.

HIện nay đã có bản patch cho PHP 5.4, 5.5. và 5.6 ở ngay trong link báo cáo CVE-2015-4024. Cụ thể hơn, bạn có thể xem ở link trực tiếp của bản patch. Cách sử dụng bản patch là như sau

  • Download bản patch về (patch-5.4.patch.txt)
  • Đổi tên thành patch-5.4.patch
  • Vào thư mục chứa source PHP của máy chủ
  • patch -p2 < /path/to/patch-5.4.patch
  • Recompile lại bản PHP vừa mới patch
  • Khởi động lại server máy chủ (apache/nginx/v.v...)

Nếu mảy chủ của bạn đang sử dụng PHP 5.3 hoặc nhỏ hơn thì sẽ không thể dùng được bản patch theo cách trên, do source code của file sửa đã thay đổi cả những phần khác, vì thế số dòng đã thay đổi, khiến cho câu lệnh patch không còn nhận được. Cách làm sẽ hơi phức tạp hơn một chút

  • Vào thư mục chứa source PHP của máy chủ
  • Tìm đến file sửa của bản vá lần này (main/rfc1867.c)
  • Thay đổi bằng tay giống như các chỉnh sửa trong patch (cái này hơi mất thời gian)
  • Recompile lại bản PHP vừa mới patch
  • Khởi động lại server máy chủ (apache/nginx/v.v...)

Cách kiểm tra tốt nhất là ... tự viết script tấn công để tấn công chính server của mình. Hiện giờ mình có script mô phỏng lại quá trình trên, tuy nhiên vì lỗ hổng này mới xuất hiện nên sẽ không công khai. Mặc dù vậy để tự viết thì khá dễ, bạn chỉ cần đọc hiểu nội dung báo cáo phân tích về code lỗi (ở đây là ngôn ngữ C) là có thể làm được ngay.

Mình sẽ lược dịch phần mô tả bug sang tiếng Việt để các bạn tiện theo dõi.

Khi một HTTP request được gửi lên server và "đến lượt" PHP xử lý, PHP sẽ đọc tuần tự từng dòng (line by line) của phần header trong HTTP request đó. Giả sử với một đoạn header như là:

Content-Disposition: form-data; name="file"; filename="test.txt" 
Content-Type: application/octet-stream 

thì PHP sẽ đọc lần lượt 2 dòng và chạy xử lý từ trên xuống dưới.

Content-Disposition: form-data; name="file"; filename="test.txt" 

Content-Type: application/octet-stream 

Xử lý khi đọc tuần tự từng dòng là đoạn code C như sau:

/* parse headers */ 
static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header TSRMLS_DC) 
{ 
char *line; 
mime_header_entry prev_entry = {0}, entry; 
int prev_len, cur_len; 

/* didn't find boundary, abort */ 
if (!find_boundary(self, self->boundary TSRMLS_CC)) { 
return 0; 
} 

/* get lines of text, or CRLF_CRLF */ 

while( (line = get_line(self TSRMLS_CC)) && line[0] != '' ) 
{ 
/* add header to table */ 
char *key = line; 
char *value = NULL; 

if (php_rfc1867_encoding_translation(TSRMLS_C)) { 
self->input_encoding = zend_multibyte_encoding_detector(line, strlen(line), self->detect_order, self->detect_order_size TSRMLS_CC); 
} 

/* space in the beginning means same header */ 
if (!isspace(line[0])) { 
value = strchr(line, ':'); 
} 

if (value) { 
*value = 0; 
do { value++; } while(isspace(*value)); 

entry.value = estrdup(value); 
entry.key = estrdup(key); 

} else if (zend_llist_count(header)) { /* If no ':' on the line, add to previous line */ 

prev_len = strlen(prev_entry.value); 
cur_len = strlen(line); 

entry.value = emalloc(prev_len + cur_len + 1); 
memcpy(entry.value, prev_entry.value, prev_len); 
memcpy(entry.value + prev_len, line, cur_len); 
entry.value[cur_len + prev_len] = ''; 

entry.key = estrdup(prev_entry.key); 

zend_llist_remove_tail(header); 
} else { 
continue; 
} 

zend_llist_add_element(header, &entry); 
prev_entry = entry; 
} 

return 1; 
} 

Ở đây nếu như thỏa mãn 2 điều kiện:

  • Dòng đang đọc không bắt đầu bởi kí tự space
  • Dòng đang đọc không chứa dấu :

thì xử lý sẽ nhảy vào 2 điều kiện

if (!isspace(line[0])) {
  value = strchr(line, ':');
} 

if (zend_llist_count(header)) {
  ... 
}

thì khi đó đoạn code tương ứng sẽ được thực thi, mà cụ thể là

prev_len = strlen(prev_entry.value); 
cur_len = strlen(line); 

entry.value = emalloc(prev_len + cur_len + 1); //allocate (prev_len + cur_len) bytes memory. 
memcpy(entry.value, prev_entry.value, prev_len); //copy prev_len bytes. 
memcpy(entry.value + prev_len, line, cur_len); // cope (prev_len + cur_len) bytes memory. 
entry.value[cur_len + prev_len] = ''; 

entry.key = estrdup(prev_entry.key); 

zend_llist_remove_tail(header); // free memory 

Đoạn code này có một vấn đề: nếu được thực thi nhiều lần liên tiếp liên tục nhau thì hàm memcpy sẽ gây ra hiện tượng thiếu hụt tài nguyên, đẩy CPU lên 100% và tê liệt hệ thống. Vì vậy attacker sẽ làm thế nào để tạo ra một vòng loop mà PHP luôn luôn nhảy vào đoạn xử lý này.

Vậy làm thế nào để tạo ra vòng loop nói trên ? Câu trả lời là tạo ra HTTP header mà nhiều dòng liên tiếp không bắt đầu bằng kí tự space và trong dòng không chưa dấu ":" . VÍ dụ:

Content-Disposition: form-data; name="file"; filename="s 
a 
a 
a 
a" 
Content-Type: application/octet-stream 

<?php phpinfo();?> 

HTTP header có max size là 2097152 bytes (2M), với size này và cách làm như trên thì có thể tạo ra request đủ sức đánh gục CPU của server :)

XIn nhắc lại các link quan trọng trong bài viết này.

  • Báo cáo bug của PHP
  • PHP ChangeLog
  • Link bản patch cho PHP5.4 +
0