Why does this code with unlink treat zip files especially?

ghz 8months ago ⋅ 58 views

I've just done a CTF where you could look through source code, I've done it a way a bit more complicated than what it was supposed to. Anyways, there's just this piece of code that I didn't fully understand how it behaves because I would expect that every file it creates ends up being removed. I beat this using a bit of a race condition, basically slowing the execution of this piece of code, but if you instead manage to sneak a zip file by changing it's extension to a non blocked one, you manage to avoid the file from being removed. Could someone explain to me why does this happen? Here's the code:

function getExtension($file) {
    $extension = strrpos($file,".");
    return ($extension===false) ? "" : substr($file,$extension+1);
}

function isitup($url){
    $ch=curl_init();
    curl_setopt($ch, CURLOPT_URL, trim($url));
    curl_setopt($ch, CURLOPT_USERAGENT, "siteisup.htb beta");
    curl_setopt($ch, CURLOPT_HEADER, 1);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    $f = curl_exec($ch);
    $header = curl_getinfo($ch);
    if($f AND $header['http_code'] == 200){
        return array(true,$f);
    }else{
        return false;
    }
    curl_close($ch);
}

if($_POST['check']){
  
    # File size must be less than 10kb.
    if ($_FILES['file']['size'] > 10000) {
        die("File too large!");
    }
    $file = $_FILES['file']['name'];
    
    # Check if extension is allowed.
    $ext = getExtension($file);
    if(preg_match("/php|php[0-9]|html|py|pl|phtml|zip|rar|gz|gzip|tar/i",$ext)){
        die("Extension not allowed!");
    }
  
    # Create directory to upload our file.
    $dir = "uploads/".md5(time())."/";
    if(!is_dir($dir)){
        mkdir($dir, 0770, true);
    }
  
  # Upload the file.
    $final_path = $dir.$file;
    move_uploaded_file($_FILES['file']['tmp_name'], "{$final_path}");
    
  # Read the uploaded file.
    $websites = explode("\n",file_get_contents($final_path));
    
    foreach($websites as $site){
        $site=trim($site);
        if(!preg_match("#file://#i",$site) && !preg_match("#data://#i",$site) && !preg_match("#ftp://#i",$site)){
            $check=isitup($site);
            if($check){
                echo "<center>{$site}<br><font color='green'>is up ^_^</font></center>";
            }else{
                echo "<center>{$site}<br><font color='red'>seems to be down :(</font></center>";
            }   
        }else{
            echo "<center><font color='red'>Hacking attempt was detected !</font></center>";
        }
    }
    
  # Delete the uploaded file.
    @unlink($final_path);
}

I tried to look for documentation for the unlink function and tried to execute this function against zip files locally but that doesn't seem to be the problem. Here is a writeup that i found that says very naturally that uploading a zip file should bypass the deletion, though i don't really understand why: https://medium.com/@Poiint/htb-updown-write-up-bf01d926ddc4

Answers

In the provided code, the file upload functionality allows users to upload files, which are then stored in a temporary directory. Later, these files are read to check the status of websites listed within the files. After reading the file contents, the uploaded file is deleted using the unlink() function.

The key to understanding why uploading a zip file bypasses deletion lies in the behavior of the unlink() function and the way PHP handles zip files.

When a zip file is uploaded and stored in the temporary directory, it is still a valid file in the filesystem. However, when the unlink() function is called to delete the file, it fails to delete the zip file due to its locked status by other processes or the PHP runtime environment. This might happen because the zip file is being used by PHP (e.g., by file_get_contents() or move_uploaded_file() functions) or by other system processes.

In contrast, other file types (such as text files) might not be locked by other processes, allowing them to be easily deleted using unlink().

This behavior allows an attacker to exploit the vulnerability by uploading a zip file with malicious contents, as the file remains on the server even after the script attempts to delete it. As a result, the malicious file can persist on the server, potentially leading to further exploitation or unauthorized access.

To prevent this vulnerability, you should ensure that uploaded files are properly validated and sanitized, and restrict the file types that can be uploaded. Additionally, consider implementing proper error handling and logging to detect and respond to such attacks.