使用perl生成合规密码
2022-05-13 15:56:46 阿炯

本文主要是用于生成合规密码,解决工作中的弱口令问题,同时增加了生成安全加密密码的方式盘点;并提供了一个生成页面,先来看看CPAN上相关的模块介绍。本文可以认为是前文《用Perl生成随机密码》的延伸版本。

Crypt::PasswdMD5 - Provide interoperable MD5-based crypt() functions
该模块提供了基于MD5

apache_md5_crypt()提供了与apache(httpd)兼容的.htpasswd文件;

unix_md5_crypt()提供了与操作系统crypt()兼容的基于MD5函数;两都均为系默认导出的函数。

如果salt值没有指定给出,一个随机的盐值将会被random_md5_salt()函数来生成,但该函数没有默认导入,需要在使用时声明导入使用。

random_md5_salt([$length])
生成指定长度并返回随机的盐值,最大(默认)长度为8。


Crypt::Password::Util - Crypt password utilities
该模块提供了几种密码加密的功能函数:生成不同算法的加密密码,对字符串进行是否为加密密码的判定及是何种类型的判定。

use v5.20;
use Data::Dumper;
use Crypt::Password::Util qw(crypt looks_like_crypt crypt_type);

say crypt('pass');#自动选择使用的类型及盐值
my $res = crypt_type('$1$mSwdJnqF$0BQNU5L7qLcFcG23DPr8i/',1);
say Dumper($res);

crypt($str) => str
生成在本系统上所能支持的最合理安全的加密密码

优先使用基于成本的加密方式,加入轮回值(rounds value),服务器可能在校验时大概每秒处理100个密码的能力,这足以应用多数场景;在OpenBSD上使用7成本( cost=7)的BCRYPT,其它系统为15000(rounds=15000)的SHA512;当本地主机上不具备这些环境时,转而使用MD5算法或直接报错。

BCRYPT
基于Blowfish
Recognized by: $2$ or $2a$ header followed by cost, followed by 22 base64-digits salt and 31 digits hash.

CRYPT
Traditional DES crypt.
Recognized by: 11 digit base64 characters.

EXT-DES
Extended DES crypt.
Recognized by: underscore followed by 19 digit base64 characters.

MD5-CRYPT
A baroque passphrase scheme based on MD5, designed by Poul-Henning Kamp and originally implemented in FreeBSD.
Recognized by: $1$ or $apr1$ header.

PLAIN-MD5
Unsalted MD5 hash.
Recognized by: 32 digits of hex characters.

SSHA256
Salted SHA256, supported by glibc 2.7+.
Recognized by: $5$ header.

SSHA512
Salted SHA512, supported by glibc 2.7+.
Recognized by: $6$ header.

looks_like_crypt($str) => bool
判定字符串$str是否为加密过的字串。

crypt_type($str[, $detail]) => str|hash
分析加密字符串$str的相关信息类型。


Crypt::Passwd::XS - Full XS implementation of common crypt() algorithms
该模块提供了几种通用的crypt()实现,当然为了效率使用了XS实现方式与验证。

函数
crypt()函数处理其支持的所有标准加盐加密方式

The unix_md5_crypt() function performs a MD5 crypt regardless of the salt prefix.

The apache_md5_crypt() function performs a APR1 crypt regardless of the salt prefix.

The unix_des_crypt() funtion performs a traditional DES crypt regardless of the salt prefix.

The unix_sha256_crypt() function performs a SHA256 crypt regardless of the salt prefix.

The unix_sha512_crypt() function performs a SHA512 crypt regardless of the salt prefix.


App-bmkpasswd/bmkpasswd
Simple bcrypt-enabled mkpasswd.
bmkpasswd [OPTIONS]... [PASSWD]

相关选项
-m, --method=TYPE  [default: bcrypt],Types:  
    bcrypt  (recommended; guaranteed available)
    sha512  (requires recent libc or Crypt::Passwd::XS)
    sha256  (requires recent libc or Crypt::Passwd::XS)
