Last active
April 15, 2021 00:11
-
-
Save wbroek/8c5b2b740ca095250fee77b200f73251 to your computer and use it in GitHub Desktop.
NTLM authentication with Axios and with none Node JS modules
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) 2019 Wouter van den Broek https://github.com/wbroek/ | |
* All rights reserved. | |
*/ | |
const axios = require('axios'); | |
const ntlm = require('./ntlm'); | |
// NODE JS | |
const https = require('https'); | |
const httpsAgent = new https.Agent({ keepAlive: true }); | |
const client = axios.create({ | |
httpsAgent, | |
agent: httpsAgent, | |
withCredentials: true, | |
shouldKeepAlive: true, | |
keepAlive: true, | |
keepAliveMsecs: 3000, | |
maxRedirects: 0, | |
'Access-Control-Allow-Origin': '*', | |
}); | |
var options = { | |
url: '', | |
username: '', | |
password: '', | |
workstation: '', | |
domain: '' | |
}; | |
client.interceptors.response.use( | |
(response) => { | |
// IF DEV console.log('Response:', response); | |
return response; | |
}, | |
(err) => { | |
// IF DEV console.log('Response error:',err); | |
const error = err.response; | |
if (error && error.status === 401 && error.headers['www-authenticate'] && error.headers['www-authenticate'] === 'Negotiate, NTLM' && !err.config.headers['X-retry']) { | |
// TYPE 1 MESSAGE | |
return sendType1Message(); | |
} else if (error && error.status === 401 && error.headers['www-authenticate'] && error.headers['www-authenticate'].substring(0,4) === 'NTLM' ) { | |
// TYPE 2 MESSAGE PARSE ANS TYPE 3 MESSAGE SEND | |
return sendType3Message(error.headers['www-authenticate']); | |
} | |
return err; | |
}, | |
); | |
client.interceptors.request.use((request) => { | |
// IF DEV console.log('Starting Request', request); | |
return request; | |
}); | |
const sendType1Message = () => { | |
var type1msg = ntlm.createType1Message(options); | |
return client({ | |
method: 'get', | |
url: options.url, | |
headers:{ | |
'Connection' : 'keep-alive', | |
'Authorization': type1msg | |
}, | |
}); | |
}; | |
const sendType3Message = token => { | |
var type2msg = ntlm.parseType2Message(token, (err) => { console.log(err) }); | |
var type3msg = ntlm.createType3Message(type2msg, options); | |
return client({ | |
method: 'get', | |
url: options.url, | |
headers:{ | |
'X-retry' : 'false', | |
'Connection' : 'Close', | |
'Authorization': type3msg | |
}, | |
}) | |
} | |
client({ | |
method: 'get', | |
url: options.url, | |
}).then((response) => { | |
console.log(response); | |
}) | |
.catch((error) => { | |
console.log(error); | |
}); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Original by Sam Decrock https://github.com/SamDecrock/ (c) 2013 | |
* Modified for use outside of Node.JS by Wouter van den Broek https://github.com/wbroek/ (c) 2019 | |
* All rights reserved. | |
*/ | |
var Buffer = require('buffer').Buffer; | |
var createCipheriv = require('browserify-cipher').createCipheriv; | |
var createHash = require('create-hash'); | |
var md4 = require('js-md4'); | |
var flags = { | |
NTLM_NegotiateUnicode : 0x00000001, | |
NTLM_NegotiateOEM : 0x00000002, | |
NTLM_RequestTarget : 0x00000004, | |
NTLM_Unknown9 : 0x00000008, | |
NTLM_NegotiateSign : 0x00000010, | |
NTLM_NegotiateSeal : 0x00000020, | |
NTLM_NegotiateDatagram : 0x00000040, | |
NTLM_NegotiateLanManagerKey : 0x00000080, | |
NTLM_Unknown8 : 0x00000100, | |
NTLM_NegotiateNTLM : 0x00000200, | |
NTLM_NegotiateNTOnly : 0x00000400, | |
NTLM_Anonymous : 0x00000800, | |
NTLM_NegotiateOemDomainSupplied : 0x00001000, | |
NTLM_NegotiateOemWorkstationSupplied : 0x00002000, | |
NTLM_Unknown6 : 0x00004000, | |
NTLM_NegotiateAlwaysSign : 0x00008000, | |
NTLM_TargetTypeDomain : 0x00010000, | |
NTLM_TargetTypeServer : 0x00020000, | |
NTLM_TargetTypeShare : 0x00040000, | |
NTLM_NegotiateExtendedSecurity : 0x00080000, | |
NTLM_NegotiateIdentify : 0x00100000, | |
NTLM_Unknown5 : 0x00200000, | |
NTLM_RequestNonNTSessionKey : 0x00400000, | |
NTLM_NegotiateTargetInfo : 0x00800000, | |
NTLM_Unknown4 : 0x01000000, | |
NTLM_NegotiateVersion : 0x02000000, | |
NTLM_Unknown3 : 0x04000000, | |
NTLM_Unknown2 : 0x08000000, | |
NTLM_Unknown1 : 0x10000000, | |
NTLM_Negotiate128 : 0x20000000, | |
NTLM_NegotiateKeyExchange : 0x40000000, | |
NTLM_Negotiate56 : 0x80000000 | |
}; | |
var typeflags = { | |
NTLM_TYPE1_FLAGS : flags.NTLM_NegotiateUnicode | |
+ flags.NTLM_NegotiateOEM | |
+ flags.NTLM_RequestTarget | |
+ flags.NTLM_NegotiateNTLM | |
+ flags.NTLM_NegotiateOemDomainSupplied | |
+ flags.NTLM_NegotiateOemWorkstationSupplied | |
+ flags.NTLM_NegotiateAlwaysSign | |
+ flags.NTLM_NegotiateExtendedSecurity | |
+ flags.NTLM_NegotiateVersion | |
+ flags.NTLM_Negotiate128 | |
+ flags.NTLM_Negotiate56, | |
NTLM_TYPE2_FLAGS : flags.NTLM_NegotiateUnicode | |
+ flags.NTLM_RequestTarget | |
+ flags.NTLM_NegotiateNTLM | |
+ flags.NTLM_NegotiateAlwaysSign | |
+ flags.NTLM_NegotiateExtendedSecurity | |
+ flags.NTLM_NegotiateTargetInfo | |
+ flags.NTLM_NegotiateVersion | |
+ flags.NTLM_Negotiate128 | |
+ flags.NTLM_Negotiate56 | |
}; | |
function createType1Message(options){ | |
var domain = escape(options.domain.toUpperCase()); | |
var workstation = escape(options.workstation.toUpperCase()); | |
var protocol = 'NTLMSSP\0'; | |
var BODY_LENGTH = 40; | |
var type1flags = typeflags.NTLM_TYPE1_FLAGS; | |
if(!domain || domain === '') | |
type1flags = type1flags - flags.NTLM_NegotiateOemDomainSupplied; | |
var pos = 0; | |
var buf = new Buffer(BODY_LENGTH + domain.length + workstation.length); | |
buf.write(protocol, pos, protocol.length); pos += protocol.length; // protocol | |
buf.writeUInt32LE(1, pos); pos += 4; // type 1 | |
buf.writeUInt32LE(type1flags, pos); pos += 4; // TYPE1 flag | |
buf.writeUInt16LE(domain.length, pos); pos += 2; // domain length | |
buf.writeUInt16LE(domain.length, pos); pos += 2; // domain max length | |
buf.writeUInt32LE(BODY_LENGTH + workstation.length, pos); pos += 4; // domain buffer offset | |
buf.writeUInt16LE(workstation.length, pos); pos += 2; // workstation length | |
buf.writeUInt16LE(workstation.length, pos); pos += 2; // workstation max length | |
buf.writeUInt32LE(BODY_LENGTH, pos); pos += 4; // workstation buffer offset | |
buf.writeUInt8(5, pos); pos += 1; //ProductMajorVersion | |
buf.writeUInt8(1, pos); pos += 1; //ProductMinorVersion | |
buf.writeUInt16LE(2600, pos); pos += 2; //ProductBuild | |
buf.writeUInt8(0 , pos); pos += 1; //VersionReserved1 | |
buf.writeUInt8(0 , pos); pos += 1; //VersionReserved2 | |
buf.writeUInt8(0 , pos); pos += 1; //VersionReserved3 | |
buf.writeUInt8(15, pos); pos += 1; //NTLMRevisionCurrent | |
// length checks is to fix issue #46 and possibly #57 | |
if(workstation.length !=0) buf.write(workstation, pos, workstation.length, 'ascii'); pos += workstation.length; // workstation string | |
if(domain.length !=0) buf.write(domain , pos, domain.length , 'ascii'); pos += domain.length; // domain string | |
return 'NTLM ' + buf.toString('base64'); | |
} | |
function parseType2Message(rawmsg, callback){ | |
var match = rawmsg.match(/NTLM (.+)?/); | |
if(!match || !match[1]) { | |
callback(new Error("Couldn't find NTLM in the message type2 comming from the server")); | |
return null; | |
} | |
var buf = new Buffer(match[1], 'base64'); | |
var msg = {}; | |
msg.signature = buf.slice(0, 8); | |
msg.type = buf.readInt16LE(8); | |
if(msg.type != 2) { | |
callback(new Error("Server didn't return a type 2 message")); | |
return null; | |
} | |
msg.targetNameLen = buf.readInt16LE(12); | |
msg.targetNameMaxLen = buf.readInt16LE(14); | |
msg.targetNameOffset = buf.readInt32LE(16); | |
msg.targetName = buf.slice(msg.targetNameOffset, msg.targetNameOffset + msg.targetNameMaxLen); | |
msg.negotiateFlags = buf.readInt32LE(20); | |
msg.serverChallenge = buf.slice(24, 32); | |
msg.reserved = buf.slice(32, 40); | |
if(msg.negotiateFlags & flags.NTLM_NegotiateTargetInfo){ | |
msg.targetInfoLen = buf.readInt16LE(40); | |
msg.targetInfoMaxLen = buf.readInt16LE(42); | |
msg.targetInfoOffset = buf.readInt32LE(44); | |
msg.targetInfo = buf.slice(msg.targetInfoOffset, msg.targetInfoOffset + msg.targetInfoLen); | |
} | |
return msg; | |
} | |
function createType3Message(msg2, options){ | |
var nonce = msg2.serverChallenge; | |
var username = options.username; | |
var password = options.password; | |
var lm_password = options.lm_password; | |
var nt_password = options.nt_password; | |
var negotiateFlags = msg2.negotiateFlags; | |
var isUnicode = negotiateFlags & flags.NTLM_NegotiateUnicode; | |
var isNegotiateExtendedSecurity = negotiateFlags & flags.NTLM_NegotiateExtendedSecurity; | |
var BODY_LENGTH = 72; | |
var domainName = escape(options.domain.toUpperCase()); | |
var workstation = escape(options.workstation.toUpperCase()); | |
var workstationBytes, domainNameBytes, usernameBytes, encryptedRandomSessionKeyBytes; | |
var encryptedRandomSessionKey = ""; | |
if(isUnicode){ | |
workstationBytes = new Buffer(workstation, 'utf16le'); | |
domainNameBytes = new Buffer(domainName, 'utf16le'); | |
usernameBytes = new Buffer(username, 'utf16le'); | |
encryptedRandomSessionKeyBytes = new Buffer(encryptedRandomSessionKey, 'utf16le'); | |
}else{ | |
workstationBytes = new Buffer(workstation, 'ascii'); | |
domainNameBytes = new Buffer(domainName, 'ascii'); | |
usernameBytes = new Buffer(username, 'ascii'); | |
encryptedRandomSessionKeyBytes = new Buffer(encryptedRandomSessionKey, 'ascii'); | |
} | |
var lmChallengeResponse = calc_resp((lm_password!=null)?lm_password:create_LM_hashed_password_v1(password), nonce); | |
var ntChallengeResponse = calc_resp((nt_password!=null)?nt_password:create_NT_hashed_password_v1(password), nonce); | |
if(isNegotiateExtendedSecurity){ | |
var pwhash = (nt_password!=null)?nt_password:create_NT_hashed_password_v1(password); | |
var clientChallenge = ""; | |
for(var i=0; i < 8; i++){ | |
clientChallenge += String.fromCharCode( Math.floor(Math.random()*256) ); | |
} | |
var clientChallengeBytes = new Buffer(clientChallenge, 'ascii'); | |
var challenges = ntlm2sr_calc_resp(pwhash, nonce, clientChallengeBytes); | |
lmChallengeResponse = challenges.lmChallengeResponse; | |
ntChallengeResponse = challenges.ntChallengeResponse; | |
} | |
var signature = 'NTLMSSP\0'; | |
var pos = 0; | |
var buf = new Buffer(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length + ntChallengeResponse.length + encryptedRandomSessionKeyBytes.length); | |
buf.write(signature, pos, signature.length); pos += signature.length; | |
buf.writeUInt32LE(3, pos); pos += 4; // type 1 | |
buf.writeUInt16LE(lmChallengeResponse.length, pos); pos += 2; // LmChallengeResponseLen | |
buf.writeUInt16LE(lmChallengeResponse.length, pos); pos += 2; // LmChallengeResponseMaxLen | |
buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length, pos); pos += 4; // LmChallengeResponseOffset | |
buf.writeUInt16LE(ntChallengeResponse.length, pos); pos += 2; // NtChallengeResponseLen | |
buf.writeUInt16LE(ntChallengeResponse.length, pos); pos += 2; // NtChallengeResponseMaxLen | |
buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length, pos); pos += 4; // NtChallengeResponseOffset | |
buf.writeUInt16LE(domainNameBytes.length, pos); pos += 2; // DomainNameLen | |
buf.writeUInt16LE(domainNameBytes.length, pos); pos += 2; // DomainNameMaxLen | |
buf.writeUInt32LE(BODY_LENGTH, pos); pos += 4; // DomainNameOffset | |
buf.writeUInt16LE(usernameBytes.length, pos); pos += 2; // UserNameLen | |
buf.writeUInt16LE(usernameBytes.length, pos); pos += 2; // UserNameMaxLen | |
buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length, pos); pos += 4; // UserNameOffset | |
buf.writeUInt16LE(workstationBytes.length, pos); pos += 2; // WorkstationLen | |
buf.writeUInt16LE(workstationBytes.length, pos); pos += 2; // WorkstationMaxLen | |
buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length, pos); pos += 4; // WorkstationOffset | |
buf.writeUInt16LE(encryptedRandomSessionKeyBytes.length, pos); pos += 2; // EncryptedRandomSessionKeyLen | |
buf.writeUInt16LE(encryptedRandomSessionKeyBytes.length, pos); pos += 2; // EncryptedRandomSessionKeyMaxLen | |
buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length + ntChallengeResponse.length, pos); pos += 4; // EncryptedRandomSessionKeyOffset | |
buf.writeUInt32LE(typeflags.NTLM_TYPE2_FLAGS, pos); pos += 4; // NegotiateFlags | |
buf.writeUInt8(5, pos); pos++; // ProductMajorVersion | |
buf.writeUInt8(1, pos); pos++; // ProductMinorVersion | |
buf.writeUInt16LE(2600, pos); pos += 2; // ProductBuild | |
buf.writeUInt8(0, pos); pos++; // VersionReserved1 | |
buf.writeUInt8(0, pos); pos++; // VersionReserved2 | |
buf.writeUInt8(0, pos); pos++; // VersionReserved3 | |
buf.writeUInt8(15, pos); pos++; // NTLMRevisionCurrent | |
domainNameBytes.copy(buf, pos); pos += domainNameBytes.length; | |
usernameBytes.copy(buf, pos); pos += usernameBytes.length; | |
workstationBytes.copy(buf, pos); pos += workstationBytes.length; | |
lmChallengeResponse.copy(buf, pos); pos += lmChallengeResponse.length; | |
ntChallengeResponse.copy(buf, pos); pos += ntChallengeResponse.length; | |
encryptedRandomSessionKeyBytes.copy(buf, pos); pos += encryptedRandomSessionKeyBytes.length; | |
return 'NTLM ' + buf.toString('base64'); | |
} | |
function create_LM_hashed_password_v1(password){ | |
// fix the password length to 14 bytes | |
password = password.toUpperCase(); | |
var passwordBytes = new Buffer(password, 'ascii'); | |
var passwordBytesPadded = new Buffer(14); | |
passwordBytesPadded.fill("\0"); | |
var sourceEnd = 14; | |
if(passwordBytes.length < 14) sourceEnd = passwordBytes.length; | |
passwordBytes.copy(passwordBytesPadded, 0, 0, sourceEnd); | |
// split into 2 parts of 7 bytes: | |
var firstPart = passwordBytesPadded.slice(0,7); | |
var secondPart = passwordBytesPadded.slice(7); | |
function encrypt(buf){ | |
var key = insertZerosEvery7Bits(buf); | |
var des = createCipheriv('DES-ECB', key, ''); | |
return des.update("KGS!@#$%"); // page 57 in [MS-NLMP]); | |
} | |
var firstPartEncrypted = encrypt(firstPart); | |
var secondPartEncrypted = encrypt(secondPart); | |
return Buffer.concat([firstPartEncrypted, secondPartEncrypted]); | |
} | |
function insertZerosEvery7Bits(buf){ | |
var binaryArray = bytes2binaryArray(buf); | |
var newBinaryArray = []; | |
for(var i=0; i<binaryArray.length; i++){ | |
newBinaryArray.push(binaryArray[i]); | |
if((i+1)%7 === 0){ | |
newBinaryArray.push(0); | |
} | |
} | |
return binaryArray2bytes(newBinaryArray); | |
} | |
function bytes2binaryArray(buf){ | |
var hex2binary = { | |
0: [0,0,0,0], | |
1: [0,0,0,1], | |
2: [0,0,1,0], | |
3: [0,0,1,1], | |
4: [0,1,0,0], | |
5: [0,1,0,1], | |
6: [0,1,1,0], | |
7: [0,1,1,1], | |
8: [1,0,0,0], | |
9: [1,0,0,1], | |
A: [1,0,1,0], | |
B: [1,0,1,1], | |
C: [1,1,0,0], | |
D: [1,1,0,1], | |
E: [1,1,1,0], | |
F: [1,1,1,1] | |
}; | |
var hexString = buf.toString('hex').toUpperCase(); | |
var array = []; | |
for(var i=0; i<hexString.length; i++){ | |
var hexchar = hexString.charAt(i); | |
array = array.concat(hex2binary[hexchar]); | |
} | |
return array; | |
} | |
function binaryArray2bytes(array){ | |
var binary2hex = { | |
'0000': 0, | |
'0001': 1, | |
'0010': 2, | |
'0011': 3, | |
'0100': 4, | |
'0101': 5, | |
'0110': 6, | |
'0111': 7, | |
'1000': 8, | |
'1001': 9, | |
'1010': 'A', | |
'1011': 'B', | |
'1100': 'C', | |
'1101': 'D', | |
'1110': 'E', | |
'1111': 'F' | |
}; | |
var bufArray = []; | |
for(var i=0; i<array.length; i +=8 ){ | |
if((i+7) > array.length) | |
break; | |
var binString1 = '' + array[i] + '' + array[i+1] + '' + array[i+2] + '' + array[i+3]; | |
var binString2 = '' + array[i+4] + '' + array[i+5] + '' + array[i+6] + '' + array[i+7]; | |
var hexchar1 = binary2hex[binString1]; | |
var hexchar2 = binary2hex[binString2]; | |
var buf = new Buffer(hexchar1 + '' + hexchar2, 'hex'); | |
bufArray.push(buf); | |
} | |
return Buffer.concat(bufArray); | |
} | |
function create_NT_hashed_password_v1(password){ | |
var buf = new Buffer(password, 'utf16le'); | |
var hash = md4.create(); | |
hash.update(buf); | |
return new Buffer(hash.digest()); | |
} | |
function calc_resp(password_hash, server_challenge){ | |
// padding with zeros to make the hash 21 bytes long | |
var passHashPadded = new Buffer(21); | |
passHashPadded.fill("\0"); | |
password_hash.copy(passHashPadded, 0, 0, password_hash.length); | |
var resArray = []; | |
var des = createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(0,7)), ''); | |
resArray.push( des.update(server_challenge.slice(0,8)) ); | |
des = createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(7,14)), ''); | |
resArray.push( des.update(server_challenge.slice(0,8)) ); | |
des = createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(14,21)), ''); | |
resArray.push( des.update(server_challenge.slice(0,8)) ); | |
return Buffer.concat(resArray); | |
} | |
function ntlm2sr_calc_resp(responseKeyNT, serverChallenge, clientChallenge){ | |
// padding with zeros to make the hash 16 bytes longer | |
var lmChallengeResponse = new Buffer(clientChallenge.length + 16); | |
lmChallengeResponse.fill("\0"); | |
clientChallenge.copy(lmChallengeResponse, 0, 0, clientChallenge.length); | |
var buf = Buffer.concat([serverChallenge, clientChallenge]); | |
var md5 = createHash('md5'); | |
md5.update(buf); | |
var sess = md5.digest(); | |
var ntChallengeResponse = calc_resp(responseKeyNT, sess.slice(0,8)); | |
return { | |
lmChallengeResponse: lmChallengeResponse, | |
ntChallengeResponse: ntChallengeResponse | |
}; | |
} | |
exports.createType1Message = createType1Message; | |
exports.parseType2Message = parseType2Message; | |
exports.createType3Message = createType3Message; | |
exports.create_NT_hashed_password = create_NT_hashed_password_v1; | |
exports.create_LM_hashed_password = create_LM_hashed_password_v1; | |
After more testing I think my initial conclusion is wrong.
This test shows that the insertZerosEvery7Bits
is properly creating a "none" parity bit.
it("parity (none)", async () => {
const password = "SecREt01".toLocaleUpperCase().padEnd(14, "\0");
const buf1 = Buffer.from(password.slice(0, 7));
const buf2 = insertZerosEvery7Bits(buf1);
const buf3 = setParity(buf1, "none");
console.log(buf1);
console.log(buf2);
console.log(buf3);
expect(buf2).toEqual(buf3);
});
PASS src/__tests__/ntlm.ts
NTLM
✓ parity (none) (8 ms)
console.log
<Buffer 53 45 43 52 45 54 30>
console.log
<Buffer 52 a2 50 6a 24 2a 50 60>
console.log
<Buffer 52 a2 50 6a 24 2a 50 60>
So I'm still not sure why the NTLM in this GIST does not work.
I implemented this in a React Native Application, and I couldn't make it work. Keeping getting 401 Error, no matter what.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I think I understand now after reading this: http://davenport.sourceforge.net/ntlm.html#theLmResponse
Unfortunately I don't think
insertZerosEvery7Bits
is working correctly and therefore breaking the encryption.I did create a substitute function that adds a parity bit to each byte of a buffer:
which also uses this function: