NTLM认证流程详解

什么是NTLM

NTLM是NT LAN Manager的缩写,即问询/应答(Challenge/Response)身份验证协议,是 Windows NT早期版本的标准安全协议。

认证流程

  • 客户端向服务器发送HTTP请求,请求获得服务器资源,如访问邮件服务器资源
  • 服务器由于开启了NTLM认证,所以返回401的错误码(未授权),提示需要NTLM认证
  • 客户端发起NTLM认证,向服务器发送协商消息
  • 服务器收到消息后,生成一个随机数Challenge,明文发送回客户端
  • 客户端接收到Challenge后,使用密码hash对Challenge加密,生成Response并发送给服务器
  • 服务器接收到Response后,会向DC(Domain Controller)发送针对客户端的验证请求,该请求主要包含以下三方面的内容:客户端用户名,客户端密码哈希值加密的Challenge和原始的Challenge
  • DC根据用户名获取该帐号的密码哈希值,对原始的Challenge进行加密。如果加密后的Challenge和服务器发送的一致,则意味着用户拥有正确的密码,验证通过,否则验证失败。DC将验证结果发给服务器,并最终反馈给客户端
image.png

下面以调用exchange邮件服务https://xxx.com/EWS/Exchange.asmx
为例进行分析,使用的网络库为httpclient 4.5.1

implementation 'org.apache.httpcomponents:httpmime:4.5.1'
implementation 'org.apache.httpcomponents:httpclient:4.5.1'
public static void main(String[] args) throws IOException {
    System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog");
    System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "all");
    
    HttpPost httpPost = new HttpPost("https://xxx.com/EWS/Exchange.asmx");
    httpPost.addHeader("Content-type", "text/xml; charset=utf-8");
    httpPost.addHeader("User-Agent", "EWS");
    httpPost.addHeader("Accept", "text/xml");
    httpPost.addHeader("Keep-Alive", "300");
    httpPost.addHeader("Connection", "Keep-Alive");
    httpPost.addHeader("Accept-Encoding", "gzip,deflate");

    RequestConfig.Builder requestConfigBuilder = RequestConfig.custom()
            .setAuthenticationEnabled(true)
            .setRedirectsEnabled(true)
            .setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.NTLM));
    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    NTCredentials webServiceCredentials = new NTCredentials("username", "password", "", "domain");
    credentialsProvider.setCredentials(new AuthScope(AuthScope.ANY), webServiceCredentials);
    HttpClientContext httpContext = HttpClientContext.create();
    httpContext.setCredentialsProvider(credentialsProvider);
    httpPost.setConfig(requestConfigBuilder.build());

    StringEntity entity = new StringEntity("<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"><soap:Header><t:RequestServerVersion Version="Exchange2010_SP2"></t:RequestServerVersion></soap:Header><soap:Body><m:GetFolder><m:FolderShape><t:BaseShape>IdOnly</t:BaseShape></m:FolderShape><m:FolderIds><t:DistinguishedFolderId Id="inbox"></t:DistinguishedFolderId></m:FolderIds></m:GetFolder></soap:Body></soap:Envelope>\n");
    httpPost.setEntity(entity);
    CloseableHttpClient httpClient = HttpClients.custom().build();
    HttpResponse httpResponse = httpClient.execute(httpPost, httpContext);
    System.out.println(httpResponse.getStatusLine().getProtocolVersion() + " " + httpResponse.getStatusLine().getStatusCode());
    System.out.println(EntityUtils.toString(httpResponse.getEntity()));
}

请求资源(客户端<=>服务器)

  • 请求报文
POST /EWS/Exchange.asmx HTTP/1.1
Content-type: text/xml; charset=utf-8
User-Agent: ExchangeServicesClient/0.0.0.0
Accept: text/xml
Keep-Alive: 300
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
Content-Length: 630
Host: xxx.com
  • 响应报文
HTTP/1.1 401 Unauthorized
Server: Microsoft-IIS/8.5
request-id: 10ca999c-84bf-4860-9248-283176dc502a
X-OWA-Version: 15.1.2375.17
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="xxx.com"
X-Powered-By: ASP.NET
X-FEServer: EX-1
Date: Sun, 06 Feb 2022 10:14:41 GMT
Content-Length: 0

Type 1(客户端->服务器)

POST /EWS/Exchange.asmx HTTP/1.1
Content-type: text/xml; charset=utf-8
User-Agent: ExchangeServicesClient/0.0.0.0
Accept: text/xml
Keep-Alive: 300
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
Content-Length: 630
Host: xxx.com
Authorization: NTLM TlRMTVNTUAABAAAAAYIIogAAAAAoAAAAAAAAACgAAAAFASgKAAAADw==

