ya-express-ntlm
Version:
316 lines (266 loc) • 10.6 kB
text/typescript
import { createHash, createHmac } from 'node:crypto';
import {
createDesEncrypt,
createLMHashedPasswordV1,
createNTHashedPasswordV1,
NTLMFlag,
NTLMTypeFlags,
Type2Message,
} from './core';
import { insertZerosEvery7Bits } from './utils';
export interface Type3MessageOptions {
readonly domain: string;
readonly workstation: string;
readonly username: string;
readonly password: string;
readonly lmPassword?: Buffer;
readonly ntPassword?: Buffer;
}
const ntlm2srCalcResp = (responseKeyNT: Buffer, serverChallenge: Buffer, clientChallenge: Buffer) => {
// padding with zeros to make the hash 16 bytes longer
const lmChallengeResponse = Buffer.alloc(clientChallenge.length + 16);
lmChallengeResponse.fill('\0');
clientChallenge.copy(lmChallengeResponse, 0, 0, clientChallenge.length);
const buf = Buffer.concat([serverChallenge, clientChallenge]);
const md5 = createHash('md5');
md5.update(buf);
const sess = md5.digest();
const ntChallengeResponse = calcResp(responseKeyNT, sess.subarray(0, 8));
return {
lmChallengeResponse,
ntChallengeResponse,
};
};
const calcResp = (passwordHash: Buffer, serverChallenge: Buffer): Buffer => {
// padding with zeros to make the hash 21 bytes long
const passHashPadded = Buffer.alloc(21);
passHashPadded.fill('\0');
passwordHash.copy(passHashPadded, 0, 0, passwordHash.length);
const resArray: Buffer[] = [];
let des = createDesEncrypt(insertZerosEvery7Bits(passHashPadded.subarray(0, 7)));
resArray.push(Buffer.from(des.update(serverChallenge.subarray(0, 8))));
des = createDesEncrypt(insertZerosEvery7Bits(passHashPadded.subarray(7, 14)));
resArray.push(Buffer.from(des.update(serverChallenge.subarray(0, 8))));
des = createDesEncrypt(insertZerosEvery7Bits(passHashPadded.subarray(14, 21)));
resArray.push(Buffer.from(des.update(serverChallenge.subarray(0, 8))));
return Buffer.concat(resArray);
};
const hmacMd5 = (key: Buffer, data: Buffer): Buffer => {
const hmac = createHmac('md5', key);
hmac.update(data);
return hmac.digest();
};
const NTOWFv2 = (pwHash: Buffer, user: string, domain: string) => hmacMd5(pwHash, Buffer.from(`${user.toUpperCase()}${domain}`, 'utf16le'));
const calcNtlmv2Resp = (
pwHash: Buffer,
username: string,
domain: string,
targetInfo: Buffer,
serverChallenge: Buffer,
clientChallenge: Buffer,
) => {
const responseKeyNTLM = NTOWFv2(pwHash, username, domain);
const lmV2ChallengeResponse = Buffer.concat([
hmacMd5(responseKeyNTLM, Buffer.concat([serverChallenge, clientChallenge])),
clientChallenge,
]);
// 11644473600000 = diff between 1970 and 1601
const now = Date.now();
const timestamp = (BigInt(now) + BigInt(11_644_473_600_000)) * BigInt(10_000); // we need BigInt to be able to write it to a buffer
const timestampBuffer = Buffer.alloc(8);
timestampBuffer.writeBigUInt64LE(timestamp);
const zero32Bit = Buffer.alloc(4, 0);
const temp = Buffer.concat([
// Version
Buffer.from([0x01, 0x01, 0x00, 0x00]),
zero32Bit,
timestampBuffer,
clientChallenge,
zero32Bit,
targetInfo,
zero32Bit,
]);
const proofString = hmacMd5(responseKeyNTLM, Buffer.concat([serverChallenge, temp]));
const ntV2ChallengeResponse = Buffer.concat([proofString, temp]);
return {
lmChallengeResponse: lmV2ChallengeResponse,
ntChallengeResponse: ntV2ChallengeResponse,
};
};
export const parseType2Message = (rawmsg: string): Type2Message => {
const match = rawmsg.match(/^NTLM\s+(.+?)(,|\s+|$)/);
if (!match?.[1]) {
throw new Error("Couldn't find NTLM in the message type2 comming from the server");
}
const buf = Buffer.from(match[1], 'base64');
const signature = buf.subarray(0, 8);
const type = buf.readInt16LE(8);
if (type !== 2) {
throw new Error("Server didn't return a type 2 message");
}
const targetNameLen = buf.readInt16LE(12);
const targetNameMaxLen = buf.readInt16LE(14);
const targetNameOffset = buf.readInt32LE(16);
const targetName = buf.subarray(targetNameOffset, targetNameOffset + targetNameMaxLen);
const negotiateFlags = buf.readInt32LE(20);
const serverChallenge = buf.subarray(24, 32);
const reserved = buf.subarray(32, 40);
const msg: Type2Message = {
signature,
type,
targetNameLen,
targetNameMaxLen,
targetNameOffset,
targetName,
negotiateFlags,
serverChallenge,
reserved,
};
if (negotiateFlags & NTLMFlag.NegotiateTargetInfo) {
msg.targetInfoLen = buf.readInt16LE(40);
msg.targetInfoMaxLen = buf.readInt16LE(42);
msg.targetInfoOffset = buf.readInt32LE(44);
msg.targetInfo = buf.subarray(msg.targetInfoOffset, msg.targetInfoOffset + msg.targetInfoLen);
}
return msg;
};
export const createMessageType3 = (msg2: Type2Message, options: Type3MessageOptions): string => {
const nonce = msg2.serverChallenge;
const { username } = options;
const { password } = options;
const { lmPassword } = options;
const { ntPassword } = options;
const { negotiateFlags } = msg2;
const isUnicode = negotiateFlags & NTLMFlag.NegotiateUnicode;
const isNegotiateExtendedSecurity = negotiateFlags & NTLMFlag.NegotiateExtendedSecurity;
const BODY_LENGTH = 72;
const domainName = encodeURIComponent(options.domain.toUpperCase());
const workstation = encodeURIComponent(options.workstation.toUpperCase());
let workstationBytes: Buffer;
let domainNameBytes: Buffer;
let usernameBytes: Buffer;
let encryptedRandomSessionKeyBytes: Buffer;
const encryptedRandomSessionKey = '';
if (isUnicode) {
workstationBytes = Buffer.from(workstation, 'utf16le');
domainNameBytes = Buffer.from(domainName, 'utf16le');
usernameBytes = Buffer.from(username, 'utf16le');
encryptedRandomSessionKeyBytes = Buffer.from(encryptedRandomSessionKey, 'utf16le');
} else {
workstationBytes = Buffer.from(workstation, 'ascii');
domainNameBytes = Buffer.from(domainName, 'ascii');
usernameBytes = Buffer.from(username, 'ascii');
encryptedRandomSessionKeyBytes = Buffer.from(encryptedRandomSessionKey, 'ascii');
}
let lmChallengeResponse = calcResp(lmPassword != null ? lmPassword : createLMHashedPasswordV1(password), nonce);
let ntChallengeResponse = calcResp(ntPassword != null ? ntPassword : createNTHashedPasswordV1(password), nonce);
if (isNegotiateExtendedSecurity) {
/*
* NTLMv2 extended security is enabled. While this technically can mean NTLMv2 extended security with NTLMv1 protocol,
* servers that support extended security likely also support NTLMv2, so use NTLMv2.
* This is also how curl implements NTLMv2 "detection".
* By using NTLMv2, this supports communication with servers that forbid the use of NTLMv1 (e.g. via windows policies)
*
* However, the target info is needed to construct the NTLMv2 response so if it can't be negotiated,
* fall back to NTLMv1 with NTLMv2 extended security.
*/
const pwHash = ntPassword != null ? ntPassword : createNTHashedPasswordV1(password);
let clientChallenge = '';
for (let i = 0; i < 8; i++) {
clientChallenge += String.fromCharCode(Math.floor(Math.random() * 256));
}
const clientChallengeBytes = Buffer.from(clientChallenge, 'ascii');
const challenges = msg2.targetInfo
? calcNtlmv2Resp(pwHash, username, domainName, msg2.targetInfo, nonce, clientChallengeBytes)
: ntlm2srCalcResp(pwHash, nonce, clientChallengeBytes);
({ lmChallengeResponse, ntChallengeResponse } = challenges);
}
const signature = 'NTLMSSP\0';
let pos = 0;
const buf = Buffer.alloc(
BODY_LENGTH
+ domainNameBytes.length
+ usernameBytes.length
+ workstationBytes.length
+ lmChallengeResponse.length
+ ntChallengeResponse.length
+ encryptedRandomSessionKeyBytes.length,
);
pos = buf.write(signature, pos, signature.length);
// type 1
pos = buf.writeUInt32LE(3, pos);
// LmChallengeResponseLen
pos = buf.writeUInt16LE(lmChallengeResponse.length, pos);
// LmChallengeResponseMaxLen
pos = buf.writeUInt16LE(lmChallengeResponse.length, pos);
// LmChallengeResponseOffset
pos = buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length, pos);
// NtChallengeResponseLen
pos = buf.writeUInt16LE(ntChallengeResponse.length, pos);
// NtChallengeResponseMaxLen
pos = buf.writeUInt16LE(ntChallengeResponse.length, pos);
// NtChallengeResponseOffset
pos = buf.writeUInt32LE(
BODY_LENGTH
+ domainNameBytes.length
+ usernameBytes.length
+ workstationBytes.length
+ lmChallengeResponse.length,
pos,
);
// DomainNameLen
pos = buf.writeUInt16LE(domainNameBytes.length, pos);
// DomainNameMaxLen
pos = buf.writeUInt16LE(domainNameBytes.length, pos);
// DomainNameOffset
pos = buf.writeUInt32LE(BODY_LENGTH, pos);
// UserNameLen
pos = buf.writeUInt16LE(usernameBytes.length, pos);
// UserNameMaxLen
pos = buf.writeUInt16LE(usernameBytes.length, pos);
// UserNameOffset
pos = buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length, pos);
// WorkstationLen
pos = buf.writeUInt16LE(workstationBytes.length, pos);
// WorkstationMaxLen
pos = buf.writeUInt16LE(workstationBytes.length, pos);
// WorkstationOffset
pos = buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length, pos);
// EncryptedRandomSessionKeyLen
pos = buf.writeUInt16LE(encryptedRandomSessionKeyBytes.length, pos);
// EncryptedRandomSessionKeyMaxLen
pos = buf.writeUInt16LE(encryptedRandomSessionKeyBytes.length, pos);
// EncryptedRandomSessionKeyOffset
pos = buf.writeUInt32LE(
BODY_LENGTH
+ domainNameBytes.length
+ usernameBytes.length
+ workstationBytes.length
+ lmChallengeResponse.length
+ ntChallengeResponse.length,
pos,
);
// NegotiateFlags =
pos = buf.writeUInt32LE(NTLMTypeFlags.TYPE2_FLAGS, pos);
// ProductMajorVersion
pos = buf.writeUInt8(5, pos);
// ProductMinorVersion
pos = buf.writeUInt8(1, pos);
// ProductBuild
pos = buf.writeUInt16LE(2600, pos);
// VersionReserved1
pos = buf.writeUInt8(0, pos);
// VersionReserved2
pos = buf.writeUInt8(0, pos);
// VersionReserved3
pos = buf.writeUInt8(0, pos);
// NTLMRevisionCurrent
pos = buf.writeUInt8(15, pos);
pos += domainNameBytes.copy(buf, pos);
pos += usernameBytes.copy(buf, pos);
pos += workstationBytes.copy(buf, pos);
pos += lmChallengeResponse.copy(buf, pos);
pos += ntChallengeResponse.copy(buf, pos);
encryptedRandomSessionKeyBytes.copy(buf, pos);
return `NTLM ${buf.toString('base64')}`;
};