diff --git a/Part1/Day11/files/1.png b/Part1/Day11/files/1.png new file mode 100644 index 0000000..d8f906b Binary files /dev/null and b/Part1/Day11/files/1.png differ diff --git a/Part1/Day11/files/10.png b/Part1/Day11/files/10.png new file mode 100644 index 0000000..f92da1f Binary files /dev/null and b/Part1/Day11/files/10.png differ diff --git a/Part1/Day11/files/11.png b/Part1/Day11/files/11.png new file mode 100644 index 0000000..395e18b Binary files /dev/null and b/Part1/Day11/files/11.png differ diff --git a/Part1/Day11/files/12.png b/Part1/Day11/files/12.png new file mode 100644 index 0000000..40b3b09 Binary files /dev/null and b/Part1/Day11/files/12.png differ diff --git a/Part1/Day11/files/13.png b/Part1/Day11/files/13.png new file mode 100644 index 0000000..9c8a249 Binary files /dev/null and b/Part1/Day11/files/13.png differ diff --git a/Part1/Day11/files/14.png b/Part1/Day11/files/14.png new file mode 100644 index 0000000..4291af8 Binary files /dev/null and b/Part1/Day11/files/14.png differ diff --git a/Part1/Day11/files/15.png b/Part1/Day11/files/15.png new file mode 100644 index 0000000..1f20bfa Binary files /dev/null and b/Part1/Day11/files/15.png differ diff --git a/Part1/Day11/files/16.png b/Part1/Day11/files/16.png new file mode 100644 index 0000000..cf681e3 Binary files /dev/null and b/Part1/Day11/files/16.png differ diff --git a/Part1/Day11/files/17.png b/Part1/Day11/files/17.png new file mode 100644 index 0000000..a16ca20 Binary files /dev/null and b/Part1/Day11/files/17.png differ diff --git a/Part1/Day11/files/18.png b/Part1/Day11/files/18.png new file mode 100644 index 0000000..64bb1b0 Binary files /dev/null and b/Part1/Day11/files/18.png differ diff --git a/Part1/Day11/files/19.png b/Part1/Day11/files/19.png new file mode 100644 index 0000000..e3610cd Binary files /dev/null and b/Part1/Day11/files/19.png differ diff --git a/Part1/Day11/files/2.png b/Part1/Day11/files/2.png new file mode 100644 index 0000000..d0b21fe Binary files /dev/null and b/Part1/Day11/files/2.png differ diff --git a/Part1/Day11/files/3.png b/Part1/Day11/files/3.png new file mode 100644 index 0000000..1e5f346 Binary files /dev/null and b/Part1/Day11/files/3.png differ diff --git a/Part1/Day11/files/4.png b/Part1/Day11/files/4.png new file mode 100644 index 0000000..c6df25b Binary files /dev/null and b/Part1/Day11/files/4.png differ diff --git a/Part1/Day11/files/5.png b/Part1/Day11/files/5.png new file mode 100644 index 0000000..58b224e Binary files /dev/null and b/Part1/Day11/files/5.png differ diff --git a/Part1/Day11/files/6.png b/Part1/Day11/files/6.png new file mode 100644 index 0000000..6d28bf4 Binary files /dev/null and b/Part1/Day11/files/6.png differ diff --git a/Part1/Day11/files/7.png b/Part1/Day11/files/7.png new file mode 100644 index 0000000..d19248b Binary files /dev/null and b/Part1/Day11/files/7.png differ diff --git a/Part1/Day11/files/8.png b/Part1/Day11/files/8.png new file mode 100644 index 0000000..a734d3c Binary files /dev/null and b/Part1/Day11/files/8.png differ diff --git a/Part1/Day11/files/9.png b/Part1/Day11/files/9.png new file mode 100644 index 0000000..fe8c791 Binary files /dev/null and b/Part1/Day11/files/9.png differ diff --git a/Part1/Day11/files/README.md b/Part1/Day11/files/README.md new file mode 100644 index 0000000..bd4e79e --- /dev/null +++ b/Part1/Day11/files/README.md @@ -0,0 +1,288 @@ +本文由红日安全成员: **licong** 编写,如有不当,还望斧正。 + +## 前言 + +大家好,我们是红日安全-代码审计小组。最近我们小组正在做一个PHP代码审计的项目,供大家学习交流,我们给这个项目起了一个名字叫 [**PHP-Audit-Labs**](https://github.com/hongriSec/PHP-Audit-Labs) 。现在大家所看到的系列文章,属于项目 **第一阶段** 的内容,本阶段的内容题目均来自 [PHP SECURITY CALENDAR 2017](https://www.ripstech.com/php-security-calendar-2017/) 。对于每一道题目,我们均给出对应的分析,并结合实际CMS进行解说。在文章的最后,我们还会留一道CTF题目,供大家练习,希望大家喜欢。下面是 **第11篇** 代码审计文章: + +## Day 11 - Pumpkin Pie + +题目如下: + +![](1.png) + +**漏洞解析:** (上图代码第11行正则表达式应改为:'/O:\d:/') + +题目考察对php反序列化函数的利用。在第10行 **loadData()** 函数中,我们发现了 **unserialize** 函数对传入的 **$data** 变量进行了反序列。在反序列化前,对变量内容进行了判断,先不考虑绕过,跟踪一下变量,看看变量是否可控。在代码 **第6行** ,调用了 **loadData()** 函数,`$data`变量来自于 **__construct()** 构造函数传入的变量。代码第32行,对 **Template** 类进行了实例化,并将 **cookie** 中键为'data'数据作为初始化数据进行传入,`$data`数据我们可控。开始考虑绕过对传入数据的判断。 + +代码 **11行** ,第一个if,截取前两个字符,判断反序列化内容是否为对象,如果为对象,返回为空。php可反序列化类型有String,Integer,Boolean,Null,Array,Object。去除掉Object后,考虑采用数组中存储对象进行绕过。 + +第二个if判断,匹配 字符串为 \'O:任意十进制:',将对象放入数组进行反序列化后,仍然能够匹配到,返回为空,考虑一下如何绕过正则匹配,PHP反序列化处理部分源码如下: + +![2](2.png) + +![3](3.png) + +![4](4.png) + +在PHP源码var_unserializer.c,对反序列化字符串进行处理,在代码568行对字符进行判断,并调用相应的函数进行处理,当字符为'O'时,调用 **yy13** 函数,在 **yy13** 函数中,对‘O‘字符的下一个字符进行判断,如果是':',则调用 **yy17** 函数,如果不是则调用 **yy3** 函数,直接return 0,结束反序列化。接着看 **yy17** 函数。通过观察yybm[]数组可知,第一个if判断是否为数字,如果为数字则跳转到 **yy20** 函数,第二个判断如果是'+'号则跳转到 **yy19** ,在 **yy19** 中,继续对 **+号** 后面的字符进行判断,如果为数字则跳转到 **yy20** ,如果不是则跳转到 **yy18** , **y18** 最终跳转到 **yy3** ,退出反序列化流程。由此,在'O:',后面可以增加'+',用来绕过正则判断。 + +绕过了过滤以后,接下来考虑怎样对反序列化进行利用,反序列化本质是将序列化的字符串还原成对应的类实例,在该过程中,我们可控的是序列化字符串的内容,也就是对应类中变量的值。我们无法直接调用类中的函数,但PHP在满足一定的条件下,会自动触发一些函数的调用,该类函数,我们称为魔术方法。通过可控的类变量,触发自动调用的魔术方法,以及魔术方法中存在的可利用点,进而形成反序列化漏洞的利用。 + +在代码31行,对象销毁时会调用 **createCache()** 函数,函数将 `$template` 中的内容放到了 `$cacheFile` 对应的文件中。 **file_put_contents()** 函数,当文件不存在时,会创建该文件。由此可构造一句话,写入当前路径。 + + `$cacheFile` 和 `$template` 为类变量,反序列化可控,由此,构造以下反序列化内容,别忘了加'+'号 + +![5](5.png) + +放入cookie需进行URL编码 + +```php +a:1:{i:0;O:+8:"Template":2:{s:9:"cacheFile";s:10:"./test.php";s:8:"template";s:25:"";}} +``` + +文件成功写入: + +![6](6.png) + +## 实例分析 + +本次实例分析,选取的是 **Typecho-1.1** 版本,在该版本中,用户可通过反序列化Cookie数据进行前台Getshell。该漏洞出现于 **install.php** 文件 **230行** ,具体代码如下: + +![7](7.png) + +在上图代码 **第3行** ,对Cookie中的数据base64解码以后,进行了反序列化操作,该值可控,接下来看一下代码触发条件。文件几个关键判断如下: + +![8](8.png) + +第一个if判断,可通过GET传递 **finish=任意值** 绕过 ,第二if判断是否有GET或者POST传参,并判断Referer是否为空,第四个if判断Referer是否为本站点。紧接着还有判断,如下图: + +![9](9.png) + +第一个if判断 **$_GET['finish']** 是否设置,然后判断 **config.inc.php文件** 是否存在,安装后已存在,第三个判断cookie中 **__typecho_config** 参数是否为空,不为空。进入else分支。综上,具体构造如下图: + +![10](10.png) + +```php +$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config'))); +Typecho_Cookie::delete('__typecho_config'); +$db = new Typecho_Db($config['adapter'], $config['prefix']); +``` + +反序列化结果存储到 **$config** 变量中,然后将 **$config['adapter']** 和 **$config['prefix']** 作为 **Typecho_Db** 类的初始化变量创建类实例。我们可以在 **var/Typecho/Db.php** 文件中找到该类构造函数代码,具体如下: + +![11](11.png) + +上图代码 **第6行** ,对传入的 **$adapterName** 变量进行了字符串拼接操作,对于PHP而言,如果 **$adapterName** 类型为对象,则会调用该类 **__toString()** 魔术方法。可作为反序列化的一个触发点,我们全局搜索一下 **__toString()** ,查看是否有可利用的点。实际搜索时,会发现有三个类都定义了 **__toString()** 方法: + +![19](19.png) + +- 第一处 **var\Typecho\Config.php**: + + ![12](12.png) + + 调用 **serialize()** 函数进行序列化操作,会自动触发 **__sleep()** ,如果存在可利用的 **__sleep()** ,则可以进一步利用。 + +- 第二处 **var\Typecho\Db\Query.php**: + ![13](13.png) + + 该方法用于构建SQL语句,并没有执行数据库操作,所以暂无利用价值。 + +- 第三处**var\Typecho\Feed.php**: + + ![14](14.png) + + 在代码 **19行** , **$this->_items** 为类变量,反序列化可控,在代码 **27行** , **$item['author']->screenName** ,如果 **$item['author']** 中存储的类没有'screenName'属性或该属性为私有属性,此时会触发该类中的 **__get()** 魔法方法,这个可作为进一步利用的点,继续往下看代码,未发现有危险函数的调用。 + +记一波魔术方法及对应的触发条件,具体如下: + +```php +__wakeup() //使用unserialize时触发 +__sleep() //使用serialize时触发 +__destruct() //对象被销毁时触发 +__call() //在对象上下文中调用不可访问的方法时触发 +__callStatic() //在静态上下文中调用不可访问的方法时触发 +__get() //用于从不可访问的属性读取数据 +__set() //用于将数据写入不可访问的属性 +__isset() //在不可访问的属性上调用isset()或empty()触发 +__unset() //在不可访问的属性上使用unset()时触发 +__toString() //把类当作字符串使用时触发 +__invoke() //当脚本尝试将对象调用为函数时触发 +``` + +在 **var/Typecho/Request.php** 的 **Typecho_Request** 类中,我们发现 **__get()** 方法,跟踪该方法的调用,具体如下图:![15](15.png) + + **array_map()** 函数和 **call_user_func** 函数,都可以作为利用点,**$filter** 作为调用函数,**$value** 为函数参数,跟踪变量,看一下是否可控。这两个变量都来源于类变量,反序列化可控。从上面的分析中,可知当 **$item['author']** 满足一定条件会触发 **__get** 方法。 + +假设 **$item['author']** 中存储 **Typecho_Request** 类实例,此时调用 **$item['author']->screenName** ,在**Typecho_Request** 类中没有该属性,就会调用类中的 **__get($key)** 方法,**$key** 传入的值为 **scrrenName** 。参数传递过程如下:`$key='scrrenName'`=>`$this->_param[$key]`=>`$value` + +我们将 **$this->_param['scrrenName']** 的值设置为想要执行的函数,构造 **$this->_filter** 为对应函数的参数值,具体构造如下: + +![16](16.png) + +接下来我们去看一下 **Typecho_Feed** 类的构造,该类在 **var/Typecho/Feed.php** 文件中,代码如下: + +![14](14.png) + +上图代码 **第7行** ,满足 **self::RSS2** 与 **$this->_type** 相等进入该分支,所以 **$this->_type** 需要构造,**item['author']** 为触发点,需要构造 **$this_items** ,具体构造如下: + +![17](17.png) + +代码 **22行** 在实际利用没必要添加,install.php在代码 **54行** 调用 **ob_start()** 函数,该函数对输出内容进行缓冲,反序列化漏洞利用结束后,在**var\Typecho\Db.php**代码121行,触发异常,在 **var\Typecho\Common.php** 代码237行调用 **ob_end_clean()函数** 清除了缓冲区内容,导致无法看见执行结果,考虑在进入到异常处理前提前报错结束程序。由此构造该数据。执行结果如下: + +![18](18.png) + +## 修复建议 + +造成该漏洞的原因主要有两点: + +- 当 **config.inc.php** 文件存在的时,可绕过判断继续往下执行代码。 +- 传入反序列化函数的参数可控 + +修复方法:在 **install.php** 文件第一行判断 **config.inc.php** 是否存在,如果存在,则退出代码执行。 + +```php + +``` + +## 结语 + +看完了上述分析,不知道大家是否对反序列化利用有了一定的了解,文中用到的CMS可以从 [这里](https://github.com/typecho/typecho/archive/v1.1-15.5.12-beta.zip) 下载,当然文中若有不当之处,还望各位斧正。如果你对我们的项目感兴趣,欢迎发送邮件到 hongrisec@gmail.com 联系我们。**Day11** 的分析文章就到这里,我们最后留了一道CTF题目给大家练手,题目如下: + +```php +method = $method; + $this->args = $args; + $this->__conn(); + } + + function __conn() { + global $db_host, $db_name, $db_user, $db_pass, $DEBUG; + if (!$this->conn) + $this->conn = mysql_connect($db_host, $db_user, $db_pass); + mysql_select_db($db_name, $this->conn); + if ($DEBUG) { + $sql = "DROP TABLE IF EXISTS users"; + $this->__query($sql, $back=false); + $sql = "CREATE TABLE IF NOT EXISTS users (username VARCHAR(64), + password VARCHAR(64),role VARCHAR(256)) CHARACTER SET utf8"; + + $this->__query($sql, $back=false); + $sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')"; + $this->__query($sql, $back=false); + } + mysql_query("SET names utf8"); + mysql_query("SET sql_mode = 'strict_all_tables'"); + } + + function __query($sql, $back=true) { + $result = @mysql_query($sql); + if ($back) { + return @mysql_fetch_object($result); + } + } + + function login() { + list($username, $password) = func_get_args(); + $sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, md5($password)); + $obj = $this->__query($sql); + + if ( $obj != false ) { + define('IN_FLAG', TRUE); + $this->loadData($obj->role); + } + else { + $this->__die("sorry!"); + } + } + + function loadData($data) { + if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) { + return unserialize($data); + } + return []; + } + + function __die($msg) { + $this->__close(); + header("Content-Type: application/json"); + die( json_encode( array("msg"=> $msg) ) ); + } + + function __close() { + mysql_close($this->conn); + } + + function source() { + highlight_file(__FILE__); + } + + function __destruct() { + $this->__conn(); + if (in_array($this->method, array("login", "source"))) { + @call_user_func_array(array($this, $this->method), $this->args); + } + else { + $this->__die("What do you do?"); + } + $this->__close(); + } + + function __wakeup() { + foreach($this->args as $k => $v) { + $this->args[$k] = strtolower(trim(mysql_escape_string($v))); + } + } +} +class SoFun{ + public $file='index.php'; + + function __destruct(){ + if(!empty($this->file)) { + include $this->file; + } + } + function __wakeup(){ + $this-> file='index.php'; + } +} +if(isset($_GET["data"])) { + @unserialize($_GET["data"]); +} +else { + new HITCON("source", array()); +} + +?> +``` + +```php +//config.php + +``` + +```php +// flag.php + +``` + diff --git a/README.md b/README.md old mode 100755 new mode 100644 index cbcda04..971b296 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Part1部分属于项目 **第一阶段** 的内容,本阶段的内容题目素 * [ [红日安全]代码审计Day8 - preg_replace函数之命令执行 ](https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day8/files/README.md) * [[红日安全]代码审计Day9 - str_replace函数过滤不当](https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day9/files/README.md) * [[红日安全]代码审计Day10 - 程序未恰当exit导致的问题](https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day10/files/README.md) +* [[红日安全]代码审计Day11 - unserialize反序列化漏洞](https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day11/files/README.md) ## PHP-Audit-Labs题解