zabbix密码复杂度有效期安全增强,符合三级等保要求。
一、关于三级等保要求
1.zabbix没有设置密码复杂度功能,密码有效期功能。
2.zabbix没有失败处理功能。
3.zabbix具备超时退出功能。
4.不符合双因素登录认证功能。
二、整改
1.zabbix 系统不具备登录失败处理功能,运维终端未设置超时退出功能。
添加失败处理功能,失败3次锁定30分钟,修改include/defines.inc.php
define('ZBX_LOGIN_ATTEMPTS', 3); define('ZBX_LOGIN_BLOCK', 1800); // sec
截图如下:
使用zhangyifan帐号测试输入5次密码,可以看到状态已锁定,截图如下:
添加自动超时退出功能,截图如下:
2.zabbix 系统和应用系统未实现密码定期更换
整改证据:zabbix数据库users表添加10位时间戳过期字段,截图如下:
程序添加90天过期字段,此处有两个文件需要修改。include/classes/api/services/CUser.php和include/schema.inc.php。
其中CUser.php创建用户时候新增代码$ins_users[0]['passwd_expired'] = time() + 3600*24*90; 代表90天后过期:
public function create(array $users) { $this->validateCreate($users); $ins_users = []; foreach ($users as $user) { unset($user['usrgrps'], $user['user_medias']); $ins_users[] = $user; } //创建用户时候添加过期时间 $ins_users[0]['passwd_expired'] = time() + 3600 * 24 * 90; $userids = DB::insert('users', $ins_users);
其中CUser.php更新用户设置时候新增代码$upd_user['passwd_expired'] = time() + 3600 * 24 * 90;代表90天后过期:
public function update(array $users) { $this->validateUpdate($users, $db_users); $upd_users = []; foreach ($users as $user) { $db_user = $db_users[$user['userid']]; $upd_user = []; // strings $field_names = ['alias', 'name', 'surname', 'autologout', 'passwd', 'refresh', 'url', 'lang', 'theme']; foreach ($field_names as $field_name) { if (array_key_exists($field_name, $user) && $user[$field_name] !== $db_user[$field_name]) { $upd_user[$field_name] = $user[$field_name]; } } // integers foreach (['autologin', 'type', 'rows_per_page'] as $field_name) { if (array_key_exists($field_name, $user) && $user[$field_name] != $db_user[$field_name]) { $upd_user[$field_name] = $user[$field_name]; } } if ($upd_user) { //添加过期时间 $upd_user['passwd_expired'] = time() + 3600 * 24 * 90; $upd_users[] = [ 'values' => $upd_user, 'where' => ['userid' => $user['userid']] ]; } }
在schema.inc.php中'users'段,最后加入数据库的过期字段。
'users' => [ 'key' => 'userid', 'fields' => [ 'userid' => [ 'null' => false, 'type' => DB::FIELD_TYPE_ID, 'length' => 20 ], ...省略一大坨... 'rows_per_page' => [ 'null' => false, 'type' => DB::FIELD_TYPE_INT, 'length' => 10, 'default' => 50 ], 'passwd_expired' => [ 'null' => false, 'type' => DB::FIELD_TYPE_INT, 'length' => 10 ] ] ],
完成以上内容,只是在创建用户和修改用户设置的时候,触发更新数据库中密码过期时间字段修改。
接下来写一个巡检代码,定期巡检数据库表中zabbix过期的用户。加到crontab里(以前写的,20231212修改使用CUser.php原生登录时候检测是否过期)
#!/usr/bin/python3 # -*- coding: utf-8 -*- # @Time : 2022/2/22 14:34 # @File : mysql.py # @Software: PyCharm import pymysql import time class Localmysqlopt(object): def __init__(self): self.db = pymysql.connect(host="localhost", user="zabbix", password="你的数据库密码", port=3306, database="zabbix", charset="utf8") self.cursor = self.db.cursor() def dml_execute(self, sql=None): try: with self.cursor as cursor: cursor.execute(sql) self.db.commit() except Exception as e: self.db.rollback() print(e) def chaxun(self): sql = "select userid,passwd_expired from users where passwd_expired < {0} and attempt_failed < 3 ".format(int(time.time())) self.cursor.execute(sql) results = self.cursor.fetchall() if results: for value in results: attempt_failed,userid = 5,value[0] #修改登录失败次数,实现锁定账户 sql = "update users set attempt_failed = '{0}' where userid = '{1}'".format(attempt_failed,userid) self.dml_execute(sql) if __name__=='__main__': db = Localmysqlopt() db.chaxun()
ps等保整改截图时候偷个懒,只截一个update方法中的内容。截图如下:
真是难为我了,做为一个运维又改php代码,又写Python的。
上面的python方法太粗暴,后面再次检查的时候改成了PHP代码。zabbix4.X版本的在CUser.php登录字段添加过期时间检测
$db_user = $this->findByAlias($user['user'], ($config['ldap_case_sensitive'] == ZBX_AUTH_CASE_SENSITIVE), $config['authentication_type'], true ); //找到上面的位置,往下添加 // Check if user is blocked. if ($db_user['passwd_expired'] <= time()){ self::exception(ZBX_API_ERROR_PARAMETERS, '密码已过期,请联系管理员重置密码'); };
3.zabbix 系统未采用密码技术保证重要数据在存储过程中的完整性。
整改证据:zabbix5.X添加密码复杂度相关代码,修改文件./app/controllers/CControllerUserUpdateGeneral.php结尾部分
if ($password1 !== null && $password2 !== null) { if ($password1 !== $password2) { error(_('Both passwords must be equal.')); return false; } if ($password1 === '' && !$this->allow_empty_password) { error(_s('Incorrect value for field "%1$s": %2$s.', _('Password'), _('cannot be empty'))); return false; } /** * 检查密码复杂度 */ if (strlen($password1) <= 8) { //必须大于8个字符 error(_('密码必须大于8字符')); return false; } if (preg_match("/^[0-9]+$/", $password1)) { //必须含有特殊字符 error(_('密码不能全是数字,请包含数字,字母大小写或者特殊字符')); return false; } if (preg_match("/^[a-zA-Z]+$/", $password1)) { error(_('密码不能全是字母,请包含数字,字母大小写或者特殊字符')); return false; } if (preg_match("/^[0-9A-Z]+$/", $password1)) { error(_('请包含数字,字母大小写或者特殊字符')); return false; } if (preg_match("/^[0-9a-z]+$/", $password1)) { error(_('请包含数字,字母大小写或者特殊字符')); return false; } } return true; } }
zabbix4.X修改web根目录的users.php目录第155行-182行
elseif (hasRequest('add') || hasRequest('update')) { $update = hasRequest('userid'); $usrgrps = getRequest('user_groups', []); /** * 检查密码复杂度 */ $password1 = getRequest('password1', ''); if (strlen($password1) <= 8) { //必须大于8个字符 show_messages(false,$update,_('密码必须大于8字符')); exit; } if (preg_match("/^[0-9]+$/", $password1)) { //必须含有特殊字符 show_messages(false,$update,_('密码不能全是数字,请包含数字,字母大小写或者特殊字符')); exit; } if (preg_match("/^[a-zA-Z]+$/", $password1)) { show_messages(false,$update,_('密码不能全是字母,请包含数字,字母大小写或者特殊字符')); exit; } if (preg_match("/^[0-9A-Z]+$/", $password1)) { show_messages(false,$update,_('请包含数字,字母大小写或者特殊字符')); exit; } if (preg_match("/^[0-9a-z]+$/", $password1)) { show_messages(false,$update,_('请包含数字,字母大小写或者特殊字符')); exit; } // password validation if (getRequest('password1', '') != getRequest('password2', '')) { show_error_message($update ? _('Cannot update user. Both passwords must be equal.') : _('Cannot add user. Both passwords must be equal.') ); }
截图如下:
测试修改zhangyifan密码为纯数字,提示密码复杂度不符合要求。截图如下:
测试修改zhangyifan帐号为3个字符,提示密码长度不符合要求。截图如下:
4.zabbix 系统未采用双因素认证登录。
目标改成如下这样,先验证图形验证码是否正确,然后发送微信验证码。
需要修改和新增以下文件
index.php判断是否提交了数据,如果提交数据 调用验证码效验功能CWebUser.php
// login via form if (hasRequest('enter')){ CWebUser::CheckValidatingCode(getRequest('wechatcode')); }
index.php添加validatingcode和wechatcode节点
$fields = [ 'name' => [T_ZBX_STR, O_NO, null, null, 'isset({enter}) && {enter} != "'.ZBX_GUEST_USER.'"', _('Username')], 'password' => [T_ZBX_STR, O_OPT, null, null, 'isset({enter}) && {enter} != "'.ZBX_GUEST_USER.'"'], 'sessionid' => [T_ZBX_STR, O_OPT, null, null, null], 'reconnect' => [T_ZBX_INT, O_OPT, P_SYS, null, null], 'enter' => [T_ZBX_STR, O_OPT, P_SYS, null, null], 'validatingcode' => [T_ZBX_STR, O_OPT, P_SYS, null, null], 'wechatcode' => [T_ZBX_STR, O_OPT, P_SYS, null, null], 'autologin' => [T_ZBX_INT, O_OPT, null, null, null], 'request' => [T_ZBX_STR, O_OPT, null, null, null], 'form' => [T_ZBX_STR, O_OPT, null, null, null] ];
insertvalidatingcode.php判断图形验证码是否正确
session_start(); if (!$_GET['yzm']){ echo "<script>window.alert('先输入图形验证码!'); setTimeout(function(){ window.close(); }, 0);</script>"; exit; } if (strtolower($_GET['yzm']) != strtolower($_SESSION['imagecode'])){ echo "<script>window.alert('图形验证码无效'); setTimeout(function(){ window.close(); }, 0);</script>"; exit; }
insertvalidatingcode.php生成微信验证码
$verificationCode = generateVerificationCode(); function generateVerificationCode() { // 实现您的验证码生成逻辑 // 例如,生成一个随机的四位验证码 return mt_rand(100000, 999999); }
insertvalidatingcode.php插入数据库验证码
require_once dirname(__FILE__).'/include/db.inc.php'; require_once dirname(__FILE__).'/include/config.inc.php'; $result = insertvalidatingcode($verificationCode); /** * 验证码插入数据库 * @param string $verificationCode * @return bool */ function insertvalidatingcode($verificationCode){ $currentTimestamp = time(); $currentDateTime = date('Y-m-d H:i:s', $currentTimestamp); $userIP = $_SERVER['HTTP_REMOTE_HOST']; $useragent = $_SERVER['HTTP_USER_AGENT']; $result = DBexecute( 'INSERT INTO validating_code (date,code,ip,useragent,status)'. ' VALUES ('.zbx_dbstr($currentDateTime).','.zbx_dbstr($verificationCode).','.zbx_dbstr($userIP).','.zbx_dbstr($useragent).',0)' ); return $result; }
insertvalidatingcode.php插入成功后调用微信通知
require_once dirname(__FILE__).'/sendmessage.inc.php';
$result = insertvalidatingcode($verificationCode); if ($result) { // 创建 MyClass 的实例 $weixin = new WeixinMessage(); // 调用 MyClass 的方法 $content = "您的zabbix验证码是" . $verificationCode .",验证码有效期5分钟。请不要转告其他人【身边云】"; $res = $weixin->send($content); echo '<script>window.close();</script>'; } else { echo '验证码插入失败!'; }
sendmessage.inc.php发送微信通知
<?php /** * 微信公众号信息处理 */ class WeixinMessage { //corpid public $corpid = '你的corpid '; //sercret public $corpsecret = '你的corpsecret'; //微信发消息api public $weixinSendApi = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token='; /** * 请求微信Api,获取AccessToken */ public function getAccessToken() { error_reporting(E_ALL); //临时存放 并不安全 $filePath = ROOT.'cache/weixinToken.txt'; $tokenInfo = array(); if(is_file($filePath)){ $tokenInfo = json_decode(file_get_contents($filePath),TRUE); } if(!isset($tokenInfo['access_token']) || time()>$tokenInfo['expires_in']){ //更新access_token $getAccessTokenApi = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$this->corpid}&corpsecret={$this->corpsecret}"; $jsonString = $this->curlGet($getAccessTokenApi); $jsonInfo = json_decode($jsonString,true); if(isset($jsonInfo['access_token'])) { $jsonInfo['expires_in'] = time() + 7100; file_put_contents($filePath, json_encode($jsonInfo)); } $tokenInfo = $jsonInfo; } if(isset($tokenInfo['access_token']) && $tokenInfo['expires_in']>time()){ return $tokenInfo['access_token']; } else { return FALSE; } } /** * 发信息接口 * * @author wanghan * @param $content 发送内容 * @param $touser 接收的用户 @all全部 多个用 | 隔开 * @param $toparty 接收的群组 @all全部 多个用 | 隔开 * @param $totag 标签组 @all全部 多个用 | 隔开 * @param $agentid 应用id * @param $msgtype 信息类型 text=简单文本 */ public function send($content,$touser='@all',$toparty='',$totag='',$agentid=1000003,$msgtype='text') { $api = $this->weixinSendApi.$this->getAccessToken(); $postData = array( 'touser' => $touser, 'toparty' => $toparty, 'totag' => $totag, 'msgtype' => $msgtype, 'agentid' => $agentid, 'text' => array( 'content' => urlencode($content) ) ); $postString = urldecode(json_encode($postData)); $ret = $this->curlPost($api,$postString); $retArr = json_decode($ret,TRUE); if(isset($retArr['errcode']) && $retArr['errcode'] == 0) { return true; } else { return false; } } /** * Curl Post数据 * @param string $url 接收数据的api * @param string $vars 提交的数据 * @param int $second 要求程序必须在$second秒内完成,负责到$second秒后放到后台执行 * @return string or boolean 成功且对方有返回值则返回 */ function curlPost($url, $vars, $second=30) { $ch = curl_init(); curl_setopt($ch,CURLOPT_TIMEOUT,$second); curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($ch,CURLOPT_POST, 1); curl_setopt($ch,CURLOPT_POSTFIELDS,$vars); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,FALSE); // curl_setopt($ch, CURLOPT_HTTPHEADER, array( // 'Content-Type: application/json; charset=utf-8', // 'Content-Length: ' . strlen($vars)) // ); $data = curl_exec($ch); curl_close($ch); if($data) return $data; return false; } /** * CURL get方式提交数据 * 通过curl的get方式提交获取api数据 * @param string $url api地址 * @param int $second 超时时间,单位为秒 * @param string $log_path 日志存放路径,如果没有就不保存日志,还有存放路径要有读写权限 * @return true or false */ function curlGet($url,$second=30,$log_path='', $host='', $port='') { $ch = curl_init(); curl_setopt($ch,CURLOPT_TIMEOUT,$second); curl_setopt($ch, CURLOPT_URL,$url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,FALSE); if(!empty($host)){ curl_setopt($ch,CURLOPT_HTTPHEADER,$host); } if(!empty($port)){ curl_setopt($ch,CURLOPT_PORT,$port); } $data = curl_exec($ch); $return_ch = curl_errno($ch); curl_close($ch); if($return_ch!=0) { if(!empty($log_path)) file_put_contents($log_path,curl_error($ch)."\n\r\n\r",FILE_APPEND); return false; } else { return $data; } } }
captcha.php生成图形验证码
<?php session_start(); // 创建空白图像,宽度为 80 像素,高度为 30 像素 $image = imagecreatetruecolor(70, 26); // 设置图像的背景为透明 imagealphablending($image, false); imagesavealpha($image, true); $transparent = imagecolorallocatealpha($image, 0, 0, 0, 127); imagefill($image, 0, 0, $transparent); // 生成随机验证码文本 $code = generateRandomCode(4); $_SESSION['imagecode'] = $code; // 设置验证码文本颜色 $textColor = imagecolorallocate($image, 255, 255, 255); // 设置验证码字体路径 $fontPath = './fonts/graphfont.ttf'; // 绘制验证码文本到图像上 imagettftext($image, 15, 0, 10, 20, $textColor, $fontPath, $code); // 添加干扰元素 for ($i = 0; $i < 15; $i++) { // 随机颜色 $color = imagecolorallocate($image, rand(1,255), rand(1,255), rand(1,255)); // 随机位置 $x = rand(0, 70); $y = rand(0, 26); // 绘制小圆点 imagesetpixel($image, $x, $y, $color); // 绘制线条 if ($i % 2 == 0) { imageline($image, $x, $y, $x + 10, $y + 10, $color); } else { imageline($image, $x, $y + 10, $x + 10, $y, $color); } } // 设置 HTTP 头部,指定图像类型为 PNG header('Content-Type: image/png'); // 输出图像到浏览器或保存到文件 imagepng($image); // 释放图像资源 imagedestroy($image); // 生成随机验证码函数 function generateRandomCode($length = 6) { $characters = 'ASDFGHJKLZXCVBNMQWERTYUIOPabcdefghijklmnopqrstuvwxyz1234567890'; $code = ''; for ($i = 0; $i < $length; $i++) { $code .= $characters[rand(0, strlen($characters) - 1)]; } return $code; } ?>
./include/views/general.login.php 登录页面增加发送验证码
echo ' <script> function changeImage() { var captchaImages = document.getElementsByName(\'picyanzhengma\')[0]; captchaImages.addEventListener(\'click\', function() { this.src = \'/zabbix/captcha.php?\' + new Date().getTime(); }); } window.onload = function() { // 获取输入框元素 var inputElement = document.getElementById(\'validatingcode\'); // 绑定 keyup 事件监听器 inputElement.addEventListener(\'keyup\', function() { // 获取输入框的值 var inputValue = inputElement.value; // 拼接新的 URL var newUrl = \'insertvalidatingcode.php?yzm=\' + encodeURIComponent(inputValue); // 将新的 URL 赋值给 <a> 标签的 href 属性 document.getElementById(\'toyanzheng\').href = newUrl; }); }; </script> ';
->addItem([ new CLabel(_('用户'), 'name'), (new CTextBox('name'))->setAttribute('autofocus', 'autofocus'), $error ]) ->addItem([new CLabel(_('密码'), 'password'), (new CTextBox('password'))->setType('password')]) ->addItem([new CLabel(_('图形验证码 '), 'validatingcode'), (new CImg('captcha.php','picyanzhengma'))->setAttribute('onclick','changeImage()'), (new CTextBox('validatingcode'))->setAttribute('maxlength', '4')->setAttribute('placeholder', 'abcd')]) ->addItem([new CLabel(_('微信验证码 '), 'wechatcode'), (new CLink('点击获取', 'insertvalidatingcode.php?yzm='))->setTarget('_blank')->setAttribute('id', 'toyanzheng'), (new CTextBox('wechatcode'))->setAttribute('maxlength', '6')]) ->addItem(new CSubmit('enter', _('登 录')))
./include/classes/user/CWebUser.php 检查微信验证码功能是否有效
/** * 验证码功能 */ public static function CheckValidatingCode($postData) { //提取数据库中保存的最后一个验证码 $codesql = DBselect( 'SELECT date,code'. ' FROM validating_code'. ' WHERE status = 0'. ' AND `date` >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)'. ' ORDER BY date DESC'. ' limit 1' ); $sqlcode = DBfetch($codesql); if (!isset($sqlcode['date'])){ echo "验证码有效期5分钟,当前输入验证码已经失效。页面将在2秒后自动返回。"; header('refresh:2;url=index.php'); exit(); } //判断验证码是否正确 if ($postData != $sqlcode['code']){ echo '验证码输入不正确,页面将在2秒后自动返回。'; header('refresh:2;url=index.php'); exit(); } }