freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

挂羊头卖狗肉(ldap背后的其他认证)
2021-01-08 18:36:55

先讲一个故事:

故事背景 :

XX 互联网公司,内网大多数的系统(其中包含wiki)采用LDAP 提供用户中心授权,LDAP 服务通过 windows 下的AD 默认实现。办公网接入通过LDAP 完成上网的二次认证。 LDAP 密码要求每三个月更新一次密码。 看似很安全的一个一个上网环境。

某天,该公司的一个高级别的员工的密码出现了泄露。 因为泄露密码触发了一系列的安全事件。

  1. XX 通过该密码,进入了该公司的办公网
  2. 登录到了wiki系统,wiki 大家都知道,相当于一个公司内部的说明书了。其中有一个列表页面,里边列出了内部所有的系统,其中包含 xxx 管理系统。
  3. 使用 泄露的密码登录到了xxx管理系统,发送了一个非法的文章。 (入侵完成,至此该公司才感知到入侵)
  4. 该文章正式发布,内部意识到,系统入侵。封掉了,所有的高级别账号。
  5. 高层为了解决这个问题,一次命令要求公司内部所有的系统强制替换动态密码登录。
  6. 各种代码可控的系统,更新完全没有问题,直接对接到公司的统一登录系统,然后限制统一登录系统使用动态登录就OK了。 但是,对于一些闭源的商用产品 比如 wiki , jira 等,就没有办法了。
  7. .............

解决故事最后的问题:

如何修改一个商用闭源的系统的登录认证,有如下的两个思路,各有不同,各有优缺点。

方案1  做一个认证的壳,包裹后端的真实服务

  1. 外层使用 nginx ,openresty 这些开源代理软件,做一个认证代理。真实后端作为隐藏资源,限制本地的ACL 只允许认证代理的访问。
  2. 当用户通过认证代理访问后端资源的时候,先判断当前用户的会话信息。如果其中包含了认证的信息,那么直接透明代理到后端的真实资源。 如果其中未包含认证的信息,那么跳转到统一认证,进行身份认证,认证完成,回跳到认证代理,写入认证信息,然后重新刷新当前的请求。 
  3. 这种解决办法存在一个问题,对于用户来讲,需要认证两次: 一次是认证的壳 (认证代理),另一个是认证的后端真实服务。
  4. 当然这种解决办法也有一定的优点,由于使用一个认证代理报过了后端真实服务的所有请求,同时这些请求都是包含了实名认证信息的。所以,这些资源对于内部系统使用审计是不可多得的资源,同时对这些审计日志按照等保的要求去做处理,也就直接帮后端的系统做了合规了。
  5. api 用户授权相对困难,因为,封装了一层壳,所以,原有的通过api 调用系统的代码,不得不再封装一层壳的认证信息。这些对于一个api 调用为主的系统(比如harbor)是极不友好的,他们不得不修改原有的api调用方法。
  6. 扩展性很棒,只需要提供一次封装就可以灵活的往这种结构里接入任何系统。后端真实服务无需做任何修改。只需要做好ACL限制即可。
  7. 最后:如果能协调好用户的使用情绪,也不失为一个很棒的解决方案。   

方案2  重新定义一个ldap 认证模块的壳,重新定义密码验证规则 

  1. 大多数系统,开源也好,闭源也好。八九成的系统都会支持ldap协议配置用户认证。所以,我们做一个ldap 认证的壳就好了。该服务通过ldap 协议解析用户名,密码重新定义用户认证规则即可。
  2. 当用户访问系统输入用户密码以后,通过后端配置好的 ldap 服务端口发送到我们自定义的认证服务,解析出用户名,密码进而自定义完成认证即可。 
  3. 这种思路相对灵活,只需要修改目标软件的认证服务配置即可。 同时,后端认证服务为自助开发,可以封装更丰富的认证逻辑。同时这种方案,配置比较简单,在应用程序的配置范围内就可以解决问题,无需做其他修改。
  4. api用户相对友好,因为,认证逻辑完全在后端拦截的服务中,调用方无感知的认证。所以,可以在接入动态认证的基础上,提供一定许可范围并且安全的静态密码(一定长度,一定规则,一定有效期),供api用户使用。
  5. 但是这种方案也有缺点。对于少数不支持ldap认证配置的系统,这种方案就无能为力了。

