DouPHP审计记录(一次失败的审计)
DouPHP审计记录
0x01 概述
DouPHP 漳州豆壳网络科技有限公司开发的一款轻量级企业网站管理系统,基于PHP+Mysql架构的,可运行在Linux、Windows、MacOSX、Solaris等各种平台上,系统搭载Smarty模板引擎,支持自定义伪静态,前台模板采用DIV+CSS设计,后台界面设计简洁明了,功能简单易具有良好的用户体验,稳定性好、扩展性及安全性强,可通过后台在线安装模块,比如会员模块、订单模块等,可面向中小型站点提供网站建设解决方案。
本次审计仅以学习为目的,在本地电脑上搭建测试。
0x02 SQL注入
因为这个系统没有设置访问路由,所以就直接从前台的几个文件开始审计了。这里直接记录值得我们关注的点:
init.php
index.php开头包含了/include/init.php进行了一些初始化设置
init.php中,关闭了magic_quotes_runtime
67行new了一个防火墙类,并且在88行调用dou_firewall()。我们继续跟进看看做了哪些操作
跟进dou_magic_quotes()
跟进addslshes_deep()
可以看到最终调用的是addslshes_deep(),不管$_GET、$_POST、$_COOKIE、$_REQUEST传入的是单独一个值还是数组,全部都会经过万恶的addslashes()洗礼。而且通过查看配置,连接数据库设置的编码是utf-8,所以只要调用了dou_firewall()的地方,我们只能找找整型SQL注入。
但是通读全文sql语句后也没有发现未被引号包裹的参数,而且在参数在放入查询语句前都进行了正则匹配,只允许存在[0-9a-zA-Z_.],可以说这套系统开发的很规范。并且所有我发现的可能存在二次注入的地方,都对参数值进行了正则匹配或转义。
0x03 逻辑漏洞
既然没有注入,只能审计其他类型的漏洞了。
通过本地搭建环境观察,前台的都是展示信息的功能,只有后台登录入口的找回密码功能可能出现逻辑漏洞。
位置:admin/login.php 136行开始到最后,是找回密码的处理代码
elseif ($rec == 'password_reset_post') {
// Action操作项的初始化
$action = $check->is_rec($_POST['action']) ? $_POST['action'] : 'default';
// 验证管理员用户名
if (!$check->is_username(trim($_POST['user_name']))) {
$dou->dou_msg($_LANG['login_password_reset_fail'], ROOT_URL . ADMIN_PATH . '/login.php?rec=password_reset', 'out');
} else {
$user_name = trim($_POST['user_name']);
}
// 验证对应的管理员邮箱
if (!$check->is_email(trim($_POST['email']))) {
$dou->dou_msg($_LANG['login_password_reset_fail'], ROOT_URL . ADMIN_PATH . '/login.php?rec=password_reset', 'out');
} else {
$email = trim($_POST['email']);
}
// 找回密码提交、重置密码提交
if ($action == 'default') { // 密码找回提交操作
// 根据用户名和邮箱获取对应的用户信息
$user = $dou->get_row('admin', '*', "user_name = '$user_name' AND email = '$email'");
// 对应的用户信息不存在
if (!$user)
$dou->dou_msg($_LANG['login_password_reset_wrong'], ROOT_URL . ADMIN_PATH . '/login.php?rec=password_reset', 'out');
// CSRF防御令牌验证
$firewall->check_token($_POST['token'], 'password_reset');
// 生成包含找回密码链接的邮件正文
$time = time();
$code = substr(md5($user['user_name'] . $user['email'] . $user['password'] . $time . $user['last_login'] . DOU_SHELL) , 0 , 16) . $time;
$site_url = rtrim(ROOT_URL, '/');
$body = $user['user_name'] . $_LANG['login_password_reset_body_0'] . ROOT_URL . ADMIN_PATH . '/login.php?rec=password_reset' . '&uid=' . $user['user_id'] . '&code=' . $code . $_LANG['login_password_reset_body_1'] . $_CFG['site_name'] . '. ' . $site_url;
// 发送找回密码邮件
if ($dou->send_mail($user['email'], $_LANG['login_password_reset'], $body)) {
$dou->dou_msg($_LANG['login_password_mail_success'] . $user['email'], ROOT_URL . ADMIN_PATH . '/login.php', 'out', '30');
} else {
$dou->dou_msg($_LANG['mail_send_fail'], ROOT_URL . ADMIN_PATH . '/login.php?rec=password_reset', 'out', '30');
}
} elseif ($action == 'reset') { // 密码重置操作
// 获取会员ID和安全码
$user_id = $check->is_number($_POST['user_id']) ? $_POST['user_id'] : '';
$code = preg_match("/^[a-zA-Z0-9]+$/", $_POST['code']) ? $_POST['code'] : '';
// 验证密码
if (!$check->is_password($_POST['password'])) {
$dou->dou_msg($_LANG['manager_password_cue'], '', 'out');
} elseif (($_POST['password_confirm'] !== $_POST['password'])) {
$dou->dou_msg($_LANG['manager_password_confirm_cue'], '', 'out');
}
// 找回密码操作
if ($dou->check_password_reset($user_id, $code)) {
// 重置密码
$sql = "UPDATE " . $dou->table('admin') . " SET password = '" . md5($_POST['password']) . "' WHERE user_id = '$user_id'";
$dou->query($sql);
$dou->dou_msg($_LANG['login_password_reset_success'], ROOT_URL . ADMIN_PATH . '/login.php', 'out', '15');
} else {
$dou->dou_msg($_LANG['login_password_reset_fail'], ROOT_URL . ADMIN_PATH . '/login.php', 'out', '15');
}
}
}
?>
elseif里的$rec是通过$_REQUEST['rec']获取的,所以我们只要get或post传入rec=password_reset_post就可以进入这一层代码
看下图,开头获取了$action后,对post传入的username和email进行了正则匹配,想要通过这两个参数做动作几乎是不可能的。
下面是$check->is_username和$check->is_email的处理代码
function is_username($username) {
if (preg_match("/^[a-zA-Z]{1}([0-9a-zA-Z]|[._]){3,19}$/", $username)) {
return true;
}
}
function is_email($email) {
if (preg_match("/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/", $email)) {
return true;
}
}
接着看$action=='default'的操作
// 找回密码提交、重置密码提交
if ($action == 'default') { // 密码找回提交操作
// 根据用户名和邮箱获取对应的用户信息
$user = $dou->get_row('admin', '*', "user_name = '$user_name' AND email = '$email'");
// 对应的用户信息不存在
if (!$user)
$dou->dou_msg($_LANG['login_password_reset_wrong'], ROOT_URL . ADMIN_PATH . '/login.php?rec=password_reset', 'out');
// CSRF防御令牌验证
$firewall->check_token($_POST['token'], 'password_reset');
// 生成包含找回密码链接的邮件正文
$time = time();
$code = substr(md5($user['user_name'] . $user['email'] . $user['password'] . $time . $user['last_login'] . DOU_SHELL) , 0 , 16) . $time;
$site_url = rtrim(ROOT_URL, '/');
$body = $user['user_name'] . $_LANG['login_password_reset_body_0'] . ROOT_URL . ADMIN_PATH . '/login.php?rec=password_reset' . '&uid=' . $user['user_id'] . '&code=' . $code . $_LANG['login_password_reset_body_1'] . $_CFG['site_name'] . '. ' . $site_url;
// 发送找回密码邮件
if ($dou->send_mail($user['email'], $_LANG['login_password_reset'], $body)) {
$dou->dou_msg($_LANG['login_password_mail_success'] . $user['email'], ROOT_URL . ADMIN_PATH . '/login.php', 'out', '30');
} else {
$dou->dou_msg($_LANG['mail_send_fail'], ROOT_URL . ADMIN_PATH . '/login.php?rec=password_reset', 'out', '30');
}
首先将post传入的用户名和邮箱放进数据库进行查询,无返回结果就结束了处理。所以这里没办法通过修改邮箱达到任意邮箱接受重置密码链接的目的。
之后就开始构造密码找回链接,并将链接发送到用户对应的邮箱中。
密码重置链接中包含的$code(168行),是将多个用户信息拼接后进行md5计算得出的,所以我们也无法直接构造密码重置链接。图中170行生成$body的代码较长,没法完全截图,其实$body后面有拼接$code。这个可以在上面我放出的密码找回功能完整代码中看到。
重置密码链接打印出来是这样的
通过分析链接可以知道$rec=password_reset,于是我们直接看login.php里$rec == 'password_reset'的代码
这里116行将$user_id和$code放进$dou->check_passwrod_reset()里,我们跟进看一下做了哪些操作
可以看到96行取出$code前面16位, 98行重新生成了$code,101行拿$code与$get_code进行对比。这里对$code进行了校验,所以这里我们也无法绕过限制。
但是这里还不是最终处理修改密码的地方,最终修改密码的地方如下:
但是遗憾的是,这里的191行也对$code进行了校验,所以找回密码功能审计到此,我无法发现漏洞。
0x04 文件上传
douphp前台没有文件上传的功能,因此这个审计都是在审计admin/里的文件
通过阅读,发现所有的文件上传功能都是使用File类进行处理,这里举例其中一个来审计
admin/system.php
当$rec=='update'时有文件上传操作,103行调用了$file->update(),我们先看看$file怎么来的。
看20行,$file是实例化了File类的对象
跟进File类,看看__construct做了如下操作:
接着重新回去看$file->upload()
这里通过点分割成数组后,取得文件后缀,并在$this->file_type中寻找是否匹配。从上面的__construct可以看到$file_type只有几个图片类型后缀,因此这里的白名单过滤,无法绕过。
接下来就直接拼接文件名,并上传文件了。所以文件上传的功能也没有挖到洞。
0x05 其他漏洞
csrf
后面还看了下后台是否存在csrf,结果是后台功能在执行操作前都会先验证csrftoken。下面我们看下csrf如何生成,是否可自行构造。
阅读下面的代码可以看到是随机数生成的。
未授权访问
未授权访问的审计很粗暴,后台页面文件开头包含了admin/include/init.php中对session做了校验,因此直接看admin目录目录的文件是否有未包含init.php的文件即可。但是最后手动翻了一些并没有发现。
0x06 总结
本次审计一共就挖了几个后台的存储xss,较大的收获也许就是对代码审计有了更深的认识,更加熟悉审计流程。而且也在这套代码中学习到了安全开发的一些技巧、思路。同时也认识到自己的不足,就是代码阅读速度较慢。
希望在后期能通过实战提高自己吧哈哈。