Merge 7cb4da2f9ffbc1349e0736d1f61acb581c0257ae into 4290ea753912378913b1f3f54e0fc5720afeda5d
This commit is contained in:
commit
b8c09ec9f7
@ -165,6 +165,7 @@
|
||||
"Derive PBKDF2 key",
|
||||
"Derive EVP key",
|
||||
"Derive HKDF key",
|
||||
"Generate NTLMv2 SMB Session Key",
|
||||
"Bcrypt",
|
||||
"Scrypt",
|
||||
"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