Skip to content

Instantly share code, notes, and snippets.

@xpn
Last active October 10, 2024 16:15
Show Gist options
  • Save xpn/23dc5b6c260a7571763ca8ca745c32f4 to your computer and use it in GitHub Desktop.
Save xpn/23dc5b6c260a7571763ca8ca745c32f4 to your computer and use it in GitHub Desktop.
Quick POC looking at how encryption works for LAPS (v2)
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");
}
}
}
}
}
}
@xpn
Copy link
Author

xpn commented Apr 18, 2023

Updated to print the authorized decryptor field

@schorschii
Copy link

@xpn do you know what these 16 bytes are which you are cutting off?

@xpn
Copy link
Author

xpn commented May 17, 2023

@xpn do you know what these 16 bytes are which you are cutting off?

It’s a header that from memory contains a length and a timestamp field (might be something else too). Next time I’m in front of the decompilation I’ll double check :)

@schorschii
Copy link

schorschii commented May 17, 2023

Thanks. I'm currently implementing encryption in the dpapi-ng python library for my LAPS4LINUX project and I found out that everything seems to work even with 16 zero bytes.
Edit: the initiator of the dpapi-ng library seems to know.

@xpn
Copy link
Author

xpn commented May 17, 2023

Ah here we go, so bytes:

  • 0-3 = UpperDateTimeStamp
  • 4-7 = LowerDateTimeStamp
  • 8-11 = EncryptedBufferSize
  • 12-15 = FlagsReserved

image

@schorschii
Copy link

Thanks :)

@VitaliyYakob
Copy link

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.

@xpn
Copy link
Author

xpn commented Sep 7, 2024

@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?

@xpn
Copy link
Author

xpn commented Sep 8, 2024

@VitaliyYakob Looks fine to me?
image

@VitaliyYakob
Copy link

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment