| 1 | const inherits = require('util').inherits; |
| 2 | const type = require('../core').type; |
| 3 | const events = require('events'); |
| 4 | const crypto = require('crypto'); |
| 5 | const forge = require('node-forge'); |
| 6 | const asn1 = forge.asn1; |
| 7 | const pki = forge.pki; |
| 8 | |
| 9 | /** |
| 10 | * NLA layer of rdp stack |
| 11 | */ |
| 12 | function NLA(transport, nlaCompletedFunc, domain, user, password) { |
| 13 | // Get NTLM ready |
| 14 | const ntlm = Create_Ntlm(); |
| 15 | ntlm.domain = domain; |
| 16 | ntlm.completedFunc = nlaCompletedFunc; |
| 17 | ntlm.user = user; |
| 18 | ntlm.password = password; |
| 19 | ntlm.response_key_nt = ntowfv2(ntlm.password, ntlm.user, ntlm.domain); |
| 20 | ntlm.response_key_lm = lmowfv2(ntlm.password, ntlm.user, ntlm.domain); |
| 21 | this.ntlm = ntlm; |
| 22 | this.state = 1; |
| 23 | |
| 24 | // Get transport ready |
| 25 | this.transport = transport; |
| 26 | // Wait 2 bytes |
| 27 | this.transport.expect(2); |
| 28 | // Next state is receive header |
| 29 | var self = this; |
| 30 | |
| 31 | this.oldDataListeners = this.transport.listeners('data'); |
| 32 | this.oldCloseListeners = this.transport.listeners('close'); |
| 33 | this.oldErrorListeners = this.transport.listeners('error'); |
| 34 | |
| 35 | // Unhook the previous transport handler |
| 36 | this.transport.removeAllListeners('data'); |
| 37 | this.transport.removeAllListeners('close'); |
| 38 | this.transport.removeAllListeners('error'); |
| 39 | |
| 40 | // Hook this module as the transport handler |
| 41 | this.transport.once('data', function (s) { |
| 42 | self.recvHeader(s); |
| 43 | }).on('close', function () { |
| 44 | self.emit('close'); |
| 45 | }).on('error', function (err) { |
| 46 | self.emit('close'); // Errors occur when NLA authentication fails, for now, just close. |
| 47 | //self.emit('error', err); |
| 48 | }); |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * inherit from a packet layer |
| 53 | */ |
| 54 | inherits(NLA, events.EventEmitter); |
| 55 | |
| 56 | /** |
| 57 | * Receive correct packet as expected |
| 58 | * @param s {type.Stream} |
| 59 | */ |
| 60 | NLA.prototype.recvHeader = function (s) { |
| 61 | //console.log('NLA - recvHeader', s); |
| 62 | var self = this; |
| 63 | var derType = new type.UInt8().read(s).value; |
| 64 | var derLen = new type.UInt8().read(s).value; |
| 65 | self.buffers = [ s.buffer ]; |
| 66 | |
| 67 | if (derLen < 128) { |
| 68 | // wait for the entire data block |
| 69 | this.transport.expect(derLen); |
| 70 | this.transport.once('data', function (s) { self.recvData(s); }); |
| 71 | } else { |
| 72 | // wait for the header size |
| 73 | this.transport.expect(derLen - 128); |
| 74 | this.transport.once('data', function (s) { self.recvHeaderSize(s); }); |
| 75 | } |
| 76 | |
| 77 | //console.log('NLA - DER', derType, derLen); |
| 78 | }; |
| 79 | |
| 80 | /** |
| 81 | * Receive correct packet as expected |
| 82 | * @param s {type.Stream} |
| 83 | */ |
| 84 | NLA.prototype.recvHeaderSize = function (s) { |
| 85 | //console.log('NLA - recvHeaderSize', s.buffer.length); |
| 86 | var self = this; |
| 87 | self.buffers.push(s.buffer); |
| 88 | if (s.buffer.length == 1) { |
| 89 | // wait for the entire data block |
| 90 | var derLen = s.buffer.readUInt8(0); |
| 91 | this.transport.expect(derLen); |
| 92 | this.transport.once('data', function (s) { self.recvData(s); }); |
| 93 | } else if (s.buffer.length == 2) { |
| 94 | // wait for the entire data block |
| 95 | var derLen = s.buffer.readUInt16BE(0); |
| 96 | this.transport.expect(derLen); |
| 97 | this.transport.once('data', function (s) { self.recvData(s); }); |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Receive correct packet as expected |
| 103 | * @param s {type.Stream} |
| 104 | */ |
| 105 | NLA.prototype.recvData = function (s) { |
| 106 | //console.log('NLA - recvData', s.buffer.length); |
| 107 | var self = this; |
| 108 | self.buffers.push(s.buffer); |
| 109 | var entireBuffer = Buffer.concat(self.buffers); |
| 110 | //console.log('entireBuffer', entireBuffer.toString('hex')); |
| 111 | |
| 112 | // We have a full ASN1 data block, decode it now |
| 113 | const der = asn1.fromDer(entireBuffer.toString('binary')); |
| 114 | const derNum = der.value[0].value[0].value.charCodeAt(0); |
| 115 | //console.log('NLA - Number', derNum); |
| 116 | |
| 117 | if (derNum == 6) { |
| 118 | if (this.state == 1) { |
| 119 | const derBuffer = Buffer.from(der.value[1].value[0].value[0].value[0].value[0].value, 'binary'); |
| 120 | const client_challenge = read_challenge_message(this.ntlm, derBuffer); |
| 121 | self.security_interface = build_security_interface(this.ntlm); |
| 122 | const peer_cert = this.transport.secureSocket.getPeerCertificate(); |
| 123 | const challenge = create_ts_authenticate(client_challenge, self.security_interface.gss_wrapex(peer_cert.pubkey.slice(24))); |
| 124 | this.ntlm.publicKeyDer = peer_cert.pubkey.slice(24); |
| 125 | this.send(challenge); |
| 126 | this.state = 2; |
| 127 | } else if (this.state == 2) { |
| 128 | const derBuffer = Buffer.from(der.value[1].value[0].value, 'binary'); |
| 129 | const publicKeyDer = self.security_interface.gss_unwrapex(derBuffer); |
| 130 | |
| 131 | // Check that the public key is identical except the first byte which is the DER encoding type. |
| 132 | if (!this.ntlm.publicKeyDer.slice(1).equals(publicKeyDer.slice(1))) { console.log('RDP man-in-the-middle detected.'); close(); return; } |
| 133 | delete this.ntlm.publicKeyDer; // Clean this up, we don't need it anymore. |
| 134 | |
| 135 | var xdomain, xuser, xpassword; |
| 136 | if (this.ntlm.is_unicode) { |
| 137 | xdomain = toUnicode(this.ntlm.domain); |
| 138 | xuser = toUnicode(this.ntlm.user); |
| 139 | xpassword = toUnicode(this.ntlm.password); |
| 140 | } else { |
| 141 | xdomain = Buffer.from(this.ntlm.domain, 'utf8'); |
| 142 | xuser = Buffer.from(this.ntlm.user, 'utf8'); |
| 143 | xpassword = Buffer.from(this.ntlm.password, 'utf8'); |
| 144 | } |
| 145 | |
| 146 | const credentials = create_ts_authinfo(self.security_interface.gss_wrapex(create_ts_credentials(xdomain, xuser, xpassword))); |
| 147 | this.send(credentials); |
| 148 | |
| 149 | // Rehook the previous transport handler |
| 150 | this.transport.removeAllListeners('data'); |
| 151 | this.transport.removeAllListeners('close'); |
| 152 | this.transport.removeAllListeners('error'); |
| 153 | |
| 154 | for (var i in this.oldDataListeners) { this.transport.once('data', this.oldDataListeners[i]); } |
| 155 | for (var i in this.oldCloseListeners) { this.transport.on('close', this.oldCloseListeners[i]); } |
| 156 | for (var i in this.oldErrorListeners) { this.transport.on('error', this.oldErrorListeners[i]); } |
| 157 | |
| 158 | // Done! |
| 159 | this.transport.expect(2); |
| 160 | this.state = 3; |
| 161 | this.ntlm.completedFunc(); |
| 162 | return; |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | // Receive next block of data |
| 167 | this.transport.expect(2); |
| 168 | this.transport.once('data', function (s) { self.recvHeader(s); }); |
| 169 | } |
| 170 | |
| 171 | |
| 172 | /** |
| 173 | * Send message throught NLA layer |
| 174 | * @param message {type.*} |
| 175 | */ |
| 176 | NLA.prototype.send = function (message) { |
| 177 | this.transport.sendBuffer(message); |
| 178 | }; |
| 179 | |
| 180 | /** |
| 181 | * close stack |
| 182 | */ |
| 183 | NLA.prototype.close = function() { |
| 184 | this.transport.close(); |
| 185 | }; |
| 186 | |
| 187 | |
| 188 | NLA.prototype.sendNegotiateMessage = function () { |
| 189 | // Create create_ts_request |
| 190 | this.ntlm.negotiate_message = create_negotiate_message(); |
| 191 | const asn1obj = |
| 192 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 193 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 194 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, asn1.integerToDer(2)), |
| 195 | ]), |
| 196 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| 197 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 198 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 199 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 200 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, this.ntlm.negotiate_message.toString('binary')) |
| 201 | ]) |
| 202 | ]) |
| 203 | ]) |
| 204 | ]) |
| 205 | ]); |
| 206 | |
| 207 | // Serialize an ASN.1 object to DER format |
| 208 | this.send(Buffer.from(asn1.toDer(asn1obj).data, 'binary')); |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Module exports |
| 213 | */ |
| 214 | module.exports = NLA; |
| 215 | |
| 216 | |
| 217 | |
| 218 | |
| 219 | |
| 220 | |
| 221 | const NegotiateFlags = { |
| 222 | NtlmsspNegociate56: 0x80000000, |
| 223 | NtlmsspNegociateKeyExch: 0x40000000, |
| 224 | NtlmsspNegociate128: 0x20000000, |
| 225 | NtlmsspNegociateVersion: 0x02000000, |
| 226 | NtlmsspNegociateTargetInfo: 0x00800000, |
| 227 | NtlmsspRequestNonNTSessionKey: 0x00400000, |
| 228 | NtlmsspNegociateIdentify: 0x00100000, |
| 229 | NtlmsspNegociateExtendedSessionSecurity: 0x00080000, |
| 230 | NtlmsspTargetTypeServer: 0x00020000, |
| 231 | NtlmsspTargetTypeDomain: 0x00010000, |
| 232 | NtlmsspNegociateAlwaysSign: 0x00008000, |
| 233 | NtlmsspNegociateOEMWorkstationSupplied: 0x00002000, |
| 234 | NtlmsspNegociateOEMDomainSupplied: 0x00001000, |
| 235 | NtlmsspNegociateNTLM: 0x00000200, |
| 236 | NtlmsspNegociateLMKey: 0x00000080, |
| 237 | NtlmsspNegociateDatagram: 0x00000040, |
| 238 | NtlmsspNegociateSeal: 0x00000020, |
| 239 | NtlmsspNegociateSign: 0x00000010, |
| 240 | NtlmsspRequestTarget: 0x00000004, |
| 241 | NtlmNegotiateOEM: 0x00000002, |
| 242 | NtlmsspNegociateUnicode: 0x00000001 |
| 243 | } |
| 244 | |
| 245 | const MajorVersion = { |
| 246 | WindowsMajorVersion5: 0x05, |
| 247 | WindowsMajorVersion6: 0x06 |
| 248 | } |
| 249 | |
| 250 | const MinorVersion = { |
| 251 | WindowsMinorVersion0: 0x00, |
| 252 | WindowsMinorVersion1: 0x01, |
| 253 | WindowsMinorVersion2: 0x02, |
| 254 | WindowsMinorVersion3: 0x03 |
| 255 | } |
| 256 | |
| 257 | const NTLMRevision = { |
| 258 | NtlmSspRevisionW2K3: 0x0F |
| 259 | } |
| 260 | |
| 261 | function decodeTargetInfo(targetInfoBuf) { |
| 262 | var r = {}, type, len, data, ptr = 0; |
| 263 | while (true) { |
| 264 | type = targetInfoBuf.readInt16LE(ptr); |
| 265 | if (type == 0) break; |
| 266 | len = targetInfoBuf.readInt16LE(ptr + 2); |
| 267 | r[type] = targetInfoBuf.slice(ptr + 4, ptr + 4 + len); |
| 268 | ptr += (4 + len); |
| 269 | } |
| 270 | return r; |
| 271 | } |
| 272 | |
| 273 | function bufToArr(b) { var r = []; for (var i = 0; i < b.length; i++) { r.push(b.readUInt8(i)); } return r; } // For unit testing |
| 274 | function compareArray(a, b) { if (a.length != b.length) return false; for (var i = 0; i < a.length; i++) { if (a[i] != b[i]) return false; } return true; } // For unit testing |
| 275 | function toUnicode(str) { return Buffer.from(str, 'ucs2'); } |
| 276 | function md4(buffer) { |
| 277 | try { |
| 278 | return crypto.createHash('md4').update(buffer).digest(); // Built in NodeJS MD4, this does not work starting with NodeJS v17 |
| 279 | } catch (ex) { |
| 280 | return Buffer.from(require('../security/md4').array(buffer.toString('binary'))); // This is the alternative if NodeJS does not support MD4 |
| 281 | } |
| 282 | } |
| 283 | function md5(str) { return crypto.createHash('md5').update(str).digest(); } |
| 284 | function hmac_md5(key, data) { return crypto.createHmac('md5', key).update(data).digest(); } |
| 285 | function ntowfv2(password, user, domain) { return hmac_md5(md4(toUnicode(password)), toUnicode(user.toUpperCase() + domain)); } |
| 286 | function lmowfv2(password, user, domain) { return ntowfv2(password, user, domain); } |
| 287 | function zeroBuffer(len) { return Buffer.alloc(len); } |
| 288 | function compute_response_v2(response_key_nt, response_key_lm, server_challenge, client_challenge, time, server_name) { |
| 289 | const response_version = Buffer.from('01', 'hex'); |
| 290 | const hi_response_version = Buffer.from('01', 'hex'); |
| 291 | const temp = Buffer.concat([response_version, hi_response_version, zeroBuffer(6), time, client_challenge, zeroBuffer(4), server_name]); |
| 292 | const nt_proof_str = hmac_md5(response_key_nt, Buffer.concat([server_challenge, temp])); |
| 293 | const nt_challenge_response = Buffer.concat([nt_proof_str, temp]); |
| 294 | const lm_challenge_response = Buffer.concat([hmac_md5(response_key_lm, Buffer.concat([server_challenge, client_challenge])), client_challenge]); |
| 295 | const session_base_key = hmac_md5(response_key_nt, nt_proof_str); |
| 296 | return [nt_challenge_response, lm_challenge_response, session_base_key]; |
| 297 | } |
| 298 | function kx_key_v2(session_base_key, _lm_challenge_response, _server_challenge) { return session_base_key; } |
| 299 | function rc4k(key, data) { return createRC4(key).update(data); } |
| 300 | |
| 301 | function createRC4(key) { |
| 302 | const obj = {}; |
| 303 | try { |
| 304 | obj.n = crypto.createCipheriv('rc4', key, null); // Built in NodeJS RC4, this does not work starting with NodeJS v17 |
| 305 | obj.update = function(x) { return obj.n.update(x); } |
| 306 | } catch (ex) { |
| 307 | const RC4 = require('../security/rc4'); // This is the alternative if NodeJS does not support RC4 |
| 308 | obj.r = new RC4(key.toString('binary')); |
| 309 | obj.update = function (x) { return Buffer.from(obj.r.encrypt(x.toString('binary')), 'hex'); } |
| 310 | } |
| 311 | return obj; |
| 312 | } |
| 313 | |
| 314 | function create_negotiate_message() { |
| 315 | return negotiate_message( |
| 316 | NegotiateFlags.NtlmsspNegociateKeyExch | |
| 317 | NegotiateFlags.NtlmsspNegociate128 | |
| 318 | NegotiateFlags.NtlmsspNegociateExtendedSessionSecurity | |
| 319 | NegotiateFlags.NtlmsspNegociateAlwaysSign | |
| 320 | NegotiateFlags.NtlmsspNegociateNTLM | |
| 321 | NegotiateFlags.NtlmsspNegociateSeal | |
| 322 | NegotiateFlags.NtlmsspNegociateSign | |
| 323 | NegotiateFlags.NtlmsspRequestTarget | |
| 324 | NegotiateFlags.NtlmsspNegociateUnicode, Buffer.alloc(0), Buffer.alloc(0) |
| 325 | ); |
| 326 | } |
| 327 | |
| 328 | function negotiate_message(flags, domain, workstation) { |
| 329 | const offset = ((flags & NegotiateFlags.NtlmsspNegociateVersion) == 0) ? 32 : 40; |
| 330 | const buf = Buffer.alloc(offset); |
| 331 | buf.write('4e544c4d53535000', 0, 8, 'hex'); // Signature (NTLMSP\0) |
| 332 | buf.writeInt32LE(1, 8); // MessageType (1) |
| 333 | buf.writeInt32LE(flags, 12); // Flags |
| 334 | buf.writeInt16LE(domain.length, 16); // DomainNameLen |
| 335 | buf.writeInt16LE(domain.length, 18); // DomainNameMaxLen |
| 336 | if (domain.length > 0) { buf.writeInt32LE(offset, 20); } // DomainNameBufferOffset |
| 337 | buf.writeInt16LE(workstation.length, 24); // WorkstationLen |
| 338 | buf.writeInt16LE(workstation.length, 26); // WorkstationMaxLen |
| 339 | if (workstation.length > 0) { buf.writeInt32LE(offset + domain.length, 28); } // WorkstationBufferOffset |
| 340 | if ((flags & NegotiateFlags.NtlmsspNegociateVersion) != 0) { |
| 341 | buf.writeUInt8(MajorVersion.WindowsMajorVersion6, 32); // ProductMajorVersion |
| 342 | buf.writeUInt8(MinorVersion.WindowsMinorVersion0, 33); // ProductMinorVersion |
| 343 | buf.writeInt16LE(6002, 34); // ProductBuild |
| 344 | //buf.writeInt16LE(0, 36); // Reserved |
| 345 | //buf.writeUInt8(0, 38); // Reserved |
| 346 | buf.writeUInt8(NTLMRevision.NtlmSspRevisionW2K3, 39); // NTLMRevisionCurrent |
| 347 | } |
| 348 | return Buffer.concat([buf, domain, workstation]); |
| 349 | } |
| 350 | |
| 351 | function mac(rc4_handle, signing_key, seq_num, data) { |
| 352 | const buf = Buffer.alloc(4); |
| 353 | buf.writeInt32LE(seq_num, 0); |
| 354 | var signature = hmac_md5(signing_key, Buffer.concat([buf, data])); |
| 355 | return message_signature_ex(rc4_handle.update(signature.slice(0, 8)), seq_num); |
| 356 | } |
| 357 | |
| 358 | function message_signature_ex(check_sum, seq_num) { |
| 359 | const buf = Buffer.alloc(16); |
| 360 | buf.writeInt32LE(1, 0); // Version |
| 361 | if (check_sum) { check_sum.copy(buf, 4, 0, 8); } // check_sum |
| 362 | if (seq_num) { buf.writeInt32LE(seq_num, 12); } // seq_num |
| 363 | return buf; |
| 364 | } |
| 365 | |
| 366 | /// Compute a signature of all data exchange during NTLMv2 handshake |
| 367 | function mic(exported_session_key, negotiate_message, challenge_message, authenticate_message) { return hmac_md5(exported_session_key, Buffer.concat([negotiate_message, challenge_message, authenticate_message])); } |
| 368 | |
| 369 | /// NTLMv2 security interface generate a sign key |
| 370 | /// By using MD5 of the session key + a static member (sentense) |
| 371 | function sign_key(exported_session_key, is_client) { |
| 372 | if (is_client) { |
| 373 | return md5(Buffer.concat([exported_session_key, Buffer.from("session key to client-to-server signing key magic constant\0")])); |
| 374 | } else { |
| 375 | return md5(Buffer.concat([exported_session_key, Buffer.from("session key to server-to-client signing key magic constant\0")])); |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | /// NTLMv2 security interface generate a seal key |
| 380 | /// By using MD5 of the session key + a static member (sentense) |
| 381 | function seal_key(exported_session_key, is_client) { |
| 382 | if (is_client) { |
| 383 | return md5(Buffer.concat([exported_session_key, Buffer.from("session key to client-to-server sealing key magic constant\0")])); |
| 384 | } else { |
| 385 | return md5(Buffer.concat([exported_session_key, Buffer.from("session key to server-to-client sealing key magic constant\0")])); |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | /// We are now able to build a security interface |
| 390 | /// that will be used by the CSSP manager to cipherring message (private keys) |
| 391 | /// To detect MITM attack |
| 392 | function build_security_interface(ntlm) { |
| 393 | const obj = {}; |
| 394 | if (ntlm) { |
| 395 | obj.signing_key = sign_key(ntlm.exported_session_key, true); |
| 396 | obj.verify_key = sign_key(ntlm.exported_session_key, false); |
| 397 | const client_sealing_key = seal_key(ntlm.exported_session_key, true); |
| 398 | const server_sealing_key = seal_key(ntlm.exported_session_key, false); |
| 399 | obj.encrypt = createRC4(client_sealing_key); |
| 400 | obj.decrypt = createRC4(server_sealing_key); |
| 401 | } |
| 402 | obj.seq_num = 0; |
| 403 | |
| 404 | obj.gss_wrapex = function (data) { |
| 405 | const encrypted_data = obj.encrypt.update(data); |
| 406 | const signature = mac(obj.encrypt, obj.signing_key, obj.seq_num, data); |
| 407 | obj.seq_num++; |
| 408 | return Buffer.concat([signature, encrypted_data]); |
| 409 | } |
| 410 | |
| 411 | obj.gss_unwrapex = function (data) { |
| 412 | const version = data.readInt32LE(0); |
| 413 | const checksum = data.slice(4, 12); |
| 414 | const seqnum = data.readInt32LE(12); |
| 415 | const payload = data.slice(16); |
| 416 | const plaintext_payload = obj.decrypt.update(payload); |
| 417 | const plaintext_checksum = obj.decrypt.update(checksum); |
| 418 | const seqnumbuf = Buffer.alloc(4); |
| 419 | seqnumbuf.writeInt32LE(seqnum, 0); |
| 420 | const computed_checksum = hmac_md5(obj.verify_key, Buffer.concat([seqnumbuf, plaintext_payload])).slice(0, 8); |
| 421 | if (!plaintext_checksum.equals(computed_checksum)) { console.log("Invalid checksum on NTLMv2"); } |
| 422 | return plaintext_payload; |
| 423 | } |
| 424 | |
| 425 | return obj; |
| 426 | } |
| 427 | |
| 428 | function Create_Ntlm() { |
| 429 | return { |
| 430 | /// Microsoft Domain for Active Directory |
| 431 | domain: "", //String, |
| 432 | /// Username |
| 433 | user: "", //String, |
| 434 | /// Password |
| 435 | password: "", // String, |
| 436 | /// Key generated from NTLM hash |
| 437 | response_key_nt: null, // Buffer |
| 438 | /// Key generated from NTLM hash |
| 439 | response_key_lm: null, // Buffer |
| 440 | /// Keep trace of each messages to compute a final hash |
| 441 | negotiate_message: null, // Buffer |
| 442 | /// Key use to ciphering messages |
| 443 | exported_session_key: crypto.randomBytes(16), // Buffer |
| 444 | /// True if session use unicode |
| 445 | is_unicode: false // Boolean |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | function authenticate_message(lm_challenge_response, nt_challenge_response, domain, user, workstation, encrypted_random_session_key, flags) { |
| 450 | const payload = Buffer.concat([lm_challenge_response, nt_challenge_response, domain, user, workstation, encrypted_random_session_key]); |
| 451 | const offset = ((flags & NegotiateFlags.NtlmsspNegociateVersion) == 0) ? 80 : 88; |
| 452 | const buf = Buffer.alloc(offset - 16); |
| 453 | buf.write('4e544c4d53535000', 0, 8, 'hex'); // Signature |
| 454 | buf.writeInt32LE(3, 8); // MessageType |
| 455 | buf.writeInt16LE(lm_challenge_response.length, 12); // LmChallengeResponseLen |
| 456 | buf.writeInt16LE(lm_challenge_response.length, 14); // LmChallengeResponseMaxLen |
| 457 | buf.writeInt32LE(offset, 16); // LmChallengeResponseBufferOffset |
| 458 | buf.writeInt16LE(nt_challenge_response.length, 20); // NtChallengeResponseLen |
| 459 | buf.writeInt16LE(nt_challenge_response.length, 22); // NtChallengeResponseMaxLen |
| 460 | buf.writeInt32LE(offset + lm_challenge_response.length, 24); // NtChallengeResponseBufferOffset |
| 461 | buf.writeInt16LE(domain.length, 28); // DomainNameLen |
| 462 | buf.writeInt16LE(domain.length, 30); // DomainNameMaxLen |
| 463 | buf.writeInt32LE(offset + lm_challenge_response.length + nt_challenge_response.length, 32); // DomainNameBufferOffset |
| 464 | buf.writeInt16LE(user.length, 36); // UserNameLen |
| 465 | buf.writeInt16LE(user.length, 38); // UserNameMaxLen |
| 466 | buf.writeInt32LE(offset + lm_challenge_response.length + nt_challenge_response.length + domain.length, 40); // UserNameBufferOffset |
| 467 | buf.writeInt16LE(workstation.length, 44); // WorkstationLen |
| 468 | buf.writeInt16LE(workstation.length, 46); // WorkstationMaxLen |
| 469 | buf.writeInt32LE(offset + lm_challenge_response.length + nt_challenge_response.length + domain.length + user.length, 48); // WorkstationBufferOffset |
| 470 | buf.writeInt16LE(encrypted_random_session_key.length, 52); // EncryptedRandomSessionLen |
| 471 | buf.writeInt16LE(encrypted_random_session_key.length, 54); // EncryptedRandomSessionMaxLen |
| 472 | buf.writeInt32LE(offset + lm_challenge_response.length + nt_challenge_response.length + domain.length + user.length + workstation.length, 56); // EncryptedRandomSessionBufferOffset |
| 473 | buf.writeInt32LE(flags, 60); // NegotiateFlags |
| 474 | if ((flags & NegotiateFlags.NtlmsspNegociateVersion) != 0) { |
| 475 | buf.writeUInt8(MajorVersion.WindowsMajorVersion6, 64); // ProductMajorVersion |
| 476 | buf.writeUInt8(MinorVersion.WindowsMinorVersion0, 65); // ProductMinorVersion |
| 477 | buf.writeInt16LE(6002, 66); // ProductBuild |
| 478 | //buf.writeInt16LE(0, 68); // Reserved |
| 479 | //buf.writeUInt8(0, 70); // Reserved |
| 480 | buf.writeUInt8(NTLMRevision.NtlmSspRevisionW2K3, 71); // NTLMRevisionCurrent |
| 481 | } |
| 482 | return [buf, payload]; |
| 483 | } |
| 484 | |
| 485 | function create_ts_authinfo(auth_info) { |
| 486 | asn1obj = |
| 487 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 488 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 489 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, asn1.integerToDer(2)), |
| 490 | ]), |
| 491 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ |
| 492 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, auth_info.toString('binary')) |
| 493 | ]) |
| 494 | ]); |
| 495 | return Buffer.from(asn1.toDer(asn1obj).data, 'binary'); |
| 496 | } |
| 497 | |
| 498 | function create_ts_credentials(domain, user, password) { |
| 499 | var asn1obj = |
| 500 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 501 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 502 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, domain.toString('binary')) |
| 503 | ]), |
| 504 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| 505 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, user.toString('binary')) |
| 506 | ]), |
| 507 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ |
| 508 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, password.toString('binary')) |
| 509 | ]) |
| 510 | ]); |
| 511 | const ts_password_cred_encoded = asn1.toDer(asn1obj).data; |
| 512 | asn1obj = |
| 513 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 514 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 515 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, asn1.integerToDer(1)), |
| 516 | ]), |
| 517 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| 518 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, ts_password_cred_encoded) |
| 519 | ]) |
| 520 | ]); |
| 521 | return Buffer.from(asn1.toDer(asn1obj).data, 'binary'); |
| 522 | } |
| 523 | |
| 524 | function create_ts_authenticate(nego, pub_key_auth) { |
| 525 | // Create create_ts_request |
| 526 | const asn1obj = |
| 527 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 528 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 529 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, asn1.integerToDer(2)), |
| 530 | ]), |
| 531 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| 532 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 533 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 534 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 535 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, nego.toString('binary')) |
| 536 | ]) |
| 537 | ]) |
| 538 | ]) |
| 539 | ]), |
| 540 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 3, true, [ |
| 541 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pub_key_auth.toString('binary')) |
| 542 | ]), |
| 543 | ]); |
| 544 | |
| 545 | // Serialize an ASN.1 object to DER format |
| 546 | return Buffer.from(asn1.toDer(asn1obj).data, 'binary'); |
| 547 | } |
| 548 | |
| 549 | function read_challenge_message(ntlm, derBuffer) { |
| 550 | |
| 551 | //console.log('ntlm.negotiate_message', ntlm.negotiate_message.toString('hex')); |
| 552 | //ntlm.negotiate_message = Buffer.from('4e544c4d53535000010000003582086000000000000000000000000000000000', 'hex'); |
| 553 | |
| 554 | // ******** |
| 555 | //ntlm.exported_session_key = Buffer.from('9a1ed052e932834a311daf90c2750219', 'hex'); // ************************* |
| 556 | //derBuffer = Buffer.from('4e544c4d53535000020000000e000e003800000035828a6259312ef59a4517dd000000000000000058005800460000000a00614a0000000f430045004e005400520041004c0002000e00430045004e005400520041004c0001000e00430045004e005400520041004c0004000e00430065006e007400720061006c0003000e00430065006e007400720061006c00070008007b7b3bee9e5ad80100000000', 'hex'); |
| 557 | |
| 558 | //console.log("YST: read_challenge_message1: ", derBuffer.toString('hex')); |
| 559 | |
| 560 | const headerSignature = derBuffer.slice(0, 8); |
| 561 | if (headerSignature.toString('hex') != '4e544c4d53535000') { console.log('BAD SIGNATURE'); } |
| 562 | const messageType = derBuffer.readInt32LE(8); |
| 563 | if (messageType != 2) { console.log('BAD MESSAGE TYPE'); } |
| 564 | const targetNameLen = derBuffer.readInt16LE(12); |
| 565 | const targetNameLenMax = derBuffer.readInt16LE(14); |
| 566 | const targetNameBufferOffset = derBuffer.readInt32LE(16); |
| 567 | const negotiateFlags = derBuffer.readInt32LE(20); |
| 568 | const serverChallenge = derBuffer.slice(24, 32); |
| 569 | const reserved = derBuffer.slice(32, 40); |
| 570 | if (reserved.toString('hex') != '0000000000000000') { console.log('BAD RESERVED'); } |
| 571 | const targetInfoLen = derBuffer.readInt16LE(40); |
| 572 | const targetInfoLenMax = derBuffer.readInt16LE(42); |
| 573 | const targetInfoBufferOffset = derBuffer.readInt32LE(44); |
| 574 | const targetName = derBuffer.slice(targetNameBufferOffset, targetNameBufferOffset + targetNameLen); |
| 575 | const targetInfoBuf = derBuffer.slice(targetInfoBufferOffset, targetInfoBufferOffset + targetInfoLen); |
| 576 | const targetInfo = decodeTargetInfo(derBuffer.slice(targetInfoBufferOffset, targetInfoBufferOffset + targetInfoLen)); |
| 577 | const timestamp = targetInfo[7]; |
| 578 | //const timestamp = Buffer.from('7b7b3bee9e5ad801', 'hex'); // ************** |
| 579 | if (timestamp == null) { console.log('NO TIMESTAMP'); } |
| 580 | const clientChallenge = crypto.randomBytes(8); |
| 581 | //const clientChallenge = Buffer.from('10aac9679ef64e66', 'hex'); // ***************************** |
| 582 | const response_key_nt = ntowfv2(ntlm.password, ntlm.user, ntlm.domain); // Password, Username, Domain |
| 583 | const response_key_lm = lmowfv2(ntlm.password, ntlm.user, ntlm.domain); // Password, Username, Domain |
| 584 | |
| 585 | //console.log("YST: target_name:", targetInfoBuf.toString('hex')); |
| 586 | //console.log("YST: timestamp:", timestamp.toString('hex')); |
| 587 | //console.log('YST: client_challenge:', clientChallenge.toString('hex')); |
| 588 | //console.log("YST: response_key_nt:", response_key_nt.toString('hex')); |
| 589 | //console.log("YST: response_key_lm:", response_key_lm.toString('hex')); |
| 590 | |
| 591 | var resp = compute_response_v2(response_key_nt, response_key_lm, serverChallenge, clientChallenge, timestamp, targetInfoBuf); |
| 592 | const nt_challenge_response = resp[0]; |
| 593 | const lm_challenge_response = resp[1]; |
| 594 | const session_base_key = resp[2]; |
| 595 | |
| 596 | //console.log('YST: nt_challenge_response:', nt_challenge_response.toString('hex')); |
| 597 | //console.log('YST: lm_challenge_response:', lm_challenge_response.toString('hex')); |
| 598 | //console.log("YST: session_base_key:", session_base_key.toString('hex')); |
| 599 | |
| 600 | const key_exchange_key = kx_key_v2(session_base_key, lm_challenge_response, serverChallenge); |
| 601 | const encrypted_random_session_key = rc4k(key_exchange_key, ntlm.exported_session_key); |
| 602 | |
| 603 | //console.log("YST: key_exchange_key:", key_exchange_key.toString('hex')); |
| 604 | //console.log("YST: self.exported_session_key:", ntlm.exported_session_key.toString('hex')); |
| 605 | //console.log("YST: encrypted_random_session_key:", encrypted_random_session_key.toString('hex')); |
| 606 | |
| 607 | ntlm.is_unicode = ((negotiateFlags & 1) != 0) |
| 608 | //console.log("YST: self.is_unicode: {}", ntlm.is_unicode); |
| 609 | var xdomain = null; |
| 610 | var xuser = null; |
| 611 | if (ntlm.is_unicode) { |
| 612 | xdomain = toUnicode(ntlm.domain); |
| 613 | xuser = toUnicode(ntlm.user); |
| 614 | } else { |
| 615 | xdomain = Buffer.from(ntlm.domain, 'utf8'); |
| 616 | xuser = Buffer.from(ntlm.user, 'utf8'); |
| 617 | } |
| 618 | |
| 619 | //console.log("YST: domain:", xdomain.toString('hex')); |
| 620 | //console.log("YST: user:", xuser.toString('hex')); |
| 621 | |
| 622 | const auth_message_compute = authenticate_message(lm_challenge_response, nt_challenge_response, xdomain, xuser, zeroBuffer(0), encrypted_random_session_key, negotiateFlags); |
| 623 | |
| 624 | // Write a tmp message to compute MIC and then include it into final message |
| 625 | const tmp_final_auth_message = Buffer.concat([auth_message_compute[0], zeroBuffer(16), auth_message_compute[1]]); |
| 626 | |
| 627 | //console.log("YST: tmp_final_auth_message: {}", tmp_final_auth_message.toString('hex')); |
| 628 | |
| 629 | const signature = mic(ntlm.exported_session_key, ntlm.negotiate_message, derBuffer, tmp_final_auth_message); |
| 630 | |
| 631 | //console.log("YST: signature: {}", signature.toString('hex')); |
| 632 | |
| 633 | const r = Buffer.concat([auth_message_compute[0], signature, auth_message_compute[1]]); |
| 634 | |
| 635 | //console.log("YST: read_challenge_message2: {}", r.toString('hex')); |
| 636 | |
| 637 | return r; |
| 638 | } |
| 639 | |
| 640 | function unitTest() { |
| 641 | console.log('--- Starting RDP NLA Unit Tests'); |
| 642 | |
| 643 | // Test format of the first client message |
| 644 | var r = create_negotiate_message(); |
| 645 | console.log(compareArray(bufToArr(r), [78, 84, 76, 77, 83, 83, 80, 0, 1, 0, 0, 0, 53, 130, 8, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) ? "negotiate_message passed." : "negotiate_message failed."); |
| 646 | |
| 647 | // Test of MD4 hash function |
| 648 | r = md4(Buffer.from("foo")); |
| 649 | console.log(compareArray(bufToArr(r), [0x0a, 0xc6, 0x70, 0x0c, 0x49, 0x1d, 0x70, 0xfb, 0x86, 0x50, 0x94, 0x0b, 0x1c, 0xa1, 0xe4, 0xb2]) ? "RC4 passed." : "RC4 failed."); |
| 650 | |
| 651 | // Test of the unicode function |
| 652 | r = toUnicode("foo"); |
| 653 | console.log(compareArray(bufToArr(r), [0x66, 0x00, 0x6f, 0x00, 0x6f, 0x00]) ? "Unicode passed." : "Unicode failed."); |
| 654 | |
| 655 | // Test HMAC_MD5 function |
| 656 | r = hmac_md5(Buffer.from("foo"), Buffer.from("bar")); |
| 657 | console.log(compareArray(bufToArr(r), [0x0c, 0x7a, 0x25, 0x02, 0x81, 0x31, 0x5a, 0xb8, 0x63, 0x54, 0x9f, 0x66, 0xcd, 0x8a, 0x3a, 0x53]) ? "HMAC_MD5 passed." : "HMAC_MD5 failed."); |
| 658 | |
| 659 | // Test NTOWFv2 function |
| 660 | r = ntowfv2("foo", "user", "domain"); |
| 661 | console.log(compareArray(bufToArr(r), [0x6e, 0x53, 0xb9, 0x0, 0x97, 0x8c, 0x87, 0x1f, 0x91, 0xde, 0x6, 0x44, 0x9d, 0x8b, 0x8b, 0x81]) ? "NTOWFv2 passed." : "NTOWFv2 failed."); |
| 662 | |
| 663 | // Test LMOWFv2 function |
| 664 | r = ntowfv2("foo", "user", "domain"); |
| 665 | console.log(compareArray(bufToArr(r), ntowfv2("foo", "user", "domain")) ? "LMOWFv2 passed." : "LMOWFv2 failed."); |
| 666 | |
| 667 | // Test compute response v2 function |
| 668 | r = compute_response_v2(Buffer.from("a"), Buffer.from("b"), Buffer.from("c"), Buffer.from("d"), Buffer.from("e"), Buffer.from("f")); |
| 669 | console.log(compareArray(bufToArr(r[0]), [0xb4, 0x23, 0x84, 0xf, 0x6e, 0x83, 0xc1, 0x5a, 0x45, 0x4f, 0x4c, 0x92, 0x7a, 0xf2, 0xc3, 0x3e, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x64, 0x0, 0x0, 0x0, 0x0, 0x66]) ? "responsev2 1 passed." : "responsev2 1 failed."); |
| 670 | console.log(compareArray(bufToArr(r[1]), [0x56, 0xba, 0xff, 0x2d, 0x98, 0xbe, 0xcd, 0xa5, 0x6d, 0xe6, 0x17, 0x89, 0xe1, 0xed, 0xca, 0xae, 0x64]) ? "responsev2 2 passed." : "responsev2 2 failed."); |
| 671 | console.log(compareArray(bufToArr(r[2]), [0x40, 0x3b, 0x33, 0xe5, 0x24, 0x34, 0x3c, 0xc3, 0x24, 0xa0, 0x4d, 0x77, 0x75, 0x34, 0xa4, 0xd0]) ? "responsev2 3 passed." : "responsev2 3 failed."); |
| 672 | |
| 673 | // Test of rc4k function |
| 674 | r = rc4k(Buffer.from("foo"), Buffer.from("bar")); |
| 675 | console.log(compareArray(bufToArr(r), [201, 67, 159]) ? "rc4k passed." : "rc4k failed."); |
| 676 | |
| 677 | // Test of sign_key function |
| 678 | r = sign_key(Buffer.from("foo"), true); |
| 679 | console.log(compareArray(bufToArr(r), [253, 238, 149, 155, 221, 78, 43, 179, 82, 61, 111, 132, 168, 68, 222, 15]) ? "sign_key 1 passed." : "sign_key 1 failed."); |
| 680 | r = sign_key(Buffer.from("foo"), false); |
| 681 | console.log(compareArray(bufToArr(r), [90, 201, 12, 225, 140, 156, 151, 61, 156, 56, 31, 254, 10, 223, 252, 74]) ? "sign_key 2 passed." : "sign_key 2 failed."); |
| 682 | |
| 683 | // Test of seal_key function |
| 684 | r = seal_key(Buffer.from("foo"), true); |
| 685 | console.log(compareArray(bufToArr(r), [20, 213, 185, 176, 168, 142, 134, 244, 36, 249, 89, 247, 180, 36, 162, 101]) ? "seal_key 1 passed." : "seal_key 1 failed."); |
| 686 | r = seal_key(Buffer.from("foo"), false); |
| 687 | console.log(compareArray(bufToArr(r), [64, 125, 160, 17, 144, 165, 62, 226, 22, 125, 128, 31, 103, 141, 55, 40]) ? "seal_key 2 passed." : "seal_key 2 failed."); |
| 688 | |
| 689 | // Test signature function |
| 690 | var rc4 = createRC4(Buffer.from("foo")); |
| 691 | r = mac(rc4, Buffer.from("bar"), 0, Buffer.from("data")); |
| 692 | console.log(compareArray(bufToArr(r), [1, 0, 0, 0, 77, 211, 144, 84, 51, 242, 202, 176, 0, 0, 0, 0]) ? "Signature passed." : "Signature failed."); |
| 693 | |
| 694 | // Test challenge message |
| 695 | r = authenticate_message(Buffer.from("foo"), Buffer.from("foo"), Buffer.from("domain"), Buffer.from("user"), Buffer.from("workstation"), Buffer.from("foo"), 0); |
| 696 | var buf = Buffer.concat([r[0], Buffer.alloc(16), r[1]]); |
| 697 | console.log(compareArray(bufToArr(buf), [78, 84, 76, 77, 83, 83, 80, 0, 3, 0, 0, 0, 3, 0, 3, 0, 80, 0, 0, 0, 3, 0, 3, 0, 83, 0, 0, 0, 6, 0, 6, 0, 86, 0, 0, 0, 4, 0, 4, 0, 92, 0, 0, 0, 11, 0, 11, 0, 96, 0, 0, 0, 3, 0, 3, 0, 107, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 102, 111, 111, 102, 111, 111, 100, 111, 109, 97, 105, 110, 117, 115, 101, 114, 119, 111, 114, 107, 115, 116, 97, 116, 105, 111, 110, 102, 111, 111]) ? "Challenge message passed." : "Challenge message failed."); |
| 698 | |
| 699 | // Test RC4 |
| 700 | rc4 = createRC4(Buffer.from("foo")); |
| 701 | r = rc4.update(Buffer.from("bar")); |
| 702 | console.log(compareArray(bufToArr(r), [201, 67, 159]) ? "RC4 1 passed." : "RC4 1 failed."); |
| 703 | r = rc4.update(Buffer.from("bar")); |
| 704 | console.log(compareArray(bufToArr(r), [75, 169, 19]) ? "RC4 2 passed." : "RC4 2 failed."); |
| 705 | |
| 706 | // Test create_ts_authenticate |
| 707 | r = create_ts_authenticate(Buffer.from("000102", 'hex'), Buffer.from("000102", 'hex')); |
| 708 | console.log(compareArray(bufToArr(r), [48, 25, 160, 3, 2, 1, 2, 161, 11, 48, 9, 48, 7, 160, 5, 4, 3, 0, 1, 2, 163, 5, 4, 3, 0, 1, 2]) ? "create_ts_authenticate passed." : "create_ts_authenticate failed."); |
| 709 | |
| 710 | // Test test_create_ts_credentials |
| 711 | r = create_ts_credentials(Buffer.from("domain"), Buffer.from("user"), Buffer.from("password")); |
| 712 | console.log(compareArray(bufToArr(r), [48, 41, 160, 3, 2, 1, 1, 161, 34, 4, 32, 48, 30, 160, 8, 4, 6, 100, 111, 109, 97, 105, 110, 161, 6, 4, 4, 117, 115, 101, 114, 162, 10, 4, 8, 112, 97, 115, 115, 119, 111, 114, 100]) ? "test_create_ts_credentials passed." : "test_create_ts_credentials failed."); |
| 713 | |
| 714 | // Test create_ts_authinfo |
| 715 | r = create_ts_authinfo(Buffer.from("foo")); |
| 716 | console.log(compareArray(bufToArr(r), [48, 12, 160, 3, 2, 1, 2, 162, 5, 4, 3, 102, 111, 111]) ? "create_ts_authinfo passed." : "create_ts_authinfo failed."); |
| 717 | |
| 718 | console.log('--- RDP NLA Unit Tests Completed'); |
| 719 | } |