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();
        }
    }

 

posted @ 2022-02-24 10:05  一起走过的路  阅读(1963)  评论(0编辑  收藏  举报