Added support for generating DPAPI commands for mimikatz

This commit is contained in:
Michael Grafnetter 2018-07-13 21:50:59 +02:00
parent e9c03462e7
commit 7453ac8457
5 changed files with 225 additions and 30 deletions

View File

@ -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);
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}