Last active
October 11, 2024 18:16
-
-
Save xpn/23dc5b6c260a7571763ca8ca745c32f4 to your computer and use it in GitHub Desktop.
Quick POC looking at how encryption works for LAPS (v2)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.DirectoryServices.Protocols; | |
using System.Globalization; | |
using System.Linq; | |
using System.Runtime.InteropServices; | |
using System.Runtime.InteropServices.ComTypes; | |
using System.Security.Policy; | |
using System.Security.Principal; | |
using System.Text; | |
using System.Threading.Tasks; | |
using System.Xml.Linq; | |
using static LAPSDecrypt.Win32; | |
using static System.Net.Mime.MediaTypeNames; | |
namespace LAPSDecrypt | |
{ | |
internal class Win32 | |
{ | |
[Flags] | |
public enum ProtectFlags | |
{ | |
NCRYPT_SILENT_FLAG = 0x00000040, | |
} | |
[UnmanagedFunctionPointer(CallingConvention.Winapi)] | |
public delegate int PFNCryptStreamOutputCallback(IntPtr pvCallbackCtxt, IntPtr pbData, int cbData, [MarshalAs(UnmanagedType.Bool)] bool fFinal); | |
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] | |
public struct NCRYPT_PROTECT_STREAM_INFO | |
{ | |
public PFNCryptStreamOutputCallback pfnStreamOutput; | |
public IntPtr pvCallbackCtxt; | |
} | |
[Flags] | |
public enum UnprotectSecretFlags | |
{ | |
NCRYPT_UNPROTECT_NO_DECRYPT = 0x00000001, | |
NCRYPT_SILENT_FLAG = 0x00000040, | |
} | |
[DllImport("ncrypt.dll")] | |
public static extern uint NCryptStreamOpenToUnprotect(in NCRYPT_PROTECT_STREAM_INFO pStreamInfo, ProtectFlags dwFlags, IntPtr hWnd, out IntPtr phStream); | |
[DllImport("ncrypt.dll")] | |
public static extern uint NCryptStreamUpdate(IntPtr hStream, IntPtr pbData, int cbData, [MarshalAs(UnmanagedType.Bool)] bool fFinal); | |
[DllImport("ncrypt.dll")] | |
public static extern uint NCryptUnprotectSecret(out IntPtr phDescriptor, Int32 dwFlags, IntPtr pbProtectedBlob, uint cbProtectedBlob, IntPtr pMemPara, IntPtr hWnd, out IntPtr ppbData, out uint pcbData); | |
[DllImport("ncrypt.dll", CharSet = CharSet.Unicode)] | |
public static extern uint NCryptGetProtectionDescriptorInfo(IntPtr hDescriptor, IntPtr pMemPara, int dwInfoType, out string ppvInfo); | |
} | |
internal class Program | |
{ | |
static int delegateCallback(IntPtr pvCallbackCtxt, IntPtr pbData, int cbData, [MarshalAs(UnmanagedType.Bool)] bool fFinal) | |
{ | |
byte[] data = new byte[cbData]; | |
Marshal.Copy(pbData, data, 0, cbData); | |
string str = Encoding.Unicode.GetString(data); | |
Console.WriteLine("[*] Password is: {0}", str); | |
return 0; | |
} | |
static void Main(string[] args) | |
{ | |
string[] attributeList = new string[] | |
{ | |
"msLAPS-PasswordExpirationTime", | |
"msLAPS-Password", | |
"msLAPS-EncryptedPassword", | |
"msLAPS-EncryptedPasswordHistory", | |
"msLAPS-EncryptedDSRMPassword", | |
"msLAPS-EncryptedDSRMPasswordHistory", | |
"ms-Mcs-AdmPwd", | |
"ms-Mcs-AdmPwdExpirationTime" | |
}; | |
// Parse arguments for DN and DC | |
Console.WriteLine("LAPSDecrypt POC by @_xpn_"); | |
if (args.Length != 2) | |
{ | |
Console.WriteLine("Usage: LAPSDecrypt.exe <DN> <DC>"); | |
Console.WriteLine("Example: LAPSDecrypt.exe \"CN=CA01,OU=LAPSManaged,DC=lab,DC=local\" \"dc01.lab.local\""); | |
return; | |
} | |
string dn = args[0]; | |
string dc = args[1]; | |
string filter = string.Format("(&(objectClass={0})({1}={2}))", "computer", "distinguishedName", dn); | |
// Create a new ldap connection | |
LdapConnection ldapConnection = new LdapConnection(dc); | |
ldapConnection.SessionOptions.ProtocolVersion = 3; | |
ldapConnection.Bind(); | |
SearchRequest searchRequest = new SearchRequest(dn, filter, SearchScope.Base, attributeList); | |
SearchResponse searchResponse = ldapConnection.SendRequest(searchRequest) as SearchResponse; | |
SearchResultEntry searchResultEntry = searchResponse.Entries[0]; | |
if (searchResponse.Entries.Count != 1) | |
{ | |
Console.WriteLine("[!] Could not find computer object"); | |
return; | |
} | |
foreach (string attVal in searchResultEntry.Attributes.AttributeNames) | |
{ | |
if (StringComparer.InvariantCultureIgnoreCase.Equals(attVal, "msLAPS-PasswordExpirationTime")) | |
{ | |
var expiry = (searchResultEntry.Attributes["msLAPS-PasswordExpirationTime"].GetValues(typeof(string))[0] as string); | |
Console.WriteLine("[*] Expiry time is: {0}", expiry); | |
} | |
else if (StringComparer.InvariantCultureIgnoreCase.Equals(attVal, "msLAPS-Password")) | |
{ | |
var unencryptedPass = (searchResultEntry.Attributes["msLAPS-Password"].GetValues(typeof(string))[0] as string); | |
Console.WriteLine("[*] Unencrypted Password: {0}", unencryptedPass); | |
} else if (StringComparer.InvariantCultureIgnoreCase.Equals(attVal, "msLAPS-EncryptedPassword")) | |
{ | |
byte[] encryptedPass = (searchResultEntry.Attributes["msLAPS-EncryptedPassword"].GetValues(typeof(byte[]))[0] as byte[]); | |
Console.WriteLine("[*] Found encrypted password of length: {0}", encryptedPass.Length); | |
Win32.NCRYPT_PROTECT_STREAM_INFO info = new NCRYPT_PROTECT_STREAM_INFO | |
{ | |
pfnStreamOutput = new PFNCryptStreamOutputCallback(delegateCallback), | |
pvCallbackCtxt = IntPtr.Zero | |
}; | |
IntPtr handle; | |
IntPtr handle2; | |
IntPtr secData; | |
uint secDataLen; | |
NTAccount ntaccount; | |
uint ret = Win32.NCryptStreamOpenToUnprotect(info, ProtectFlags.NCRYPT_SILENT_FLAG, IntPtr.Zero, out handle); | |
if (ret == 0) | |
{ | |
IntPtr alloc = Marshal.AllocHGlobal(encryptedPass.Length); | |
Marshal.Copy(encryptedPass, 16, alloc, encryptedPass.Length - 16); | |
// Get the authorized decryptor of the blob | |
ret = Win32.NCryptUnprotectSecret(out handle2, 0x41, alloc, (uint)encryptedPass.Length - 16, IntPtr.Zero, IntPtr.Zero, out secData, out secDataLen); | |
if (ret == 0) | |
{ | |
string sid; | |
ret = NCryptGetProtectionDescriptorInfo(handle2, IntPtr.Zero, 1, out sid); | |
if (ret == 0) | |
{ | |
SecurityIdentifier securityIdentifier = new SecurityIdentifier(sid.Substring(4, sid.Length - 4)); | |
try | |
{ | |
ntaccount = (securityIdentifier.Translate(typeof(NTAccount)) as NTAccount); | |
Console.WriteLine("[*] Authorized Decryptor: {0}", ntaccount.ToString()); | |
} catch | |
{ | |
Console.WriteLine("[*] Authorized Decryptor SID: {0}", securityIdentifier.ToString()); | |
} | |
} | |
} | |
// Decrypt the blob | |
ret = Win32.NCryptStreamUpdate(handle, alloc, encryptedPass.Length - 16, true); | |
Console.WriteLine("[*] Decrypted Password"); | |
} | |
} | |
} | |
} | |
} | |
} |
@VitaliyYakob Not sure, I’ll take a look when I’m back in front of this code, but if you could provide further details of what you are seeing that would help?
@VitaliyYakob Looks fine to me?
@xpn Is it done in one terminal window?
It turns out that "itadmin" already has the rights to decrypt the password.
If I give my account the rights to decrypt, this utility also works for me and shows the password, but then what's the point if you can use get-lapsadpassword?
Because this is a demonstration of how Get-LapsADPassword works under the hood. We often create tools based on the internals of the technique.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Is there a code update?
Today we tested it here (Windows LAPS), and got a strange result:
If there are no password decryption rights, the utility does not return the password.
If grant access to the decrypt, the password is displayed. If delete the user from the group after that, the password continues to be displayed.