这篇文章目前还没有实现具体的功能,只实现了明文登录,因为我缺少一些数据,比如通过密码生成hash,以及通过challenge生成response,我不知道怎么实现,因此这篇文章也是一个交流的文章,希望大佬看见提个建议,我在功能实现后也会补全该文章。
环境:1.服务器 windows server 2022 中文版 AD (推荐下载windows server 2012 R2)
2.语言:C++
3.实现流程中的API:依赖openldap 2.6库
1.NTLM认证是什么?流程是什么?AD服务器是什么?
在这里我不过多述说,推荐还不懂的浏览以下文章,搞懂NTLM流程
http://t.csdnimg.cn/MF2DT
2.通过LDAP连接到AD服务器
2.1:创建LDAP句柄指针
之后所有的操作都是基于LDAP指针进行操作,例如增删改查AD服务的数据
//ldap是自定义的,uri是string类型的IP地址,端口AD都默认为389 ldap://<IP>:389
LDAP *ldap;
std::string uri = "ldap://192.168.XXX.XXX:389";
int rc = ldap_initialize(&ldap, uri.c_str());
if (rc != LDAP_SUCCESS)
{
std::cerr << "ldap_initialize failed: " << ldap_err2string(rc) << std::endl;
return rc;
}
2.2: 协商ldap的版本
//这些都是固定参数,当然你也可用不协商使用ldap版本,亲测明文登录依旧可以增删改查
int version = LDAP_VERSION3
rc = ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &version);
if (rc != LDAP_SUCCESS)
{
std::cerr << "ldap_set_option failed: " << ldap_err2string(rc) << std::endl;
return rc;
}
2.3协商使用NTLM认证,明文登录不需要做
// 3.设置认证方式为NTLM,我不知道这是否有效,但是服务器却没有拒绝我
int option = LDAP_OPT_X_SASL_MECH;
const char *mech = "NTLM";
rc = ldap_set_option(ldap, option, mech);
if (rc = LDAP_OPT_SUCCESS)
{
std::cerr << "Failed to set NTLM authentication mechanism: " << std::endl;
return 1;
}
2.4进行绑定
2.4.1使用明文进行绑定登录,这非常不安全,通过抓包工具能直接获取到用户的密码
//这是一个同步函数,阻塞等待结果
if ((res = ldap_sasl_bind_s(ldap,
_dn.c_str(),
LDAP_SASL_SIMPLE,
_cred.c_str(),
NULL, NULL, NULL)) != LDAP_SUCCESS)
{
printf("LDAP BIND FAIL %d:%s\n", res, ldap_err2string(res));
}
参数ldap:上面初始化的ldap句柄指针
参数_dn: std::string _dn = "CN=Administrator,CN=Users,DC=test,DC=org"; "CN=<登录的账户名>,CN=<账户所在的组>,DC=<域名前半段>,DC=<域名后半段>"。账户所在
LDAP_SASL_SIMPLE:进行简单绑定认证
_cred: std::string bind_password = "xxxxxxxx"; 密码
后面的3个参数不用管,涉及了更复杂的操作,填写完这些参数,应该就能正常绑定上了
2.4.2 使用NTLM进行认证绑定,抓包工具能抓不到密码,因为全程没有明文密码的交流,这是我问题所在,两天了一点头绪没有,愁死
1.根据NTLM的流程,首先要发送账户名和hash来换取challenge响应。但是我不知道如何通过自己的密码生成hash,ldap库也没有这个功能函数(或许是我自己没查到),从第三个参数能看出来这是一个请求。这里的cred包含的不应是密码,而是hash
struct berval cred;
cred.bv_val = (char *)bind_password.c_str();
cred.bv_len = bind_password.length();
// 第一次的第4个参数应该是一个加密hash,如何得到呢
rc = ldap_ntlm_bind(ldap, bind_dn.c_str(), LDAP_AUTH_NTLM_REQUEST, &cred, nullptr, nullptr, &msgid_int); // 用您的用户名和密码替换NULL参数
if (rc != LDAP_SUCCESS)
{
printf("Error binding to LDAP server with NTLM authentication: %s", ldap_err2string(rc));
exit(1);
}
2.因为ldap_ntlm_bind是一个异步函数,要使用ldap_result来获取响应以及运行的结果,响应存放在res中,两者通过msgid_int这个参数进行联系
rc = ldap_result(ldap, msgid_int, 0, nullptr, &res);
if (rc != LDAP_SUCCESS)
{
printf("ldap_result error\n: %s", ldap_err2string(rc));
exit(1);
}
3.使用ldap_parse_ntlm_bindresult解析出challenge,存放在challenge中
// 通过解析结果获得challenge
rc = ldap_parse_ntlm_bind_result(ldap, res, &challenge);
if (rc != LDAP_SUCCESS)
{
printf("ldap_parse_ntlm_bind_result: %s", ldap_err2string(rc));
exit(1);
}
4.再次调用ldap_ntlm_bin函数向服务器发送通过challenge加密的response,如果与服务器信息对上,那么绑定应该就能成功,与第一个绑定函数的区别在于第三个参数,这里可用看见后缀为RESPONSE,我这里填写challenge是错的,因为我也不知道如通过密码散列将challenge转化为response。
// 这个地方发送的应该是reponse,如何通过密码散列加密challenge,得到reponse呢
rc = ldap_ntlm_bind(ldap, bind_dn.c_str(), LDAP_AUTH_NTLM_RESPONSE, &challenge, nullptr, nullptr, &msgid_int); // 用您的用户名和密码替换NULL参数
if (rc != LDAP_SUCCESS)
{
printf("Error binding to LDAP server with NTLM authentication: %s", ldap_err2string(rc));
exit(1);
}
到这里,NTLM如果绑定成功应该就已经结束了,可惜本人才疏学浅,实在不知道如何将passwd生成AD识别的hash,以及将challenge转化为response。国内资料真的寥寥无几,我现在完全没有了研究的方向。希望有大佬看见,能够指定一二,一个星期没有看见自己的进步压力有点大。
2.5 通过LDAP句柄指针进行搜索,这里我先简单列一下函数,因为篇幅太长,等后面我会补上,主要时间太紧了,周末就加上详细数据。
1.ldap_search_ext_s:能够搜索指定的属性,例如搜索电话等,并返回结果
2.ldap_first_entry,ldap_next_entry; 两个函数共同作用,遍历结果搜索条目,
3.ldap_first_attributem,ldap_next_attribute ;两个函数共同作用,遍历条目搜索属性
最后只需要判断这个属性是不是你所需要的属性即可。
附上搜索mail例子一份,可自行查看
std::string getEmail(ADServer &server)
{
LDAPMessage *result, *entry;
char *attrs[] = {"mail", NULL};
// 1.函数能够所搜索你需要查找的信息。LDAP句柄指针,搜索DN(你开始搜索的地方)
int rc = ldap_search_ext_s(server.getld(), _dn, LDAP_SCOPE_SUBTREE, "(objectClass=*)", attrs, 0, NULL, NULL, NULL, 1000, &result);
if (rc != LDAP_SUCCESS)
{
printf("LDAP SEARCH FAIL %d:%s\n", rc, ldap_err2string(rc));
return "";
}
std::string email;
for (entry = ldap_first_entry(server.getld(), result); entry != NULL; entry = ldap_next_entry(server.getld(), entry))
{
BerElement *ber = NULL;
char *attr;
for (attr = ldap_first_attribute(server.getld(), entry, &ber); attr != NULL; attr = ldap_next_attribute(server.getld(), entry, ber))
{
if (std::string(attr) == "mail")
{
struct berval **vals = ldap_get_values_len(server.getld(), entry, attr);
if (vals != NULL)
{
email = std::string(vals[0]->bv_val, vals[0]->bv_len); // Assuming there's only one value for "mail"
ldap_value_free_len(vals);
}
}
ldap_memfree(attr);
}
if (ber != NULL)
{
ber_free(ber, 0);
}
}
ldap_msgfree(result);
return email;
}
3.参考资料推荐
https://www.openldap.org/software/man.cgi 这是openldap官网,在搜索栏里man一下ldap,就能看到很多的ldap API,同时注意很多API在v3版本已经被禁用。
4.结言
希望有这方面经验的大佬指定一下,因为代码资料几乎没有,以上全是我摸爬滚打出来的,明文实测可以运行。真期待自己NTLM认证成功的那一天。我也不知道为什么老板要用C++写,听说C#全是封装好的接口,传递一个账户,密码就能通过kerberos,NTLM认证,哎。
如果需要环境搭建的,可以说一下,我周末也写一写。
最后本人应届毕业生菜鸡一个,文章错误很多,仅记录,或许还能逗你一乐