-w, --workcost=NUM    Bcrypt work-cost factor; default 08. Higher is slower. Should be a two-digit power of 2.
-c, --check=HASH    Compare password against given HASH
-s, --strong    Use strongly-random salt generation
-b, --benchmark    Show timers; useful for comparing hash generation
--available    List available methods (one per line)
--version    Display version information and available methods

If PASSWD is missing, it is prompted for interactively.


MD5是一种被广泛使用的密码散列函数,可以产生出一个128位的散列值(hash value),用于确保信息传输完整一致。MD5算法的原理可简要的叙述为:MD5码以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

MD5全称MD5信息摘要算法(MD5 Message-Digest Algorithm),使用密码散列函数产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由美国密码学家罗纳德-李维斯特设计:
1992年公开,用来取代MD4算法,这套算法程序在RFC 1321标准中被加以规范。
1996年后被证实存在弱点可被加以破解,对于高度安全性的数据建议改用SHA-2。
2004年MD5算法无法防止碰撞,因此不适用安全性认证,如SSL公开密钥认证或数字签名等用途。

Linux下可使用grub-md5-crypt命令可以以MD5格式对GRUB的密码进行添加加密口令。

2004年,王小云团队证实了MD5不再安全;2017年,CWI与Google的研究人员证实了SHA-1不再安全。有鉴于此,Hash算法有如下的新的选择:
1)、SHA-2家族,2002年发布的二代算法,推荐使用SHA256或SHA512
2)、SHA-3,2012年发布的第三代安全散列算法,因SHA-2与SHA-1采用了相同的处理引擎,NIST担心SHA-2家族被破只是时间问题,所以需要不同和备用算法;不过目前还没有且支持SHA-3的库甚少
3)、慢速加盐散列:包括bcrypt、scrypt、PBKDF2

在extmail的邮件系统中的密码默认设计就是md5加盐的方式(webman.cf中的SYS_CRYPT_TYPE项)
md5crypt加密方式,密码格式如:$1$xyz$XXX

以'$'分开的三部分分别为:magic、salt、password-hash

无论是何种哈希都是摘要算法,是将任意长度的任意字节对应成固定长度的一段字节。这段摘要字节因为包含很多不易显示的字符,所以通常使用hex或者base64等类似方法将它转换成可见字符显示出来。因此用$将hash切割成三部分:"1"、"xyz"、"XXX",即:magic、salt、password-hash。其中password-hash实际上就是哈希完成后的字符串,再通过类似base64的算法转换成了可见字符串。

这个salt(盐)值的特点是加密后的密文中记录了这段salt,即$符号及其包含的字符,比如加密结果中的magic与salt是$1$k0Q4EA49$,所以对原始密码为123456, salt为k0Q4EA49加密算法的Perl实现如下:say crypt('123456','$1$k0Q4EA49$');
$1$k0Q4EA49$v7OvFiQ95NVV8bMFnDnqM.

知道这个原理之后可将extmail的mailbox账号信息验证集成到其它系统之中。另外Linux下的/etc/shadow,grub的md5-crypt的加密方式与上述原理完全一样,如果要将某账号的密码强行修改为123456而不借助其它工具,可使用文档编辑工具将/etc/shadow文件中对应账号第二个字段内容更换为$1$k0Q4EA49$v7OvFiQ95NVV8bMFnDnqM.

magic

magic是表明这一段哈希是通过什么算法得到的,对应关系如下:
$0 = DES
$1 = MD5
$2a(2y) = Blowfish
$5 = SHA-256
$6 = SHA-512

目标hash的magic为1,说明是md5加密。当然内部实现不会是单纯单次md5,但总体来说是以MD5为hash函数,通过多次计算得到的最终值。

sha256的哈希
$5$OyrHJnq569$09t208oDT3jJWLX1A1pG3tlv6bxzpFBKw94eFGP2z23

sha512的哈希
$6$GhQ8DKP/wh$zeiqoy8b5nNaze4uf.z.aShKFSXamCDRW10KQyYisu68aoJG/wwle6zTX/DK3NnKH/U00P7KHgcNqQEFTUYg2.