Authorization中的认证信息TlRMTVNTUAABAAAAAYIIogAAAAAoAAAAAAAAACgAAAAFASgKAAAADw==为固定值,生成规则为对固定字节数组进行base64编码,然后转换为String

byte[]bytes=new byte[]{78, 84, 76, 77, 83, 83, 80, 0, 1, 0, 0, 0, 1, -126, 8, -94, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 5, 1, 40, 10, 0, 0, 0, 15};
byte[]base64=Base64.encodeBase64(bytes);
System.out.println(new String(base64,0,base64.length, StandardCharsets.US_ASCII));
//output: TlRMTVNTUAABAAAAAYIIogAAAAAoAAAAAAAAACgAAAAFASgKAAAADw==

字节数组bytes的组成如下:

SIGNATURE messageType Flags Domain Domain
[78, 84, 76, 77, 83, 83, 80, 0] [1, 0, 0, 0] [1, -126, 8, -94] [0, 0] [0, 0]
Domain Offset Host Host Host Offset Version Build NTLM revision
[40, 0, 0, 0] [0, 0] [0, 0] [40, 0, 0, 0] [5, 1] [40, 10, 0, 0] [0, 15]

Type 2(服务器->客户端)

HTTP/1.1 401 Unauthorized
Server: Microsoft-IIS/8.5
request-id: 7ec66b3a-b31e-4569-aea5-76edca6dd97b
WWW-Authenticate: NTLM TlRMTVNTUAACAAAADAAMADgxxxxxxxxxxxxxxxEAAAABgOAJQAAAA9aAEUATgBNAEUATgACAAwAWgBFAE4ATQBFAE4AAQAIAEUAWAAtADEABAAWAHoAZQBuAG0AZQBuAC4AYwBvAHIAcAADACAARQBYAC0AMQAuAHoAZQBuAG0AZQBuAC4AYwBvAHIAcAAFABYAegBlAG4AbQBlAG4ALgBjAG8AcgBwAAcACAD8JnFbQhvYAQAAAAA=
X-OWA-Version: 15.1.2375.17
WWW-Authenticate: Negotiate
WWW-Authenticate: Basic realm="xxx.com"
X-Powered-By: ASP.NET
X-FEServer: EX-1
Date: Sun, 06 Feb 2022 10:14:41 GMT
Content-Length: 0

Type 3(客户端->服务器)

POST /EWS/Exchange.asmx HTTP/1.1
Content-type: text/xml; charset=utf-8
User-Agent: ExchangeServicesClient/0.0.0.0
Accept: text/xml
Keep-Alive: 300
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
Content-Length: 630
Host: xxx.com
Authorization: NTLM TlRMTVNTUAADxxxxxxxxxxxxxxxxxLQAYAAAAAwADAAUAQAADAAMACABAAAAAAAALAEAAAAAAAAsAQAABYKIogUBKAoAAAAPNXkU/KGJ902Hu30Coa3vMvKpcRDQ7F3Ax4fiNLAVa9htbnnjMBNx5wEBAAAAAAAAMEf6W0Ib2AFK8uwnZtst1wAAAAACAAwAWgBFAE4ATQBFAE4AAQAIAEUAWAAtADEABAAWAHoAZQBuAG0AZQBuAC4AYwBvAHIAcAADACAARQBYAC0AMQAuAHoAZQBuAG0AZQBuAC4AYwBvAHIAcAAFABYAegBlAG4AbQBlAG4ALgBjAG8AcgBwAAcACAD8JnFbQhvYAQAAAAAAAAAAWgBFAE4ATQBFAE4AawBvAG4AZwBwAGYA

最终响应报文(服务器->客户端)

HTTP/1.1 200 OK
Cache-Control: private
Transfer-Encoding: chunked
Content-Type: text/xml; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
Server: Microsoft-IIS/8.5
request-id: bfe3362c-bacb-4c4a-8a40-dbf5cc5c2e2b
X-DiagInfo: EX-2
X-BEServer: EX-2
X-AspNet-Version: 4.0.30319
Set-Cookie: exchangecookie=a988e63f541647a592956c572e7b8de2; expires=Mon, 06-Feb-2023 10:14:42 GMT; path=/; HttpOnly
Set-Cookie: X-BackEndCookie=S-1-5-21-583588886-2180775141-1717425756-27500=u56Lnp2ejJqBzp3Hxs2dmc7Szs2bnNLLy53H0p7MzJvSxpqcyM6az8/JzZqagYHNz83N0s/M0s/Hq87Pxc7LxcvNgYWakZKakdGckI2Pgc8=; expires=Tue, 08-Mar-2022 10:14:42 GMT; path=/EWS; secure; HttpOnly
Persistent-Auth: true
X-Powered-By: ASP.NET
X-FEServer: EX-1
Date: Sun, 06 Feb 2022 10:14:41 GMT

