
إثبات مفهوم لـ Gitea restore-repo file:// تضمين ملف محلي (CWE-73) ينسخ أهداف الروابط الرمزية خارج تفريغ غير موثوق إلى مرفقات الإصدار.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitea-restore-file-open
المقال التقني: https://abraxaslabs.tech/research/gitea-restore-file-open
Gitea 1.27.3 - Gitea
CVE-2026-59765 وضع عميل HTTP من نوع hostmatcher على OpenWithClient. يتجاهل فرع file:// ذلك العميل ويستدعي os.Open. استعادة تفريغ (dump) لا تحتوي على DownloadFunc. بعد ذلك يفتح gitea_uploader.go عنوان URL المُعاد كتابته file://. يتبع os.Open رابطًا رمزيًا (symlink) من جهة التفريغ. يبقى الدمج (join) داخل التفريغ. أما الرابط الرمزي فلا.
استعادة المشغّل (operator) لتفريغ غير موثوق تنسخ هدف الرابط الرمزي إلى مرفق إصدار (release attachment). وبصفتها uid الخاص بـ Gitea، فإن ذلك يعني أي ملف يستطيع حساب الخدمة قراءته.
| المعرّف | لا يوجد CVE بعد |
| CWE | CWE-73 |
| CVSS | متوسط: 4.2 CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:N/A:N |
| المنتج | Gitea |
| المتأثر | حتى v1.27.3 (146cc3e)؛ المشغّل المحلي gitea restore-repo |
| المصادقة | استعادة مشغّل محلي لتفريغ غير موثوق |
| الترخيص | GNU Affero GPL v3.0 |
| المختبر | 127.0.0.1 فقط |
سلّم المسؤول تفريغًا غير موثوق (دليل "نسخة احتياطية"، أو استعادة من مصدر بعيد غير موثوق) يحتوي على رابط رمزي تحت المسار المدمج داخل التفريغ. تنسخ الاستعادة الملف الهدف إلى مرفق إصدار على المستودع المستعاد. ومن يستطيع قراءة ذلك المستودع يقوم بتنزيله.
إذا سمّى مؤلف التفريغ app.ini، فإن ذلك سرقة بيانات اعتماد (INTERNAL_TOKEN، SECRET_KEY، JWT، كلمة مرور قاعدة البيانات). كان أوراكل المختبر ملف شاهد مزروع، وليس app.ini. ليس تصعيدًا محليًا للامتيازات (OS LPE). وليس غير مُصادَق. وليس هروبًا من دمج جذر التفريغ. الترحيل الحي (live migrate) من GitHub/GitLab/Gitea يضبط DownloadFunc ولا يستدعي os.Open على file:// من المهاجم.
كانت خريطتا استعادة على الطاولة المتبقية. اختبرت الواضحة أولًا في المختبر.
الاكتشاف 8 كان هروبًا من دمج جذر التفريغ: ضع DownloadURL مطلقًا في release.yml، آملًا أن يُسقط FilePathJoinAbs بادئة التفريغ بالطريقة التي يفعلها filepath.Join(base, "/etc/passwd") الساذج. على 1.27.3 لا يفعل ذلك. كل sub يصبح filepath.Clean("/"+s) ثم filepath.Join(base, cleaned). بقي دمج النسخة المتماثلة لـ /tmp/dump + /tmp/GITEA-JOIN-OUTSIDE على /tmp/dump/tmp/GITEA-JOIN-OUTSIDE. أعادت الاستعادة كتابة file:// تحت التفريغ وفشلت بـ open ...: no such file or directory. هذا ليس هروب دمج على هذه النسخة.
الاكتشاف 9 هو ما نجح. زرعت ملف شاهد خارج التفريغ، ووضعت رابطًا رمزيًا في المسار المدمج داخل التفريغ، وشغّلت gitea restore-repo. الاستعادة rc=0. احتوى مرفق الإصدار المستعاد على GITEA-FILE-OPEN-WITNESS.
المنعطفات الخاطئة المسجلة مسبقًا: هروب دمج جذر التفريغ عبر DownloadURL مطلق؛ LFI عبر file:// في الترحيل الحي؛ X-Gitea-Internal-Auth (الاستعادة هي CLI بصفة uid الخاص بـ Gitea، وليست /api/internal)؛ معاملة HTTP الاستعادة كأنها بعيدة غير مُصادَقة (CVSS محلي، امتياز عالٍ، تفاعل مستخدم)؛ reverse shell. مسرح.
cd lab
./run.sh
استهدف فقط http://127.0.0.1:18137. يقوم Compose بتركيب lab/dump للقراءة فقط. يزرع run.sh الشاهد داخل الحاوية، ويبني التفريغ، ويضع الرابط الرمزي، ثم gitea restore-repo.
restore-rc=0 Restore repo labadmin/restored successfully
download status=200 snippet=GITEA-FILE-OPEN-WITNESS
SUCCESS GITEA-RESTORE-FILE-OPEN
أزل file:// من OpenWithClient، أو ارفض متابعة روابط dump الرمزية، ولا تستعد dumps غير موثوقة. يجب ألا يحتوي المرفق المُستعاد على الشاهد خارج الـ dump.
uri.go · gitea_uploader.go · FilePathJoinAbsGNU Affero GPL v3.0. راجع LICENSE. مختبر loopback فقط. دون أي ضمان.