除了通过magic来判断密文的加密方式以外,通过哈希的长度也可以判断。
perl -MMIME::Base64 -E 'say length(decode_base64("v7OvFiQ95NVV8bMFnDnqM."))'
15

有时视情况为16,base64编码的长度视情况是有变化的。这是md5的摘要的长度(hex后长度为32),反过来也能佐证这个哈希的加密方式为md5。

salt

salt是此次哈希的盐值,长度是8位,超过8的后面的位数将不影响哈希的结果。在正常情况下,进行加密的时候,这个盐值是随机字符串。

合规盐值的范围:['.', '/', 0..9, 'A'..'Z', 'a'..'z']

password-hash

就是password加密完成后得到的hash,这依赖于其所使用的算法。

在密码学中,对于防范哈希暴力破解的一种方式就是“密钥延伸”,简单来说就是利用多次hash计算,来延长暴力破解hash的时间。现代linux系统使用的hash方法为SHA-512(Unix),算法核心为sha512,可以通过查看/etc/shadow中对应的行(可登录用户)来查看。


目前较佳的组合是使用的加密方法是Bcrypt+Blowfish,可有效地缓解破解的速度,另外还可以指定其cost值来调整。

cost在Blowfish算法中就是延缓其速度,增加破解难度的选项,如果将cost设置为12,生成的hash,破解起来速度可以降到10 words/s,相当于一万个密码的字典需要用16分钟,极大地增加了密码碰撞的难度。


crypt(Unix)及其派生版本https://en.wikipedia.org/wiki/Crypt_(Unix)

crypt is a POSIX C library function. It is typically used to compute the hash of user account passwords. The function outputs a text string which also encodes the salt (usually the first two characters are the salt itself and the rest is the hashed result), and identifies the hash algorithm used (defaulting to the "traditional" one explained below). This output string forms a password record, which is usually stored in a text file.

The GNU C Library used by almost all Linux distributions provides an implementation of the crypt function which supports the DES, MD5, and (since version 2.7) SHA-2 based hashing algorithms mentioned above. Ulrich Drepper, the glibc maintainer, rejected bcrypt (scheme 2) support since it isn't approved by NIST. A public domain crypt_blowfish library is available for systems without bcrypt. It has been integrated into glibc in SUSE Linux. In addition, the aforementioned libxcrypt is used to replace the glibc crypt() on yescrypt-enabled systems.

crypt变种参考

Bcrypt
Ccrypt
Scrypt
Mcrypt
Blowfish


如何验证用crypt生成的密码

通过上面的密码处理过程,在此逆向验证用户输入的密码的正确性。

使用了MD5算法生成了如下的密码对
原生密码:ecNfvEgjr6Oaq
加密密码:$1$eGTdMpcj$fmHKS6afuocL7f.e49hG41

use v5.20;
use MIME::Base64;

#Orig PassWord(原生密码)
my $opwd='ecNfvEgjr6Oaq';

#Encryped PassWord with MD5(已生成的加盐密码)
my $enpwd='$1$eGTdMpcj$fmHKS6afuocL7f.e49hG41';

#取出Hash类型,盐值,加密密码
#ht:hash type,hs:hash salt,epd:encryped password
my ($ht,$hs,$epd)=(split(/\$/,$enpwd))[1..3];
say "HashType:$ht,HashSalt:$hs,EnPwd:$epd";

#Verify(校验一次)
my $enpwd2=CORE::crypt($opwd,sprintf('$1$%s',$hs));
say "Enp:$enpwd2";

输出结果:
HashType:1,HashSalt:eGTdMpcj,EnPwd:fmHKS6afuocL7f.e49hG41
Enp:$1$eGTdMpcj$fmHKS6afuocL7f.e49hG41

反向验证通过,取得盐值是关键。


下面提供一个在网页中生成原生与加密密码的实现,基于nginx+mojo+jquery+zui的技术实现,类似于相当多的网站上提供的密码生成器,这里不同的是:生成密码的同时还生成了其对应的加密密码串。