密码hash

算法定义

  1. 将password字符串转化为Unicode16进制小端序列字符串

  2. 对Unicode字符串做MD4运算

如原始的密码为123456,则对应的密码hash为32ED87BDB5FDC5E9CBA88547376818D4

nodejs实现

var crypto = require('crypto');
function getNTLMHash(password){
  var buf = Buffer.from(password, 'utf16le');
  var md4 = crypto.createHash('md4');
  md4.update(buf);
  return Buffer.from(md4.digest());
}
var hash=getNTLMHash('123456')
console.log("ntlm hash:"+hash.toString('hex').toUpperCase())

java实现

implementation "com.sun.mail:javax.mail:1.6.2"
public static void main(String[] args) {
    String str="123456";
    byte[]bytes=str.getBytes(StandardCharsets.UTF_16LE);
    MD4 md4=new MD4();
    byte[]hash=md4.digest(bytes);
    System.out.println("ntlm hash:"+toHex(hash));
}

private static char[] hex = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };
private static String toHex(byte[] bytes) {
    char[] result = new char[bytes.length * 2];
    for (int index = 0, i = 0; index < bytes.length; index++) {
        int temp = bytes[index] & 0xFF;
        result[i++] = hex[temp >> 4];
        result[i++] = hex[temp & 0xF];
    }
    return new String(result);
}

OkHttp支持NTLM验证

<uses-library
    android:name="org.apache.http.legacy"
    android:required="false" />
  • 添加NTLMAuthenticator类,代码如下:
import android.text.TextUtils;

import org.apache.http.impl.auth.NTLMEngineException;

import java.util.List;
import java.util.Locale;

import okhttp3.Authenticator;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;

public class NTLMAuthenticator implements Authenticator {

    private final String domain;
    private final String username;
    private final String password;
    private final String workstation;
    private final NTLMEngineImpl engine = new NTLMEngineImpl();

    public NTLMAuthenticator(String username, String password, String domain, String workstation) {
        this.username = username;
        this.password = password;
        this.domain = !TextUtils.isEmpty(domain) ? domain.toUpperCase(Locale.ROOT) : "";
        this.workstation = !TextUtils.isEmpty(workstation) ? workstation.toUpperCase(Locale.ROOT) : "";
    }

    @Override
    public Request authenticate(Route route, Response response) {
        final List<String> WWWAuthenticate = response.headers().values("WWW-Authenticate");
        if (WWWAuthenticate.contains("NTLM")) {
            try {
                String ntlmMsg1 = engine.generateType1Msg(null, null);
                return response.request().newBuilder().header("Authorization", "NTLM " + ntlmMsg1).build();
            } catch (NTLMEngineException e) {
                e.printStackTrace();
            }
        }
        String ntlmMsg3 = null;
        try {
            ntlmMsg3 = engine.generateType3Msg(username, password, domain, workstation, WWWAuthenticate.get(0).substring(5));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return response.request().newBuilder().header("Authorization", "NTLM " + ntlmMsg3).build();
    }
}
  • 设置OkHttp
val client = OkHttpClient.Builder()
        .authenticator(NTLMAuthenticator("username", "password", "domain", ""))
        .build()

参考资源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • 文章目录 0x01前言简介 0x02基本介绍 1.本地登录认证 2.网络登录认证 0x03LM/NTLM版本优缺 ...
    全栈工程师修炼指南阅读 1,502评论 0 3
  • 一、Windows中的认证 1.1 单机认证 NTLM Hash是支持Net NTML认证协议及本地认证过程中的一...
    CJ21阅读 3,353评论 1 1
  • 1.LM Hash & NTLM Hash windows内部是不保存明文密码的,只保存密码的hash。其中,本机...
    小白荣阅读 4,420评论 0 1
  • 这篇文章介绍了 URL Loading System 相关知识,涉及以下内容: URLSession类型。 URL...
    pro648阅读 9,155评论 4 24
  • 在内部入侵测试期间,横向移动是白帽子黑客技术人员寻求信息以提高他或他对信息系统的特权的重要组成部分。在这种情况下,...
    5f4120c4213b阅读 507评论 0 0