- 首发于 https://blog.lou00.top/index.php/archives/12/
首先是判断安装的条件
//第一
file_exists(dirname(__FILE__) . '/config.inc.php')
//第二
$db = Typecho_Db::get();
$installed = $db->fetchRow($db->select()->from('table.options')->where('name = ?', 'installed'));
挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
if (empty($_SERVER['HTTP_REFERER'])) {
exit;
}
$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port'])) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
没感觉这步有啥用,referer和host头都是可以自己设的
获取版本号以及语言
$options = new stdClass();
$options->generator = 'Typecho ' . Typecho_Common::VERSION;
list($soft, $currentVersion) = explode(' ', $options->generator);
$options->software = $soft;
$options->version = $currentVersion;
list($prefixVersion, $suffixVersion) = explode('/', $currentVersion);
/** 获取语言 */
$lang = _r('lang', Typecho_Cookie::get('__typecho_lang'));
$langs = Widget_Options_General::getLangs();
if (empty($lang) || (!empty($langs) && !isset($langs[$lang]))) {
$lang = 'zh_CN';
}
if ('zh_CN' != $lang) {
$dir = defined('__TYPECHO_LANG_DIR__') ? __TYPECHO_LANG_DIR__ : __TYPECHO_ROOT_DIR__ . '/usr/langs';
Typecho_I18n::setLang($dir . '/' . $lang . '.mo');
}
Typecho_Cookie::set('__typecho_lang', $lang);
接下来就比较乱了,它把html与php写在了一起,都9012年了,不推荐这种写法,按照访问顺序介绍好了
第一次访问
第一次访问是不带任何请求的,只是但存访问其install.php
这里可以看到其多次使用了_e
函数
// /var/Typecho/Common.php
function _e() {
$args = func_get_args();
echo call_user_func_array('_t', $args);
}
function _t($string) {
if (func_num_args() <= 1) {
return Typecho_I18n::translate($string);
} else {
$args = func_get_args();
array_shift($args);
return vsprintf(Typecho_I18n::translate($string), $args);
}
}
_e
的作用是如果传入的是数组的话可以通过call_user_func_array
调用_t
,然后百度了一下I18n
原来是国际化的意思,跟进Typecho_I18n::translate
public static function translate($string)
{
self::init();
return self::$_loaded ? self::$_loaded->translate($string) : $string;
private static function init()
{
/** GetText支持 */
if (false === self::$_loaded && self::$_lang && file_exists(self::$_lang)) {
self::$_loaded = new Typecho_I18n_GetTextMulti(self::$_lang);
}
}
默认self::$_loaded = flase
而且没有那个文件
所以直接返回了字符
第二次访问
点击我准备好了, 开始下一步
进行下一步
发现它自动给你带上一个config
的post请求
现在访问的应该是install.php/?config
首先是_p
函数
function _p($adapter) {
switch ($adapter) {
case 'Mysql':
return Typecho_Db_Adapter_Mysql::isAvailable();
case 'Mysqli':
return Typecho_Db_Adapter_Mysqli::isAvailable();
case 'Pdo_Mysql':
return Typecho_Db_Adapter_Pdo_Mysql::isAvailable();
case 'SQLite':
return Typecho_Db_Adapter_SQLite::isAvailable();
case 'Pdo_SQLite':
return Typecho_Db_Adapter_Pdo_SQLite::isAvailable();
case 'Pgsql':
return Typecho_Db_Adapter_Pgsql::isAvailable();
case 'Pdo_Pgsql':
return Typecho_Db_Adapter_Pdo_Pgsql::isAvailable();
default:
return false;
}
}
// 以Pdo_SQLite为例
// /var/Typecho/Db/Adapter/Pdo/SQLite.php
public static function isAvailable()
{
return parent::isAvailable() && in_array('sqlite', PDO::getAvailableDrivers());
}
会寻找对应的函数,如果函数存在则说明扩展存在在数据库配置是会多一个选项
第三次访问
第三次访问增加了一堆post请求并通过_rForm
赋值
function _rFrom() {
$result = array();
$params = func_get_args();
foreach ($params as $param) {
$result[$param] = isset($_REQUEST[$param]) ?
(is_array($_REQUEST[$param]) ? NULL : $_REQUEST[$param]) : NULL;
}
return $result;
}
检测数据库连接
try {
$installDb->query('SELECT 1=1');
} catch (Typecho_Db_Adapter_Exception $e) {
$success = false;
echo '
';
} catch (Typecho_Db_Exception $e) {
$success = false;
echo ' ';
}
存在时可以对应前面的一个安装条件,以免反回404
if($success) {
// 重置原有数据库状态
if (isset($installDb)) {
try {
$installDb->query($installDb>update('table.options')->rows(array('value' => 0))->where('name = ?', 'installed'));
} catch (Exception $e) {
// do nothing
}
}
下面就是我们的config.inc.php
$lines = array_slice(file(__FILE__), 1, 31);
$lines[] = "
/** 定义数据库参数 */
\$db = new Typecho_Db('{$adapter}', '" . _r('dbPrefix') . "');
\$db->addServer(" . (empty($config) ? var_export($dbConfig, true) : $config) . ", Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set(\$db);
";
第四次访问
创建好之后
跳转到/install.php?start
接下来就是一个比较有意思的点了
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
跟入
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')
可以自己控制
意思说如果你可以任意删除文件,那么利用删除config.inc.php
直接将洞扩大为RCE,注:www-data用户要有config.inc.php
权限
接下来一大段的数据库初始化操作,由于太长了就不贴代码了
总之在数据库执行错误时,也就是原有库中存在数据时会返回选项,删除(postdelete=1
)或者使用原有数据(postgoahead=1
)
使用原有数据
会跳转header('Location: ./install.php?finish&use_old');
删除
if(_r('delete')) {
//删除原有数据
$dbPrefix = $config['prefix'];
$tableArray = array($dbPrefix . 'comments',$dbPrefix . 'contents', $dbPrefix . 'fields',$dbPrefix . 'metas', $dbPrefix . 'options', $dbPrefix . 'relationships', $dbPrefix . 'users',);
foreach($tableArray as $table) {
if($type == 'Mysql') {
$installDb->query("DROP TABLE IF EXISTS `{$table}`");
} elseif($type == 'Pgsql') {
$installDb->query("DROP TABLE {$table}");
} elseif($type == 'SQLite') {
$installDb->query("DROP TABLE {$table}");
}
}
echo '
';
}
第五次访问
?finish
变回1,代表已完成,之后访问都会是404了
$db->query($db->update('table.options')->rows(['value' => 1])->where('name = ?', 'installed'));
到此结束