mirror of
https://github.com/MichaelGrafnetter/DSInternals
synced 2025-01-21 06:20:46 +00:00
Added support for generating DPAPI commands for mimikatz
This commit is contained in:
parent
e9c03462e7
commit
7453ac8457
@ -46,7 +46,7 @@ D960C85C25BC6433FAE2E0E04ADC96820350FD88B92922F40F3F2855FE2573746A9CE2D3E9B9F7BB
|
||||
D696BC1BBA1400000029B473EA61602F51CDCCB15C5982D3F6F83D09EB".Replace(Environment.NewLine, String.Empty).HexToBinary();
|
||||
|
||||
var roamedObject = new RoamedCredential(blob, TestUser, TestSID);
|
||||
Assert.AreEqual(@"Administrator\Crypto\RSA\S-1-5-21-4534338-1127018997-2609994386-500\701577141985b6923998dcca035c007a_f8b7bbef-d227-4ac7-badd-3a238a7f741e", roamedObject.FileName);
|
||||
Assert.AreEqual(@"Administrator\Crypto\RSA\S-1-5-21-4534338-1127018997-2609994386-500\701577141985b6923998dcca035c007a_f8b7bbef-d227-4ac7-badd-3a238a7f741e", roamedObject.FilePath);
|
||||
Assert.AreEqual(RoamedCredentialType.RSAPrivateKey, roamedObject.Type);
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ BAA60416FC595319FB786F77167679F01908519F2BE75A0EF062C90ACF56C117AA3B3416B7FBE60B
|
||||
F1C522630C6A625070E8F81671A7A4BBB8D1FFBA4DA094B48C64050810306BB3FB538069FB87DCFBF00501B9D0A99DDF6C93CC0774660C97564E".Replace(Environment.NewLine, String.Empty).HexToBinary();
|
||||
|
||||
var roamedObject = new RoamedCredential(blob, TestUser, TestSID);
|
||||
Assert.AreEqual(@"Administrator\SystemCertificates\My\Certificates\3B83BFA7037F6A79B3F3D17D229E1BC097F35B51", roamedObject.FileName);
|
||||
Assert.AreEqual(@"Administrator\SystemCertificates\My\Certificates\3B83BFA7037F6A79B3F3D17D229E1BC097F35B51", roamedObject.FilePath);
|
||||
Assert.AreEqual(RoamedCredentialType.CNGCertificate, roamedObject.Type);
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ DCD2F6E392B6867C0836B85F64D95BC5F506D213070CD973417A049A775C5907E903CE595603AFDE
|
||||
248345A9FA22CA076AFF971F55829D1426D3667084194C1318B34587E014C5DF".Replace(Environment.NewLine, String.Empty).HexToBinary();
|
||||
|
||||
var roamedObject = new RoamedCredential(blob, TestUser, TestSID);
|
||||
Assert.AreEqual(@"Administrator\Protect\S-1-5-21-4534338-1127018997-2609994386-500\7fc19508-7b85-4a7c-9e5d-15f9e00e7ce5", roamedObject.FileName);
|
||||
Assert.AreEqual(@"Administrator\Protect\S-1-5-21-4534338-1127018997-2609994386-500\7fc19508-7b85-4a7c-9e5d-15f9e00e7ce5", roamedObject.FilePath);
|
||||
Assert.AreEqual(RoamedCredentialType.DPAPIMasterKey, roamedObject.Type);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,8 @@
|
||||
private const string RSAP12FileNameFormat = "ntds_capi_{0}.pfx";
|
||||
private const string LegacyKeyFileNameFormat = "ntds_legacy_{0}.key";
|
||||
private const string UnknownKeyFileNameFormat = "ntds_unknown_{0}_{1}.key";
|
||||
private const string DummyNamingContext = "DC=unknown,DC=int";
|
||||
private const string KiwiCommandFormat = "REM Add this parameter to at least the first dpapi::masterkey command: /pvk:\"{0}\"";
|
||||
private const int PVKHeaderSize = 6 * sizeof(int);
|
||||
private const uint PVKHeaderMagic = 0xb0b5f11e;
|
||||
private const uint PVKHeaderVersion = 0;
|
||||
@ -69,6 +71,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Create unit tests for DPAPIBackupKey
|
||||
public DPAPIBackupKey(Guid id, byte[] blob)
|
||||
{
|
||||
// Validate the input
|
||||
Validator.AssertNotNull(blob, "blob");
|
||||
Validator.AssertMinLength(blob, KeyVersionSize, "blob");
|
||||
|
||||
// Process the parameters
|
||||
this.KeyId = id;
|
||||
this.Data = blob;
|
||||
this.Type = (DPAPIBackupKeyType)BitConverter.ToInt32(this.Data, KeyVersionOffset);
|
||||
|
||||
// Generate some dummy distinguished name
|
||||
this.DistinguishedName = String.Format(BackupKeyDNFormat, this.KeyId, DummyNamingContext);
|
||||
}
|
||||
|
||||
public override string FilePath
|
||||
{
|
||||
get
|
||||
{
|
||||
switch(this.Type)
|
||||
{
|
||||
case DPAPIBackupKeyType.RSAKey:
|
||||
// .pvk file
|
||||
return String.Format(RSAKeyFileNameFormat, this.KeyId);
|
||||
case DPAPIBackupKeyType.LegacyKey:
|
||||
// .key file
|
||||
return String.Format(LegacyKeyFileNameFormat, this.KeyId);
|
||||
case DPAPIBackupKeyType.Unknown:
|
||||
// Generate an additional random ID to prevent potential filename conflicts
|
||||
int rnd = new Random().Next();
|
||||
return String.Format(UnknownKeyFileNameFormat, this.KeyId, rnd);
|
||||
default:
|
||||
// Saving pointers or other domain key types to files is not supported.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string KiwiCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Type == DPAPIBackupKeyType.RSAKey ? String.Format(KiwiCommandFormat, this.FilePath) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public DPAPIBackupKeyType Type
|
||||
{
|
||||
get;
|
||||
@ -86,7 +135,7 @@
|
||||
private set;
|
||||
}
|
||||
|
||||
public override void SaveTo(string directoryPath)
|
||||
public override void Save(string directoryPath)
|
||||
{
|
||||
// The target directory must exist
|
||||
Validator.AssertDirectoryExists(directoryPath);
|
||||
@ -104,8 +153,7 @@
|
||||
byte[] certificate = this.Data.Cut(RSAPrivateKeyOffset + privateKeySize, certificateSize);
|
||||
|
||||
// Create PVK file
|
||||
var pvkFile = String.Format(RSAKeyFileNameFormat, this.KeyId);
|
||||
fullFilePath = Path.Combine(directoryPath, pvkFile);
|
||||
fullFilePath = Path.Combine(directoryPath, this.FilePath);
|
||||
byte[] pvk = EncapsulatePvk(privateKey);
|
||||
File.WriteAllBytes(fullFilePath, pvk);
|
||||
|
||||
@ -122,15 +170,11 @@
|
||||
break;
|
||||
case DPAPIBackupKeyType.LegacyKey:
|
||||
// We create one KEY file, while cropping out the key version.
|
||||
string keyFile = String.Format(LegacyKeyFileNameFormat, this.KeyId);
|
||||
fullFilePath = Path.Combine(directoryPath, keyFile);
|
||||
fullFilePath = Path.Combine(directoryPath, this.FilePath);
|
||||
File.WriteAllBytes(fullFilePath, this.Data.Cut(KeyVersionSize));
|
||||
break;
|
||||
case DPAPIBackupKeyType.Unknown:
|
||||
// Generate an additional random ID to prevent potential filename conflicts
|
||||
int rnd = new Random().Next();
|
||||
string unknownKeyFile = String.Format(UnknownKeyFileNameFormat, this.KeyId, rnd);
|
||||
fullFilePath = Path.Combine(directoryPath, unknownKeyFile);
|
||||
fullFilePath = Path.Combine(directoryPath, this.FilePath);
|
||||
File.WriteAllBytes(fullFilePath, this.Data);
|
||||
break;
|
||||
case DPAPIBackupKeyType.PreferredLegacyKeyPointer:
|
||||
@ -209,4 +253,4 @@
|
||||
return pvk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,84 @@
|
||||
namespace DSInternals.Common.Data
|
||||
{
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
public abstract class DPAPIObject
|
||||
{
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to the generated Mimikatz DPAPI batch processing script.
|
||||
/// </summary>
|
||||
public const string KiwiFilePath = "kiwiscript.txt";
|
||||
|
||||
/// <summary>
|
||||
/// DPAPI blob.
|
||||
/// </summary>
|
||||
public byte[] Data
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
public abstract void SaveTo(string directoryPath);
|
||||
/// <summary>
|
||||
/// Gets the relative path to which this blob will be saved.
|
||||
/// </summary>
|
||||
public abstract string FilePath
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Mimikatz command that would process the DPAPI blob.
|
||||
/// </summary>
|
||||
public abstract string KiwiCommand
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the DPAPI blob to an appropriate file in the current directory.
|
||||
/// </summary>
|
||||
public void Save()
|
||||
{
|
||||
this.SaveTo(Directory.GetCurrentDirectory());
|
||||
this.Save(Directory.GetCurrentDirectory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the DPAPI blob to an appropriate file in the specified directory.
|
||||
/// </summary>
|
||||
/// <param name="directoryPath">Directory to save the DPAPI blob to.</param>
|
||||
public abstract void Save(string directoryPath);
|
||||
|
||||
/// <summary>
|
||||
/// Appends the Mimikatz command to a text file in the current directory.
|
||||
/// </summary>
|
||||
public void SaveKiwiCommand()
|
||||
{
|
||||
this.SaveKiwiCommand(Directory.GetCurrentDirectory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the Mimikatz command to a text file in the specified directory.
|
||||
/// </summary>
|
||||
/// <param name="directoryPath">Directory to save the text file to.</param>
|
||||
public void SaveKiwiCommand(string directoryPath)
|
||||
{
|
||||
string command = this.KiwiCommand;
|
||||
|
||||
if(string.IsNullOrEmpty(command))
|
||||
{
|
||||
// Mimikatz probably does not support this DPAPI object type, so there is nothing to write to the script file
|
||||
return;
|
||||
}
|
||||
|
||||
// The target directory must exist
|
||||
Validator.AssertDirectoryExists(directoryPath);
|
||||
|
||||
var filePath = Path.Combine(directoryPath, KiwiFilePath);
|
||||
using (var writer = File.AppendText(filePath))
|
||||
{
|
||||
writer.WriteLine(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,11 @@
|
||||
|
||||
public class RoamedCredential : DPAPIObject
|
||||
{
|
||||
private const string MasterKeyCommandFormat = "dpapi::masterkey /in:\"{0}\" /sid:{1}";
|
||||
private const string CapiKeyCommandFormat = "dpapi::capi /in:\"{0}\"";
|
||||
private const string CNGKeyCommandFormat = "dpapi::cng /in:\"{0}\"";
|
||||
private const string CertificateCommandFormat = "crypto::system /file:\"{0}\" /export";
|
||||
private const string CurrentMasterKeyPointerId = "Preferred";
|
||||
private const int MinLength = 132;
|
||||
private const int IdentifierMaxSize = 93;
|
||||
private const int HashSize = 20;
|
||||
@ -119,12 +124,12 @@
|
||||
private set;
|
||||
}
|
||||
|
||||
public override void SaveTo(string directoryPath)
|
||||
public override void Save(string directoryPath)
|
||||
{
|
||||
// The target directory must exist
|
||||
Validator.AssertDirectoryExists(directoryPath);
|
||||
|
||||
string fullFilePath = Path.Combine(directoryPath, this.FileName);
|
||||
string fullFilePath = Path.Combine(directoryPath, this.FilePath);
|
||||
|
||||
// Some blobs need to be saved in a specific folder structure that we have to create first
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullFilePath));
|
||||
@ -136,7 +141,7 @@
|
||||
/// <summary>
|
||||
/// Gets the path to the credential file.
|
||||
/// </summary>
|
||||
public string FileName
|
||||
public override string FilePath
|
||||
{
|
||||
get
|
||||
{
|
||||
@ -180,9 +185,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
public override string KiwiCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
switch(this.Type)
|
||||
{
|
||||
case RoamedCredentialType.DPAPIMasterKey:
|
||||
return this.Id != CurrentMasterKeyPointerId ? String.Format(MasterKeyCommandFormat, this.FilePath, this.AccountSid) : null;
|
||||
case RoamedCredentialType.CNGCertificate:
|
||||
case RoamedCredentialType.CNGRequest:
|
||||
case RoamedCredentialType.CryptoApiCertificate:
|
||||
case RoamedCredentialType.CryptoApiRequest:
|
||||
return String.Format(CertificateCommandFormat, this.FilePath);
|
||||
case RoamedCredentialType.RSAPrivateKey:
|
||||
case RoamedCredentialType.DSAPrivateKey:
|
||||
return String.Format(CapiKeyCommandFormat, this.FilePath);
|
||||
case RoamedCredentialType.CNGPrivateKey:
|
||||
return String.Format(CNGKeyCommandFormat, this.FilePath);
|
||||
default:
|
||||
// Unknown/future credential type
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Format("{0}: {1}", this.Type, this.FileName);
|
||||
return String.Format("{0}: {1}", this.Type, this.FilePath);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
namespace DSInternals.PowerShell.Commands
|
||||
{
|
||||
using DSInternals.Common.Data;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Management.Automation;
|
||||
|
||||
[Cmdlet(VerbsData.Save, "DPAPIBlob")]
|
||||
@ -8,9 +10,14 @@
|
||||
[OutputType("None")]
|
||||
public class SaveDPAPIBlobCmdlet : PSCmdletEx
|
||||
{
|
||||
private const string VerboseMessageFormat = "Creating DPAPI file {0}.";
|
||||
private const string AccountParameterSet = "FromAccount";
|
||||
private const string ObjectParameterSet = "FromObject";
|
||||
|
||||
[Parameter(
|
||||
Mandatory = true,
|
||||
ValueFromPipeline = true
|
||||
ValueFromPipeline = true,
|
||||
ParameterSetName = ObjectParameterSet
|
||||
)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
[Alias("DPAPIBlob", "Object", "Blob", "BackupKey")]
|
||||
@ -19,25 +26,75 @@
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
[Parameter(
|
||||
Mandatory = true,
|
||||
ValueFromPipeline = true,
|
||||
ParameterSetName = AccountParameterSet
|
||||
)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public DSAccount Account
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Parameter(
|
||||
Mandatory = true,
|
||||
Position = 0
|
||||
)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
[Alias("Path")]
|
||||
[Alias("Path", "OutputPath")]
|
||||
public string DirectoryPath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private string AbsoluteDirectoryPath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
protected override void BeginProcessing()
|
||||
{
|
||||
this.AbsoluteDirectoryPath = this.ResolveSinglePath(this.DirectoryPath);
|
||||
}
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
string resolvedPath = this.ResolveSinglePath(this.DirectoryPath);
|
||||
// TODO: Exception handling
|
||||
// TODO: Verbose
|
||||
// TODO: WhatIf
|
||||
this.DPAPIObject.SaveTo(resolvedPath);
|
||||
switch(this.ParameterSetName)
|
||||
{
|
||||
case ObjectParameterSet:
|
||||
this.ProcessSingleObject(this.DPAPIObject);
|
||||
break;
|
||||
case AccountParameterSet:
|
||||
// Extract all roamed credentials from an account
|
||||
foreach(var blob in this.Account.RoamedCredentials ?? Enumerable.Empty<DPAPIObject>())
|
||||
{
|
||||
this.ProcessSingleObject(blob);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSingleObject(DPAPIObject blob)
|
||||
{
|
||||
string filePath = blob.FilePath;
|
||||
if (String.IsNullOrEmpty(filePath))
|
||||
{
|
||||
// There is nothing to save
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the blob
|
||||
string verboseMessage = String.Format(VerboseMessageFormat, filePath);
|
||||
this.WriteVerbose(verboseMessage);
|
||||
blob.Save(this.AbsoluteDirectoryPath);
|
||||
|
||||
// Append the Mimikatz command to a script file
|
||||
blob.SaveKiwiCommand(this.AbsoluteDirectoryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user