feat(Crypto): Add Generate NTLMv2 SMB Session Key operation
This commit is contained in:
parent
19c8a8a310
commit
7cb4da2f9f
@ -155,6 +155,7 @@
|
|||||||
"Derive PBKDF2 key",
|
"Derive PBKDF2 key",
|
||||||
"Derive EVP key",
|
"Derive EVP key",
|
||||||
"Derive HKDF key",
|
"Derive HKDF key",
|
||||||
|
"Generate NTLMv2 SMB Session Key",
|
||||||
"Bcrypt",
|
"Bcrypt",
|
||||||
"Scrypt",
|
"Scrypt",
|
||||||
"JWT Sign",
|
"JWT Sign",
|
||||||
|
|||||||
299
src/core/operations/GenerateNTLMv2SMBSessionKey.mjs
Normal file
299
src/core/operations/GenerateNTLMv2SMBSessionKey.mjs
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
/**
|
||||||
|
* @author mansiverma897993
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
import CryptoApi from "crypto-api/src/crypto-api.mjs";
|
||||||
|
import CryptoJS from "crypto-js";
|
||||||
|
import {runHash} from "../lib/Hash.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to a UTF-16LE binary string.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function toUtf16LE(str) {
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const code = str.charCodeAt(i);
|
||||||
|
out += String.fromCharCode(code & 0xff) + String.fromCharCode((code >> 8) & 0xff);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the NT Hash (MD4 of UTF-16LE representation) of a password.
|
||||||
|
*
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {string} Hex encoded NT Hash
|
||||||
|
*/
|
||||||
|
function calculateNTHash(password) {
|
||||||
|
const buf = new ArrayBuffer(password.length * 2);
|
||||||
|
const bufView = new Uint16Array(buf);
|
||||||
|
for (let i = 0; i < password.length; i++) {
|
||||||
|
bufView[i] = password.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return runHash("md4", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate NTLMv2 SMB Session Key operation
|
||||||
|
*/
|
||||||
|
class GenerateNTLMv2SMBSessionKey extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GenerateNTLMv2SMBSessionKey constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Generate NTLMv2 SMB Session Key";
|
||||||
|
this.module = "Crypto";
|
||||||
|
this.description = "Calculates the NTLMv2 ResponseKeyNT, SessionBaseKey (KeyExchangeKey) and decrypts the random session key/exported session key to generate the final SMB session key for Wireshark decryption.<br><br>Pass either a full NTLMv2 hash line (e.g. from responder/hashcat) as the input, or use the individual fields in the arguments.<br><br>If no Encrypted Session Key is provided, the SessionBaseKey itself will be returned as the final session key.";
|
||||||
|
this.infoURL = "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/c0250a97-2940-40c7-82fb-20d208c71e96";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
"name": "Username",
|
||||||
|
"type": "string",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Domain",
|
||||||
|
"type": "string",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Password / NT Hash",
|
||||||
|
"type": "toggleString",
|
||||||
|
"value": "",
|
||||||
|
"toggleValues": ["Hex (NT Hash)", "UTF8 (Plaintext)"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NTProofStr",
|
||||||
|
"type": "string",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Encrypted Session Key",
|
||||||
|
"type": "string",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Session ID",
|
||||||
|
"type": "string",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Output Format",
|
||||||
|
"type": "option",
|
||||||
|
"value": [
|
||||||
|
"Raw Session Key",
|
||||||
|
"Wireshark UAT line (SessionID,SessionKey)",
|
||||||
|
"TShark Command Line Option",
|
||||||
|
"JSON",
|
||||||
|
"Detailed Text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const cleanHex = (val) => {
|
||||||
|
if (!val) return "";
|
||||||
|
return val.trim().replace(/^0x/i, "").replace(/[\s:]/g, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
let username = "";
|
||||||
|
let domain = "";
|
||||||
|
let ntProofStr = "";
|
||||||
|
let encryptedSessionKey = "";
|
||||||
|
let sessionId = "";
|
||||||
|
|
||||||
|
// 1. Attempt to parse main input first
|
||||||
|
if (input && input.trim()) {
|
||||||
|
const lines = input.trim().split(/\r?\n/);
|
||||||
|
const firstLine = lines[0].trim();
|
||||||
|
if (firstLine.includes("::") && firstLine.split(":").length >= 5) {
|
||||||
|
const parts = firstLine.split(":");
|
||||||
|
username = parts[0];
|
||||||
|
domain = parts[2];
|
||||||
|
ntProofStr = parts[4];
|
||||||
|
} else {
|
||||||
|
for (const line of lines) {
|
||||||
|
const colonIdx = line.indexOf(":");
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
const key = line.substring(0, colonIdx).trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||||
|
const val = line.substring(colonIdx + 1).trim();
|
||||||
|
if (key === "username" || key === "user") username = val;
|
||||||
|
else if (key === "domain" || key === "workgroup") domain = val;
|
||||||
|
else if (key === "ntproofstr" || key === "ntproof") ntProofStr = val;
|
||||||
|
else if (key === "encryptedsessionkey" || key === "sessionkeyencrypted" || key === "encsessionkey") encryptedSessionKey = val;
|
||||||
|
else if (key === "sessionid" || key === "id") sessionId = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall back to UI args if not parsed from input
|
||||||
|
username = username || args[0];
|
||||||
|
domain = domain || args[1];
|
||||||
|
ntProofStr = cleanHex(ntProofStr || args[3]);
|
||||||
|
encryptedSessionKey = cleanHex(encryptedSessionKey || args[4]);
|
||||||
|
sessionId = cleanHex(sessionId || args[5]);
|
||||||
|
|
||||||
|
const passwordHashArg = args[2];
|
||||||
|
const passwordHashString = passwordHashArg.string.trim();
|
||||||
|
const passwordHashOption = passwordHashArg.option;
|
||||||
|
const outputFormat = args[6];
|
||||||
|
|
||||||
|
if (!passwordHashString) {
|
||||||
|
throw new OperationError("Password or NT Hash is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let ntHashHex = "";
|
||||||
|
if (passwordHashOption === "Hex (NT Hash)") {
|
||||||
|
const cleanNtHash = cleanHex(passwordHashString);
|
||||||
|
if (!/^[a-fA-F0-9]{32}$/.test(cleanNtHash)) {
|
||||||
|
throw new OperationError("NT Hash must be a 32-character hex string.");
|
||||||
|
}
|
||||||
|
ntHashHex = cleanNtHash;
|
||||||
|
} else {
|
||||||
|
ntHashHex = calculateNTHash(passwordHashString);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
throw new OperationError("Username is required.");
|
||||||
|
}
|
||||||
|
if (!ntProofStr) {
|
||||||
|
throw new OperationError("NTProofStr is required.");
|
||||||
|
}
|
||||||
|
if (!/^[a-fA-F0-9]{32}$/.test(ntProofStr)) {
|
||||||
|
throw new OperationError("NTProofStr must be a 32-character hex string.");
|
||||||
|
}
|
||||||
|
if (encryptedSessionKey && !/^[a-fA-F0-9]{32}$/.test(encryptedSessionKey)) {
|
||||||
|
throw new OperationError("Encrypted Session Key must be a 32-character hex string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanedSessionId = cleanHex(sessionId);
|
||||||
|
if (cleanedSessionId) {
|
||||||
|
if (!/^[a-fA-F0-9]+$/.test(cleanedSessionId)) {
|
||||||
|
throw new OperationError("Session ID must be a hex string.");
|
||||||
|
}
|
||||||
|
if (cleanedSessionId.length < 16) {
|
||||||
|
cleanedSessionId = cleanedSessionId.padStart(16, "0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Calculate ResponseKeyNT
|
||||||
|
// ResponseKeyNT = HMAC-MD5(unicode(upper(UserName) + Domain), NT Hash)
|
||||||
|
const ntHashBytes = Utils.convertToByteString(ntHashHex, "hex");
|
||||||
|
const userDomainBytes = toUtf16LE(username.toUpperCase() + domain);
|
||||||
|
const hasher1 = CryptoApi.getHasher("md5");
|
||||||
|
const mac1 = CryptoApi.getHmac(ntHashBytes, hasher1);
|
||||||
|
mac1.update(userDomainBytes);
|
||||||
|
const responseKeyNT = mac1.finalize();
|
||||||
|
const responseKeyNTHex = CryptoApi.encoder.toHex(responseKeyNT);
|
||||||
|
|
||||||
|
// Step 2: Calculate SessionBaseKey (also called KeyExchangeKey)
|
||||||
|
// SessionBaseKey = HMAC-MD5(ResponseKeyNT, NTProofStr)
|
||||||
|
const ntProofStrBytes = Utils.convertToByteString(ntProofStr, "hex");
|
||||||
|
const hasher2 = CryptoApi.getHasher("md5");
|
||||||
|
const mac2 = CryptoApi.getHmac(responseKeyNT, hasher2);
|
||||||
|
mac2.update(ntProofStrBytes);
|
||||||
|
const sessionBaseKey = mac2.finalize();
|
||||||
|
const sessionBaseKeyHex = CryptoApi.encoder.toHex(sessionBaseKey);
|
||||||
|
|
||||||
|
// Step 3: Decrypt Encrypted Session Key via RC4 (if provided)
|
||||||
|
let decryptedSessionKey = "";
|
||||||
|
if (encryptedSessionKey) {
|
||||||
|
const ciphertextWA = CryptoJS.enc.Hex.parse(encryptedSessionKey);
|
||||||
|
const keyWA = CryptoJS.enc.Hex.parse(sessionBaseKeyHex);
|
||||||
|
const decrypted = CryptoJS.RC4.encrypt(ciphertextWA, keyWA);
|
||||||
|
decryptedSessionKey = decrypted.ciphertext.toString(CryptoJS.enc.Hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalKey = encryptedSessionKey ? decryptedSessionKey : sessionBaseKeyHex;
|
||||||
|
|
||||||
|
// Output formatting
|
||||||
|
switch (outputFormat) {
|
||||||
|
case "Raw Session Key":
|
||||||
|
return finalKey;
|
||||||
|
case "Wireshark UAT line (SessionID,SessionKey)":
|
||||||
|
return `${cleanedSessionId || "0000000000000000"},${finalKey}`;
|
||||||
|
case "TShark Command Line Option":
|
||||||
|
return `-o "uat:smb2_seskey_list:${cleanedSessionId || "0000000000000000"},${finalKey},\\"\\",\\"\\""`;
|
||||||
|
case "JSON":
|
||||||
|
return JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
domain: domain,
|
||||||
|
ntHash: ntHashHex,
|
||||||
|
ntProofStr: ntProofStr,
|
||||||
|
responseKeyNT: responseKeyNTHex,
|
||||||
|
sessionBaseKey: sessionBaseKeyHex,
|
||||||
|
encryptedSessionKey: encryptedSessionKey || null,
|
||||||
|
decryptedSessionKey: decryptedSessionKey || null,
|
||||||
|
finalSessionKey: finalKey,
|
||||||
|
sessionId: cleanedSessionId || null,
|
||||||
|
wiresharkUat: `${cleanedSessionId || "0000000000000000"},${finalKey}`
|
||||||
|
}, null, 4);
|
||||||
|
case "Detailed Text": {
|
||||||
|
let out = `NTLMv2 / SMB Session Key Derivation Details:
|
||||||
|
---------------------------------------------
|
||||||
|
Username: ${username}
|
||||||
|
Domain: ${domain}
|
||||||
|
NT Hash: ${ntHashHex}
|
||||||
|
NTProofStr: ${ntProofStr}
|
||||||
|
|
||||||
|
ResponseKeyNT: ${responseKeyNTHex}
|
||||||
|
(HMAC-MD5 of Username + Domain using NT Hash as key)
|
||||||
|
|
||||||
|
SessionBaseKey: ${sessionBaseKeyHex}
|
||||||
|
(HMAC-MD5 of NTProofStr using ResponseKeyNT as key)
|
||||||
|
`;
|
||||||
|
if (encryptedSessionKey) {
|
||||||
|
out += `
|
||||||
|
Encrypted SessionKey: ${encryptedSessionKey}
|
||||||
|
Decrypted SessionKey: ${decryptedSessionKey}
|
||||||
|
(RC4 decrypted Encrypted SessionKey using SessionBaseKey as key)
|
||||||
|
|
||||||
|
Final Session Key: ${finalKey}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
out += `
|
||||||
|
Final Session Key: ${finalKey}
|
||||||
|
(Using SessionBaseKey directly since no Encrypted SessionKey was provided)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (cleanedSessionId) {
|
||||||
|
out += `
|
||||||
|
Session ID: ${cleanedSessionId}
|
||||||
|
|
||||||
|
Wireshark SMB2 Preferences entry:
|
||||||
|
Session ID: ${cleanedSessionId}
|
||||||
|
Session Key: ${finalKey}
|
||||||
|
|
||||||
|
TShark Option:
|
||||||
|
-o "uat:smb2_seskey_list:${cleanedSessionId},${finalKey},\\"\\",\\"\\""
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new OperationError(`Unknown output format: ${outputFormat}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenerateNTLMv2SMBSessionKey;
|
||||||
88
tests/operations/tests/GenerateNTLMv2SMBSessionKey.mjs
Normal file
88
tests/operations/tests/GenerateNTLMv2SMBSessionKey.mjs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Generate NTLMv2 SMB Session Key operation tests
|
||||||
|
*
|
||||||
|
* @author mansiverma897993
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TestRegister from "../../lib/TestRegister.mjs";
|
||||||
|
|
||||||
|
TestRegister.addTests([
|
||||||
|
{
|
||||||
|
name: "Generate NTLMv2 SMB Session Key: Manual fields with Plaintext Password (mrealman)",
|
||||||
|
input: "",
|
||||||
|
expectedOutput: "20a642c086ef74eee26277bf1d0cff8c",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Generate NTLMv2 SMB Session Key",
|
||||||
|
args: [
|
||||||
|
"mrealman",
|
||||||
|
"WORKGROUP",
|
||||||
|
{ string: "Blockbuster1", option: "UTF8 (Plaintext)" },
|
||||||
|
"16e816dead16d4ca7d5d6dee4a015c14",
|
||||||
|
"fde53b54cb676b9bbf0fb1fbef384698",
|
||||||
|
"",
|
||||||
|
"Raw Session Key"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Generate NTLMv2 SMB Session Key: Manual fields with NT Hash (eshellstrop)",
|
||||||
|
input: "",
|
||||||
|
expectedOutput: "facfbdf010d00aa2574c7c41201099e8",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Generate NTLMv2 SMB Session Key",
|
||||||
|
args: [
|
||||||
|
"eshellstrop",
|
||||||
|
"WORKGROUP",
|
||||||
|
{ string: "3f29138a04aadc19214e9c04028bf381", option: "Hex (NT Hash)" },
|
||||||
|
"0ca6227a4f00b9654a48908c4801a0ac",
|
||||||
|
"c24f5102a22d286336aac2dfa4dc2e04",
|
||||||
|
"",
|
||||||
|
"Raw Session Key"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Generate NTLMv2 SMB Session Key: Input NTLMv2 Hashcat Line (eshellstrop)",
|
||||||
|
input: "eshellstrop::WORKGROUP:1122334455667788:0ca6227a4f00b9654a48908c4801a0ac:0101000000000000",
|
||||||
|
expectedOutput: "facfbdf010d00aa2574c7c41201099e8",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Generate NTLMv2 SMB Session Key",
|
||||||
|
args: [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
{ string: "3f29138a04aadc19214e9c04028bf381", option: "Hex (NT Hash)" },
|
||||||
|
"",
|
||||||
|
"c24f5102a22d286336aac2dfa4dc2e04",
|
||||||
|
"",
|
||||||
|
"Raw Session Key"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Generate NTLMv2 SMB Session Key: Wireshark UAT line output with Session ID (mrealman)",
|
||||||
|
input: "",
|
||||||
|
expectedOutput: "0000100000000041,20a642c086ef74eee26277bf1d0cff8c",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Generate NTLMv2 SMB Session Key",
|
||||||
|
args: [
|
||||||
|
"mrealman",
|
||||||
|
"WORKGROUP",
|
||||||
|
{ string: "Blockbuster1", option: "UTF8 (Plaintext)" },
|
||||||
|
"16e816dead16d4ca7d5d6dee4a015c14",
|
||||||
|
"fde53b54cb676b9bbf0fb1fbef384698",
|
||||||
|
"0x0000100000000041",
|
||||||
|
"Wireshark UAT line (SessionID,SessionKey)"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
Loading…
x
Reference in New Issue
Block a user