具体实现:

方案1 实现:

通过 openresty , nginx 的模块扩展来实现。 比如 : https://github.com/Siecje/nginx-auth-proxy

或者 也可以通过lua 控制 各个hook 来完成这个需求。

具体实现本文不讲述。

方案2 实现:

通过一个提供ldap 服务的类库来实现请求拦截。作者通过

https://github.com/vjeantet/ldapserver的类库来完成封装一个伪ldap 服务,本文的主要目的是完成认证,所以,直接忽略了bind以外的其他的请求。如果读者感兴趣也可以,实现 Add , Modify , Delete 的请求,就可以完全替代 ldap 服务了。 (暗笑)不过你把ldap 服务都实现了 , 那公司的AD 也就没啥用了,所以

适可而止吧,少年。

参考: https://github.com/vjeantet/ldapserver/blob/master/examples/simple/main.go来实现bind 请求 .

// Listen to 10389 port for LDAP Request
// and route bind request to the handleBind func
package main

import (
	"log"
	"os"
	"os/signal"
	"syscall"

	ldap "github.com/vjeantet/ldapserver"
)

func main() {

	//ldap logger
	ldap.Logger = log.New(os.Stdout, "[server] ", log.LstdFlags)

	//Create a new LDAP Server
	server := ldap.NewServer()

	routes := ldap.NewRouteMux()
	routes.Bind(handleBind)
	server.Handle(routes)

	// listen on 10389
	go server.ListenAndServe(":10389")

	// When CTRL+C, SIGINT and SIGTERM signal occurs
	// Then stop server gracefully
	ch := make(chan os.Signal)
	signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
	<-ch
	close(ch)

	server.Stop()
}

// 封装具体的自定义的用户认证逻辑
func authUser(bindName, bindPass string) bool {
	return true
}

// 返回匹配用户的DN
func dnMaker(bindName string) string {
	return bindName
}

func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
	r := m.GetBindRequest()
	res := ldap.NewBindResponse(ldap.LDAPResultSuccess)

	bindName := string(r.Name())
	bindPass := string(r.AuthenticationSimple())

	log.Printf("Bind failed User=%s, Pass=%s", bindName, bindPass)

	if authUser(bindName, bindPass) {
		res.SeMatchedDN(dnMaker(bindName))
		w.Write(res)
		return
	}

	res.SetResultCode(ldap.LDAPResultInvalidCredentials)
	res.SetDiagnosticMessage("invalid credentials")
	w.Write(res)
}


注:这个实例代码只是实现了简单的bind 请求的判断。

多数系统会让用户配置一个 base dn , filter 这类请求则需要用户实现 handleSearch 请求。

这类认证的具体流程如下:

  1. 使用admin dn ,pass 完成管理员认证
  2. 通过 filter 构建搜索条件,发往认证服务器,完成搜索 ,获取用户的信息
  3. 根据用户的返回信息再次触发bind 请求。 

一个简单的handleSearch 实现如下:

func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
	r := m.GetSearchRequest()
	log.Printf("Request BaseDn=%s", r.BaseObject())
	log.Printf("Request Filter=%s", r.FilterString())
	log.Printf("Request Attributes=%s", r.Attributes())

	select {
	case <-m.Done:
		log.Printf("Leaving handleSearch... for msgid=%d", m.MessageID)
		return
	default:
	}

	e := ldap.NewSearchResultEntry("cn=Valere JEANTET, " + string(r.BaseObject()))
	// 配置邮件属性
	e.AddAttribute("mail", "valere.jeantet@gmail.com")
	// 通过 AddAttribute 可以添加其他复杂的属性
	w.Write(e)

	res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess)
	w.Write(res)

}

最后 , 对于动态认证的需求,需要在一个库中存储用户和种子的对应关系。当请求过来的时候,通过用户名获取匹配到的种子,通过OTP 算法计算对应的密码 进行匹配即可。

注: 种子数据注意加密,并妥善保管相应的秘钥。

# 系统安全 # 网络安全技术 # LDAP
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者