谈及到双因子认证或多因子认证时,解决方案有很多,可能会想到短信验证码、RSA动态令牌、Google Authenticator或者Duo,在国内由于某些限制的原因,Google Authenticator和Duo的应用案例较少,而RSA动态令牌主要常见于一些大公司里面,而且使用RSA动态令牌需要独立独立部署RSA 服务器,并且软件需要收费,RSA SecurID密保也是要收费的,如何实现一种经济实惠的双因素认证呢?这里介绍利用python的pam_python模块实现SSH的短信双因素认证。
首先需要简单了解一下Linux中的PAM模块。PAM(PluggableAuthentication Module,可插拔认证模块)机制,采用模块化设计和插件功能,使用户可以轻易地在应用程序中插入新的认证模块或替换原先的组件,同时不必对应用程序做任何修改。详细的PAM介绍可参考: http://www.infoq.com/cn/articles/linux-pam-one 。Pam_python是一款开源的python模块,它将需要使用C语言编写的PAM模块转换成了可以使用python语言编写。
接下来就介绍如何利用pam_python模块实现SSH的短信双因素认证。首先需要编译和安装pam_python模块。依次执行以下命令:
yum install pampam-devel -y #安装编译需要的依赖
wget -Opam-python_1.0.6.tar.gz https://sourceforge.net/projects/pam-python/files/latest/download?source=files --no-check-certificate #下载pam_python模块
tar xvfpam -python_1.0.6.tar.gz
cd pam-python_1.0.6
make lib
cp src/build/lib.linux-x86_64-2.7/pam_python.so/lib64/security/.
接下来就是编写python脚本实现SSH登陆的短信双因素认证。既然是短信双因素认证,就需要一个短信接口用来发送短信。
生成随机验证码并调用短信发送接口发送给用户手机号码。
最后是SSH登陆与用户交互过程并校验验证码的准确性。
完整代码内容如下:
# -*- coding: utf-8 -*- import random, string, hashlib, requests import urllib,urllib2 import pwd, syslog class SMSOperation: """短信发送接口""" def__init__(self, pin, phone_num): self.pin = pin self.phone_num = phone_num self.url = "短信服务地址" self.params = {"account":"短信服务用户名","pswd":"短信服务密码", "msg":"One Time Pin:"+str(pin),"mobile":str(phone_num), "needstatus":"false","extno":""} defparse_number(self): """设置用户手机号参数""" try: self.params['mobile'] =self.phone_num return 1 except: auth_log("Invalidphone number %s. Please check." % (user)) defsend_text(self,pamh): """发送请求""" try: self.parse_number() except: auth_log("Invalidphone number %s. Please check." % (user)) #msg = pamh.Message(pamh.PAM_ERROR_MSG,"The params are : (%s)"% (self.params)) #pamh.conversation(msg) resp = requests.post(self.url, data=self.params) temp =resp.content.split(',')[1] if(temp!=0): auth_log("Message cannot be sentto (%s), please check." %(self.phone_num) ) def auth_log(msg): """保存日志到/var/log/messages""" syslog.openlog(facility=syslog.LOG_AUTH) syslog.syslog("MultiFactors Authentication: " + msg) syslog.closelog() def get_hash(plain_text): """获取短信验证码的sha512字符串,与用户输入的验证码进行校验""" key_hash = hashlib.sha512() key_hash.update(plain_text) return key_hash.digest() def get_user_number(user): """获取用户手机号码""" try: comments = pwd.getpwnam(user).pw_gecos except: auth_log("No local user (%s) found." % user) return -1 try: return comments.split(',')[2] # 返回用户手机号 except: auth_log("Invalid comment block for user %s. Phone number must belisted as Office Phone" % (user)) return -1 def gen_key(pamh, user, user_number, length): """生成短信验证码并发送到用户手机""" pin= ''.join(random.choice(string.digits) for i in range(length)) msg= pamh.Message(pamh.PAM_ERROR_MSG, "The pin is: (%s)" % (pin)) # 登陆界面输出验证码,测试目的,实际使用中注释掉即可 pamh.conversation(msg) sms= SMSOperation(pin, user_number) try: sms.send_text(pamh) except: if not user_number: auth_log("No phonenumber listed for user (%s)." % (user)) else: auth_log("Errorsending PIN to the given SMS number. (%s)" % (user_number)) return -1 return get_hash(pin) def pam_sm_authenticate(pamh, flags, argv): PIN_LENGTH = 6 # 短信验证码长度 try: user = pamh.get_user() user_number = get_user_number(user) except pamh.exception, e: return e.pam_result ifuser is None or user_number == -1: msg = pamh.Message(pamh.PAM_ERROR_MSG,"[1]Unable to get user's phone number./nPlease check.") pamh.conversation(msg) return pamh.PAM_ABORT pin= gen_key(pamh, user, user_number, PIN_LENGTH) ifpin == -1: msg = pamh.Message(pamh.PAM_ERROR_MSG, "[2]One time PIN could notbe generated./nPlease check (%s)" % (user_number)) pamh.conversation(msg) return pamh.PAM_ABORT forattempt in range(0,3): # 仅允许三次错误尝试 msg = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Enter one time PIN:") resp = pamh.conversation(msg) if get_hash(resp.resp) == pin: #用户输入与生成的验证码进行校验 return pamh.PAM_SUCCESS else: continue return pamh.PAM_AUTH_ERR def pam_sm_setcred(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_acct_mgmt(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_open_session(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_close_session(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_chauthtok(pamh, flags, argv): return pamh.PAM_SUCCESS
保存脚本文件到/lib64/security/目录下,这里将脚本命名为:Multiauth.py。
接下来是配置SSHD,启用PAM的短信认证模块。
vi /etc/pam.d/sshd 添加一行内容如下:
auth requisite pam_python.so Multiauth.py
同时需要启用ChallengeResponseAuthentication,修改/etc/ssh/sshd_config,将ChallengeResponseAuthentication设置为yes即可。
在上面的代码里面,获取用户手机号码的时候使用的方法是:pwd.getpwnam(user).pw_gecos,这里我将用户的手机号码信息存放在/etc/passwd文件中,当然可以根据需要存放在其他地方,这里只是用作演示。
一切配置完成之后,需要重启ssh服务:systemctl restart sshd.service
重启完成之后,使用gary账号ssh登陆目标主机时,就启用了短信双因素认证了。
如果在运行过程中发生错误,异常日志信息记录在/var/log/secure文件中,正常的运行日志记录在/var/log/messages中。
这里简单介绍了如何利用pam_python模块实现ssh登陆的短信验证码双因素认证,在实际的安全场景中,实现业务主机的双因素安全认证,可根据实际的安全需求进行调整,例如针对不同用户开启短信验证码,用户手机号码的存储,短信验证码的发送频率限制,短信接口访问的限制。
*本文作者:yuegui_2004,转载请注明FreeBuf.COM