项目场景
在健康云信息服务上线后,通过专业的网络安全团队,对系统进行全面的检测。渗透检测结果显示系统存在明显漏洞:文件上传漏洞、手机验证码发送接口流控功能、SQL注入漏洞、越权漏洞和 jQuery 版 本信息等5项内容。
文件上传漏洞
漏洞证明
现有前端上传采用layui中的upload进行前端的文件大小、扩展名的验证,但是对于修改后缀名的危险文件无法进行过滤,需要采用服务端验证。
前端上传代码
upload.render({ elem: '#uploadlicense' , url: '?m=Index&a=indexDeal&act=upImg&fromType=license' , multiple: false , size: 512 , accept: 'file' , exts: 'jpg|png' , number: 1 , before: function (obj) { loadingIndex = layer.load(); } , done: function (res, index) { layer.close(loadingIndex); if (res.code != "0") { return layer.msg('上传失败:' + res.msg); } if (res.code == "0") { $("#user_license").val(res.imgUrl); var imgHtml = "<a href=\"" + res.imgUrl + "\" target=\"_blank\">预览</a>"; $("#pre_user_license").html(imgHtml); } } });
修复建议
1.在服务器端进行过滤。
2.服务器端读取文件的部分内容作判断,可防止攻击者伪装文件类型上传。
核心代码
$allow_type = array('application/pdf', 'image/png', 'image/jpeg', 'video/mp4'); //服务器端检查上传文件类型; $tmpname = $file['tmp_name']; $finfo = finfo_open(FILEINFO_MIME_TYPE);//返回 mime 类型 $mimetype = finfo_file($finfo, $tmpname); finfo_close($finfo); if (!in_array($mimetype, $allow_type)) { $res['code'] = "1"; $res['msg'] = "不支持该文件类型上传"; die(json_encode_lockdata($res)); }
FILEINFO服务器配置(宝塔)
- 安装FILEINFO插件
- 配置php.ini
- 安装之后我们需要重启php确保生效。
手机验证码流控功能完善
未对每分钟IP请求发送验证码次数限制,通过随机生成手机号批量发送验证码,造成验证码发送次数配额消耗,增大运营成本。
手机验证码发送接口流控功能不完全
修复建议
1.增加对IP访问此接口的流控功能。
2.增加验证码。
核心代码
增加验证码功能
<div class="layui-form-item"> <label class="layui-form-label" style="width: 20%;">验证码</label> <div class="layui-input-inline"><input type="text" name="captcha" id="captcha" autocomplete="off" class="layui-input"></div> <div class="layui-form-mid layui-word-aux"> <img src="?m=Login&a=loginDeal&act=getCode" id="getCode" alt="" title="点击刷新验证码" style="cursor: pointer;"><span class="x-red"> * </span> 发送手机短信验证</div> </div> <div class="layui-form-item"> <label class="layui-form-label" style="width: 20%;">短信验证码</label> <div class="layui-input-inline"><input type="text" name="smscode" id="smscode" lay-verify="smscode" autocomplete="off" class="layui-input" disabled="disabled"></div> <div class="layui-input-inline"> <input type="button" class="layui-btn layui-btn-primary" id="btnSendCode" disabled="disabled" value="获取验证码"> </div> </div>
$("#user_phone").change(function () { var mobile = $.trim($("#user_phone").val()); if (mobile.length == 11) { $("#smscode").attr("disabled", false).css({"background-color": "#fff"}); $("#btnSendCode").attr("disabled", false).removeClass("layui-btn layui-btn-primary").addClass("layui-btn layui-btn-normal"); //单击发送验证码; document.getElementById("btnSendCode").onclick = function () { var captcha = $("#captcha").val(); //获取网站验证码; $.getJSON("?m=Login&a=loginDeal&act=captcha", {mobile:mobile,captcha:captcha}, function (res) { if (res.code == '0') { layer.msg(res.msg, {icon: 1,time: 2000}); } else { if (wait != 60) { console.log("请" + wait + "秒后再试!"); return; } if (mobile == '') { console.log("请填写手机号码"); return; } $("#smscode").val(""); time(document.getElementById("btnSendCode")); $.getJSON("?m=Sms&a=smsDeal&act=login", {mobile: mobile}, function (res2) { //console.log(res2.total); if (res2.code == '0') { layer.msg(res2.msg, {icon: 1,time: 2000}); } else { //console.log(res2); } }); } }); } } else { $("#smscode").attr("disabled", true); $("#btnSendCode").attr("disabled", true).removeClass("layui-btn layui-btn-normal").addClass("layui-btn layui-btn-primary"); } });
增加5分钟同一IP发送5次短信的验证
- 验证5分钟内的发送条数
//读取数据库记录; $fromTime = time() - 300; $toTime = time(); $sql = "select log_id FROM " . $db->table('log') . " WHERE 1"; $sql .= " AND logs = 'smscode' AND log_ip ='" . getip() . "'"; $sql .= " AND log_time > '" . $fromTime . "' AND log_ip <'" . $toTime . "'"; $sql .= " ORDER BY log_id DESC"; $row = $db->queryall($sql); if (count($row) >= 5) { $res['code'] = 0; $res['msg'] = '5分钟内最多发送5次短信.'; $res['total'] = count($row); die(json_encode_lockdata($res)); } else { $send = SmsDemo::sendSms($mobile, $signName, $templateCode, $rmdCode, '', '', '', '', 1); $res['code'] = 1; $res['send'] = $send; $_SESSION['code'] = $rmdCode; die(json_encode_lockdata($res)); }
- 添加发送短信记录
$user_phone = get_param('mobile'); $captcha = isset($_GET["captcha"]) ? trim($_GET["captcha"]) : ''; if ($captcha != $_SESSION['authcode']) { $res['code'] = "0"; $res['msg'] = "验证码错误"; die(json_encode_lockdata($res)); } else { //增加数据库记录; addlogs($user_phone, 'smscode', '', time(), getip()); $res['code'] = "1"; $res['msg'] = "验证码通过"; //验证码自动销毁; session_destroy(); die(json_encode_lockdata($res)); }
SQL注入漏洞
web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作。
主页中调用的查询API存在SQL注入
- API访问参数传递
api/api.php?act=getMarkers&token=3cab7ce4142608c0f40c785b5ab5ca24&page=1&limit=300&keys=a&province=%E5%B1%B1%E4%B8%9C%E7%9C%81&city=&area=
- Limit字段存在堆叠注入
Payload:act=getMarkers&token=3cab7ce4142608c0f40c785b5ab5ca24&page=1&limit=300;SELECT SLEEP(5)#&keys=a&province=%E5%B1%B1%E4%B8%9C%E7%9C%81&city=&area=
漏洞证明
修复建议
- 输入过滤:严格控制输入数据的类型、长度,增加输入合法性判断;禁止出现一些特殊字符或关键词;
- 预编译SQL语句(参数化查询);
- 部署WAF;
核心代码
做参数过滤
@$p = get_safe2('page') == "" ? 1 : get_safe2('page');//获取用户选择的页码 @$pagesize = get_safe2('limit') == "" ? 600 : get_safe2('limit');//获取用户选择的每页显示多少条数据 @$limit = ($p - 1) * $pagesize;//偏移量
越权漏洞
通过修改cookie值,可越权查看编辑其他用户的信息。
漏洞证明
核心代码
原来只通过user_id进行参数传递,由于使用自增ID,容易被猜测出来。现在同步增加$user_name查询参数,进行强制限制。
case "show"; $user_id = $_COOKIE['user_id']; //判断是否一致,防止修改查看 2022-11-09 BY Poleung; $user_name = $_COOKIE['dbUser']; $row = $db->fetch('user', '*', array('user_id' => $user_id, 'user_name' => $user_name), ' user_id DESC'); //不存在数据; if (!$row) { redirect('?m=Pop&a=tips&act=tips&tips_id=7'); } $pieces = explode(',', $row['user_type']); break;
#脆弱的Javascript库
修复建议
升 级 JavaScript 库 到 最 新 版 本 , 官 方 网 址 : https://jqueryui.com/download/,临时解决方案:隐藏 jQuery 版 本信息,避免被攻击者识别出版本号
@漏刻有时