###############################主脚本
#!/opt/perl/bin/perl
use v5.20;
use utf8;
use Encode;
use JSON::XS;
use Time::Piece;
use Data::Dumper;
use FindBin qw($Bin);
use IO::Socket::INET;
use Mojolicious::Lite;
use Encode qw(decode encode);
#运维工具箱
#2022-03-29T13,生成密码及其Hash码

binmode(STDIN,":encoding(utf8)");
binmode(STDOUT,":encoding(utf8)");
binmode(STDERR,":encoding(utf8)");

#载入配置文件
#require "/etc/freeoa/main.conf";

#载入通用函数
#require "$Bin/common.pl";

app->log->level('debug');
app->secrets(['mojoliciou','rock3me']);
app->defaults('max_clients'=>100,'max_requests'=>100,'workers'=>9);

##############################
any '/'=>sub{
    my $self = shift;
    $self->stash(title=>'FreeOA工具箱');
    $self->render(text=>'Ban for access here!',status=>419);
};

any '/mypwd'=>sub{
    my $self=shift;
    $self->render('ywgenpwd',title=>'将军令');
};

post '/genpwd'=>sub{
    my $self=shift;
    my ($hcat)=($self->param('hcat')//1);
    #最小长度,最大长度,密码,用户传入的长度
    my ($minlen,$maxlen,$pwd,$uinlen)=(11,19,'',0);

    #对用户传入的参数进行预处理:是否传参,参数长度等
    $uinlen=genrandintv($minlen,$maxlen) unless(defined($uinlen));
    if(($uinlen<$minlen) || ($uinlen>$maxlen)){
        $uinlen=genrandintv($minlen,$maxlen);
    }

    #开始循环处理密码
    while($pwd=genpwd($uinlen)){
        last if(($pwd=~/^\D/) && ($pwd=~/\d/) && ($pwd=~/[a-z]/) && ($pwd=~/[A-Z]/));
    }

    if($hcat eq '1'){
        $self->render(json=>{"ppwd"=>"$pwd","cpwd"=>CORE::crypt($pwd,sprintf('$1$%s',rand_b64chars(8)))});
    }elsif($hcat eq '5'){
        $self->render(json=>{"ppwd"=>"$pwd","cpwd"=>CORE::crypt($pwd,sprintf('$5$%s',rand_b64chars(8)))});
    }elsif($hcat eq '6'){
        $self->render(json=>{"ppwd"=>"$pwd","cpwd"=>CORE::crypt($pwd,sprintf('$6$%s',rand_b64chars(16)))});
    }else{
        $self->render(json=>{"error"=>"输入参数有误."});
    }

};

##############################
#密码生成函数
sub genpwd{
    my ($tlen)=@_;
    return join '',map { ('a'..'z', 'A'..'Z', '0'..'9')[rand 62] } 1..$tlen;
}

#生成区间随机数
sub genrandintv{
    my ($minlen,$maxlen)=@_;
    my ($radlen);
    #say "Got min:$minlen,max:$maxlen.";
    while($radlen=int(rand(20))){
        #say "in while get:$radlen";
        last if(($radlen>=$minlen) && ($radlen<=$maxlen));
    }
    return $radlen?$radlen:'13';#处理while(0)的情况
}

#生成指定长度的盐值,sha256为8,sha512为16
sub rand_b64chars {
    state $dummy=do{ require Bytes::Random::Secure };
    my $num_chars=shift;
    my $num_bytes=int($num_chars * 3/4) + 1;
    my $res=substr(Bytes::Random::Secure::random_bytes_base64($num_bytes),0,$num_chars);
    $res=~s/\+/./g;
    return $res;
}

app->start('daemon','--listen','http://127.0.0.1:9993');


###############################页面模板
templates/ywgenpwd.html.ep
% content_for header => begin
 <meta charset="utf-8" content="text/html" http-equiv="Content-Type" />
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <link rel="stylesheet" type="text/css" href="zui/css/zui.min.css" />
 <link rel="stylesheet" type="text/css" href="zui/css/zui-theme.min.css" />
 <script type="text/javascript" src="jquery.min.1.12.4.js"></script>
 <script type="text/javascript" src="zui/js/zui.min.js"></script>
<style>
.center{margin:auto;width:70%;border:4px solid #c26bdd;padding:10px;display:flex}
</style>
% end

<!DOCTYPE html>
<html>
 <head><title><%= title %></title>
 %= content_for 'header'
 </head>
<body>

<hr />

<div id="tabs" style="width:900px;">

<form action="/" method="post" id="form_hd">
<label for="dt">Hash类型</label>
    <select size="1" name="hcat" id="hcat">
        <option value="1">MD5</option>
        <option value="5">SHA256</option>
        <option value="6">SHA512</option>
  </select>
    <button class="btn btn-info" type="button" id="btn_get_now" onclick="getpwd()">点立得</button>
</form>

<div class="alert alert-primary">原生密码:<div id="ppwd"></div></div>
<div class="alert alert-primary-inverse">加密密码:<div id="cpwd"></div></div>
<div class="center"><div id="timer"></div>秒后刷新,请及时复制.原生密码只能自己持有,切勿告诉他人!</div>

</div>

<script type="text/javascript">
var timend=59;
var refmsg=new $.zui.Messager({type:'success'});
refmsg.show('请及时复制保存,每分钟将会自动刷新.');

$(document).ready(function(){
        getpwd();
    var time=timend;
    setInterval(function(){
        time--;
        $('#timer').text(time);
        if(time === 0){
                    getpwd();
                    time=timend;
                }
    },1000);
});

function getpwd(){
    $.ajax({
      async: true,type: "POST",url: '/ywkits/genpwd',
        data: $('form#form_hd').serialize(),
    success: function(data){
        var json=eval(data);
        $('#ppwd').html(json.ppwd);
        $('#cpwd').html(json.cpwd);
    }
  });
};

$('#hcat').change(function(){
    $('#btn_get_now').trigger('click');
});

</script>

<hr />

</body></html>



另外启行将相关的依赖:jquery,ZUI的前端框架文件在同级public目录放置完成。nginx中的配置如下:
#运维工具
location /ywkits/ {
    limit_conn one 10;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_pass http://127.0.0.1:9993/;
}

配置完成后,访问如下地址:
http://ip:port/ywkits/mypwd

这里提供一个运行时截图,用户如果不喜欢当前生成的密码,可以点击“点立得”按钮,立即生成新的密码,不必等到页面中的到时重新生成或刷新页面。



备注:
1、生成了11至19位的随机密码,这里没有引入其它的特殊字符,只以字符开头,1分钟自动更新,仿将军令
2、按用户指定的Hash类型生成对应的Hash加密串,用户可以将加密串发送给他人配置,而无需将原生密码传递,提高密码的安全性
3、上述脚本的生成加密后的结果在Debian Linux 11版本系统/etc/shadow中的第二列直接为可登录的用户替换后测试成功登录


另附上对“弱密码”、“弱口令”的相关定义:
对于采用静态口令认证技术的系统或设备,帐户口令应满足以下要求:
1.口令长度应至少8位;
2.口令应包括数字、小写字母、大写字母、特殊符号4类中至少3类
3.口令应与用户名无相关性,口令中不得包含用户名的完整字符串、大小写变位或形似变换的字符串;
4.应更换系统或设备的出厂默认口令;
5.口令设置应避免键盘排序密码。

不满足以上要求任意一项的口令相关问题,可定义为“弱口令”漏洞。对于通过其他技术手段或社会工程等方式(如社工库碰撞、密码表爆破、配置文件解密、社会工程学、监听探测或利用其他安全漏洞等),获取到帐户口令的相关问题,不定义为“弱口令”。

这里参考了《理解等保测评中二、三级》中的相关要求。



该文章最后由 阿炯 于 2022-06-25 22:58:26 更新,目前是第 2 版。