sam_honeypot_enum.c source are reproduced verbatim with attribution captions.
Executive Summary
Honeytokens and honeypot accounts are some of the highest-signal tripwires defenders can place inside Active Directory: any interaction with them is, by construction, illegitimate. The flip side — and the gap the CYPFER offensive team explores in this article — is that decoy identities cannot manufacture behavioural history without using themselves, and the asymmetry between cheap-to-fake structure and expensive-to-fake activity is exactly what gives them away.
The piece walks through a quiet enumeration strategy that pulls user and machine metadata via the LSA and Net (SAMR) APIs over MS-RPC on port 445 instead of LDAP, then reads the lastLogon attribute as an oracle: an account that looks valuable on paper but has never authenticated is almost certainly a trap. The same logic applies to machine accounts ending in $, where a lastLogon of zero is structurally impossible for any host that actually joined the domain. A full sam_honeypot_enum.c implementation is included.
Hunting Honey Pots: When Never Logged In Means Everything
In a red team engagement, the opening moves shape everything that follows. Useful reconnaissance is not about pulling the largest possible slice of the directory — it is about pulling the smallest slice that still answers the question that matters. At this stage the question is simple: which identities in this environment are real, and which ones were planted for me to trip over? Subtle inconsistencies in identity data routinely betray defensive controls such as honeypots and honeytoken accounts long before any interaction takes place, and an operator who reads those inconsistencies correctly avoids the trap rather than springing it.
Active Directory exposes most of its structure through LDAP, but in a mature environment, leaning on LDAP for everything is a mistake. Directory queries are among the most heavily instrumented internal traffic. Defenders baseline what normal lookups look like and then alert on volume, breadth, and on specific attributes that attackers typically pull. A single query against the wrong object can mean the difference between a quiet foothold and an analyst opening a ticket with your session ID attached.
Two native Windows surfaces let you collect account metadata without speaking LDAP at all. The LSA APIs and the Net APIs both return identity information through everyday system mechanisms that legitimate software calls constantly. Functions like LsaLookupNames2 and LsaEnumerateAccountRights talk to LSASS to resolve principals and their privileges; NetUserEnum and NetUserGetInfo return user records over standard SAMR / MS-RPC channels on port 445. Backup agents, inventory tools and management consoles hit these same calls all day, so the traffic blends into the operational baseline instead of standing out against it.
That blending is precisely what makes these interfaces valuable for honeypot detection: you can gather just enough signal to separate the decoys from the real population without generating the high-confidence alert that interacting with a decoy is designed to produce. Before going further into the directory, it helps to see what this quiet enumeration actually returns. The screenshot below shows a NetUserEnum sweep pulling the account list straight from SAMR with no LDAP bind at all.

NetUserEnum — every row arrives over the same RPC pipe a legitimate management tool would use. Source: original article.Each row in that output came back over the same RPC pipe a normal management tool uses. No search filter, no paged LDAP result set, nothing for a query baseline to flag as anomalous. From the defender’s point of view this looks like routine SAMR activity — precisely the cover an operator wants while assembling an initial list of accounts to investigate.
The Role of LDAP and Its Visibility
LDAP is still the richest reconnaissance surface available once you have a foothold. It exposes structured access to every directory object — users, groups, computers, service principals — along with the attributes that describe their history and behaviour. Nothing else inside an internal network gives you the same density of relationships in a single protocol, which is exactly why defenders watch it so closely.
From a detection standpoint, LDAP enumeration is high-signal. Security platforms profile the shape of directory traffic and treat broad attribute sweeps, recursive group expansion, and queries against sensitive objects as strong indicators of reconnaissance. A normal workstation does not enumerate every user object and request servicePrincipalName, adminCount, and userAccountControl in one pass — so when something does, it stands out sharply against the recorded norm.
The risk climbs further once honeytoken accounts enter the picture. These accounts are built to be unused and deliberately attractive, typically carrying names like svc_backup_adm or sql_da that imply elevated access. Any interaction at all — a directory lookup against their attributes, an authentication attempt, even a curious read — can be flagged as suspicious, because no legitimate user has any reason to touch them. The decoy does not need to catch a compromise; it only needs to catch curiosity, and an LDAP query is curiosity rendered in protocol.
For that reason, careful operators minimise direct LDAP interaction with unknown objects and prefer quieter collection methods when the same fact can be obtained another way. The directory still gets used — but it gets used surgically, and only after lower-signal techniques have already narrowed the target set.
A practical way to gather this information while staying clear of LDAP detection is to read it through the SAM remote interface instead of the directory. The MS-SAMR protocol exposes the account database over the same RPC transport that ordinary management tooling already relies on, so a short sequence of SamConnect → SamOpenDomain → SamEnumerateUsersInDomain → SamQueryInformationUser returns every sAMAccountName together with its lastLogon value without issuing a single directory search. Because no LDAP bind and no search filter are ever sent, the query baselines defenders rely on never register the activity, and the collection blends into the normal SAM traffic that already flows to the controller over port 445. The same calls extend cleanly to machine accounts ending in $, which means decoy users and decoy computers can be mapped entirely through the SAM layer. A complete implementation of SAM enumeration together with LSA translation, sam_honeypot_enum.c, is reproduced below.
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <winternl.h>
#include <ntsecapi.h>
#include <stdio.h>
#include <stdlib.h>
#define SAM_SERVER_CONNECT 0x0001
#define SAM_SERVER_LOOKUP_DOMAIN 0x0020
#define SAM_SERVER_ENUMERATE_DOMAINS 0x0010
#define DOMAIN_LOOKUP 0x0200
#define DOMAIN_LIST_ACCOUNTS 0x0100
#define USER_READ_GENERAL 0x0008
#define USER_READ_LOGON 0x0001
#define USER_READ_ACCOUNT 0x0020
#define UserAllInformation 21
typedef PVOID SAMPR_HANDLE;
typedef SAMPR_HANDLE *PSAMPR_HANDLE;
typedef struct _SAM_RID_ENUM {
ULONG RelativeId;
LSA_UNICODE_STRING Name;
} SAM_RID_ENUM, *PSAM_RID_ENUM;
typedef struct _USER_ALL_INFO {
LARGE_INTEGER LastLogon;
LARGE_INTEGER LastLogoff;
LARGE_INTEGER PasswordLastSet;
LARGE_INTEGER AccountExpires;
LARGE_INTEGER PasswordCanChange;
LARGE_INTEGER PasswordMustChange;
LSA_UNICODE_STRING UserName;
} USER_ALL_INFO, *PUSER_ALL_INFO;
typedef NTSTATUS (NTAPI *fnSamConnect)(
PLSA_UNICODE_STRING ServerName, PSAMPR_HANDLE ServerHandle,
ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes);
typedef NTSTATUS (NTAPI *fnSamOpenDomain)(
SAMPR_HANDLE ServerHandle, ACCESS_MASK DesiredAccess,
PSID DomainId, PSAMPR_HANDLE DomainHandle);
typedef NTSTATUS (NTAPI *fnSamEnumerateUsersInDomain)(
SAMPR_HANDLE DomainHandle, PULONG EnumerationContext,
ULONG UserAccountControl, PVOID *Buffer,
ULONG PreferredMaximumLength, PULONG CountReturned);
typedef NTSTATUS (NTAPI *fnSamOpenUser)(
SAMPR_HANDLE DomainHandle, ACCESS_MASK DesiredAccess,
ULONG UserId, PSAMPR_HANDLE UserHandle);
typedef NTSTATUS (NTAPI *fnSamQueryInformationUser)(
SAMPR_HANDLE UserHandle, ULONG UserInformationClass, PVOID *Buffer);
typedef NTSTATUS (NTAPI *fnSamCloseHandle)(SAMPR_HANDLE SamHandle);
typedef NTSTATUS (NTAPI *fnSamFreeMemory)(PVOID Buffer);
static fnSamConnect SamConnect;
static fnSamOpenDomain SamOpenDomain;
static fnSamEnumerateUsersInDomain SamEnumerateUsersInDomain;
static fnSamOpenUser SamOpenUser;
static fnSamQueryInformationUser SamQueryInformationUser;
static fnSamCloseHandle SamCloseHandle;
static fnSamFreeMemory SamFreeMemory;
static void InitLsaString(PLSA_UNICODE_STRING d, PWSTR s)
{
size_t n = s ? wcslen(s) : 0;
d->Length = (USHORT)(n * sizeof(WCHAR));
d->MaximumLength = (USHORT)((n + 1) * sizeof(WCHAR));
d->Buffer = s;
}
static void PrintU(const LSA_UNICODE_STRING *u)
{
int wlen, n;
char *b;
if (!u || !u->Buffer || !u->Length) { printf("(null)"); return; }
wlen = (int)(u->Length / sizeof(WCHAR));
n = WideCharToMultiByte(CP_UTF8, 0, u->Buffer, wlen, NULL, 0, NULL, NULL);
b = (char *)malloc((size_t)n + 1);
if (!b) return;
WideCharToMultiByte(CP_UTF8, 0, u->Buffer, wlen, b, n, NULL, NULL);
b[n] = '\0';
printf("%s", b);
free(b);
}
static PSID BuildAccountSid(PSID domainSid, ULONG rid)
{
DWORD len = GetLengthSid(domainSid) + sizeof(DWORD);
PSID full = (PSID)malloc(len);
UCHAR *count;
if (!full) return NULL;
if (!CopySid(len, full, domainSid)) { free(full); return NULL; }
count = GetSidSubAuthorityCount(full);
*GetSidSubAuthority(full, *count) = rid;
*count = (UCHAR)(*count + 1);
return full;
}
static void PrintLastLogon(LARGE_INTEGER ll)
{
FILETIME ft;
SYSTEMTIME st;
if (ll.QuadPart == 0) {
printf("never (0) <-- no authentication history (possible honeypot)");
return;
}
ft.dwLowDateTime = ll.LowPart;
ft.dwHighDateTime = (DWORD)ll.HighPart;
if (FileTimeToSystemTime(&ft, &st)) {
printf("%04d-%02d-%02d %02d:%02d:%02dZ",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
} else {
printf("0x%08lx%08lx", (unsigned long)ll.HighPart, (unsigned long)ll.LowPart);
}
}
static void Fail(const char *what, NTSTATUS st)
{
fprintf(stderr, "[-] %s failed: NTSTATUS=0x%08lx (Win32=%lu)\n",
what, (unsigned long)st, (unsigned long)LsaNtStatusToWinError(st));
}
int main(int argc, char **argv)
{
wchar_t target[512];
LSA_OBJECT_ATTRIBUTES loa;
LSA_UNICODE_STRING sysName, samServerName;
LSA_HANDLE policy = NULL;
POLICY_DNS_DOMAIN_INFO *dns = NULL;
PSID domainSid = NULL;
HMODULE hSam;
SAMPR_HANDLE serverH = NULL, domainH = NULL;
OBJECT_ATTRIBUTES samOA;
ULONG enumCtx = 0, returned = 0, total = 0, honeypots = 0;
NTSTATUS st;
if (argc < 2) {
fprintf(stderr, "usage: %s <dc-hostname-or-ip>\n", argv[0]);
return 1;
}
_snwprintf(target, 512, L"\\\\%hs", argv[1]);
target[511] = L'\0';
hSam = LoadLibraryW(L"samlib.dll");
if (!hSam) { fprintf(stderr, "[-] cannot load samlib.dll\n"); return 1; }
SamConnect = (fnSamConnect) GetProcAddress(hSam, "SamConnect");
SamOpenDomain = (fnSamOpenDomain) GetProcAddress(hSam, "SamOpenDomain");
SamEnumerateUsersInDomain = (fnSamEnumerateUsersInDomain)GetProcAddress(hSam, "SamEnumerateUsersInDomain");
SamOpenUser = (fnSamOpenUser) GetProcAddress(hSam, "SamOpenUser");
SamQueryInformationUser = (fnSamQueryInformationUser) GetProcAddress(hSam, "SamQueryInformationUser");
SamCloseHandle = (fnSamCloseHandle) GetProcAddress(hSam, "SamCloseHandle");
SamFreeMemory = (fnSamFreeMemory) GetProcAddress(hSam, "SamFreeMemory");
if (!SamConnect || !SamOpenDomain || !SamEnumerateUsersInDomain ||
!SamOpenUser || !SamQueryInformationUser || !SamCloseHandle || !SamFreeMemory) {
fprintf(stderr, "[-] missing samlib.dll export\n");
return 1;
}
ZeroMemory(&loa, sizeof(loa));
InitLsaString(&sysName, target);
st = LsaOpenPolicy(&sysName, &loa,
POLICY_VIEW_LOCAL_INFORMATION | POLICY_LOOKUP_NAMES,
&policy);
if (!NT_SUCCESS(st)) { Fail("LsaOpenPolicy", st); return 1; }
st = LsaQueryInformationPolicy(policy, PolicyDnsDomainInformation, (PVOID *)&dns);
if (!NT_SUCCESS(st)) {
Fail("LsaQueryInformationPolicy", st);
LsaClose(policy);
return 1;
}
if (dns == NULL || dns->Sid == NULL) {
fprintf(stderr, "[-] host returned no domain SID -- target is not a "
"domain controller (workgroup / standalone host?)\n");
if (dns) LsaFreeMemory(dns);
LsaClose(policy);
return 1;
}
domainSid = dns->Sid;
printf("[*] target DC : %ls\n", target);
printf("[*] domain (NetBIOS): "); PrintU(&dns->Name); printf("\n");
printf("[*] domain (DNS) : "); PrintU(&dns->DnsDomainName); printf("\n\n");
ZeroMemory(&samOA, sizeof(samOA));
samOA.Length = sizeof(samOA);
InitLsaString(&samServerName, target);
st = SamConnect(&samServerName, &serverH,
SAM_SERVER_CONNECT | SAM_SERVER_LOOKUP_DOMAIN | SAM_SERVER_ENUMERATE_DOMAINS,
&samOA);
if (!NT_SUCCESS(st)) { Fail("SamConnect", st); goto cleanup; }
st = SamOpenDomain(serverH, DOMAIN_LOOKUP | DOMAIN_LIST_ACCOUNTS, domainSid, &domainH);
if (!NT_SUCCESS(st)) { Fail("SamOpenDomain", st); goto cleanup; }
printf("%-8s %-28s %-28s %s\n", "RID", "sAMAccountName (SAMR)", "resolved (LSAT)", "lastLogon");
printf("-------- ---------------------------- ---------------------------- ---------\n");
do {
PSAM_RID_ENUM list = NULL;
ULONG i;
st = SamEnumerateUsersInDomain(domainH, &enumCtx, 0,
(PVOID *)&list, 1000, &returned);
if (!NT_SUCCESS(st) && st != STATUS_MORE_ENTRIES) {
Fail("SamEnumerateUsersInDomain", st);
break;
}
for (i = 0; i < returned; i++) {
ULONG rid = list[i].RelativeId;
PSID sid = BuildAccountSid(domainSid, rid);
SAMPR_HANDLE userH = NULL;
PUSER_ALL_INFO uai = NULL;
printf("%-8lu ", (unsigned long)rid);
{
int pad = 28;
int len = (int)(list[i].Name.Length / sizeof(WCHAR));
PrintU(&list[i].Name);
while (len++ < pad) putchar(' ');
putchar(' ');
}
if (sid) {
PLSA_REFERENCED_DOMAIN_LIST refDoms = NULL;
PLSA_TRANSLATED_NAME names = NULL;
NTSTATUS lst = LsaLookupSids(policy, 1, &sid, &refDoms, &names);
if (NT_SUCCESS(lst) && names && names[0].Use != SidTypeUnknown) {
int pad = 28, len = (int)(names[0].Name.Length / sizeof(WCHAR));
PrintU(&names[0].Name);
while (len++ < pad) putchar(' ');
putchar(' ');
} else {
printf("%-28s ", "(unresolved)");
}
if (names) LsaFreeMemory(names);
if (refDoms) LsaFreeMemory(refDoms);
free(sid);
} else {
printf("%-28s ", "(sid error)");
}
st = SamOpenUser(domainH,
USER_READ_GENERAL | USER_READ_LOGON | USER_READ_ACCOUNT,
rid, &userH);
if (NT_SUCCESS(st)) {
st = SamQueryInformationUser(userH, UserAllInformation, (PVOID *)&uai);
if (NT_SUCCESS(st) && uai) {
PrintLastLogon(uai->LastLogon);
if (uai->LastLogon.QuadPart == 0) honeypots++;
SamFreeMemory(uai);
} else {
printf("query failed (0x%08lx)", (unsigned long)st);
}
SamCloseHandle(userH);
} else {
printf("open failed (0x%08lx)", (unsigned long)st);
}
putchar('\n');
total++;
}
SamFreeMemory(list);
} while (st == STATUS_MORE_ENTRIES);
printf("\n[*] %lu user objects enumerated, %lu with lastLogon == 0 (decoy candidates)\n",
(unsigned long)total, (unsigned long)honeypots);
cleanup:
if (domainH) SamCloseHandle(domainH);
if (serverH) SamCloseHandle(serverH);
if (dns) LsaFreeMemory(dns);
if (policy) LsaClose(policy);
FreeLibrary(hSam);
return 0;
}
Understanding the LastLogon Attribute
The lastLogon attribute is one of the most dependable indicators of genuine account activity in Active Directory. It records the last successful interactive or network authentication for a user or computer object and is updated by the domain controller that processed the logon. The value is stored as a Windows FILETIME — a 64-bit count of 100-nanosecond intervals since January 1, 1601 — which is why a populated value reads as a large meaningless integer until you convert it back to a date.
The detail that makes this attribute so useful is its behaviour at the boundary. When an account has never authenticated, lastLogon is never written, so it sits at its default value of zero. There is no partial state and no ambiguity: an account either carries a real timestamp because something used it, or it carries a flat zero because nothing ever has. That binary quality is what turns a timestamp into an oracle, and the screenshot below shows the contrast against a suspected honeytoken next to a normal user.

lastLogon = 0; the genuine user returns a populated FILETIME that converts to a recent date. Source: original article.One caveat keeps this honest in a multi-controller environment: lastLogon is not replicated between domain controllers, so each controller keeps its own copy, and a busy account can read as zero on a controller it has simply never authenticated against. The replicated lastLogonTimestamp attribute smooths this over for auditing purposes but is deliberately imprecise and lags by days. For decoy hunting, the non-replicated lastLogon remains the sharper instrument — provided the value is checked across more than one controller before any conclusion is drawn.
One more practical detail matters when reading tool output rather than the raw attribute. Many utilities do not leave the value as a literal zero. Because lastLogon is stored as a FILETIME starting at January 1, 1601, a stored zero is the exact start of that epoch, and any tool that blindly converts the number into a calendar date will render it as a timestamp sitting on that origin. In practice it most often shows up as 12/31/1600, because the conversion to local time subtracts a timezone offset and pushes the value just behind the 1601 boundary. A displayed date of 12/31/1600 (or 1/1/1601 in tools that print in UTC) is therefore not a real logon from the distant past — it is the cosmetic representation of a lastLogon that was never set, and it should be read as exactly the same honeypot signal as a literal zero.
Honey Pots and the Absence of History
Most honeytoken accounts and decoy computer objects share the same structural weakness: they are created to detect interaction, not to perform real work. Nobody logs into them, no scheduled task runs as them, no service uses them. They are dressed up to look valuable, given plausible names and group memberships, and then left untouched in the directory as bait.
The consequence is that they look completely legitimate at the schema level while carrying no behavioural history whatsoever. No authentication trail, no password age reflecting real rotation, no group policy processing, no logon timestamp. Every attribute that describes who an object is can be forged in advance, but the attributes that describe what an object has done accumulate only through use — and use is the one thing a decoy cannot fake without defeating its own purpose.
This gap between a convincing identity and an empty history is what makes decoys detectable. Placed beside the real population and compared on activity attributes, the decoy stops blending in. The screenshot below lines up a genuine account against a honeypot across exactly those behavioural fields:

lastLogon, non-zero logonCount, and a believable pwdLastSet; the honeypot shows zeros and defaults across the board even while presenting an attractive name and elevated group membership. Source: original article.The real account on the left shows a recent lastLogon, a non-zero logonCount, and a password whose age fits a plausible rotation policy. The honeypot on the right shows zeros and defaults across every one of those fields while still presenting an attractive name and an elevated-looking group. Identity attributes alone would never separate the two — the behavioural attributes do it immediately.
Using LastLogon as an Oracle
The core inference is simple. An account that appears valuable but has never been used is far more likely to be artificial than overlooked, because real privileged accounts get used and real disused accounts rarely keep their privilege. By correlating the displayed importance of an account — its name, its group membership, its apparent role — with the lastLogon attribute, the inconsistencies that betray a decoy become visible without ever authenticating against it.
The technique works because of an asymmetry in defender effort. Defenders invest heavily in making the structure of a decoy convincing — the name, the description, the membership in a Tier-0 group — but they rarely invest in simulating a long-term behavioural pattern, because doing so realistically would mean actually using the account and exposing it to the very compromise it is meant to catch. The structure is cheap to fake; the history is expensive. The history is where the lie shows.
Treating the absence of a lastLogon value as an oracle lets an operator triage a target list cheaply. Accounts with genuine history move to the top as real candidates; accounts that are valuable on paper but empty in practice get marked as probable decoys and avoided. None of this requires touching the suspected object directly, which is the whole point: the decision to leave an account alone is made from quiet metadata collected through Net and LSA calls, not from the loud directory interaction that would trip the trap.
The Same Tell on Machine Accounts
Everything above applies just as cleanly to computer accounts — and in several respects with even greater force. Every domain-joined workstation and server holds a machine account in Active Directory, represented by a sAMAccountName ending in a dollar sign such as WS014$ or SQLPROD01$. These objects are not cosmetic. The $ account is the identity a machine uses to authenticate to the domain, and it is exercised continuously throughout normal operation rather than sitting idle.
The reason the lastLogon signal is even sharper on computers is the enrollment process itself. A machine cannot become a member of the domain without first talking to a domain controller. When a host is joined — whether through the classic NetJoinDomain call, an unattended provisioning package, or an Offline Domain Join blob — it establishes a Netlogon secure channel and authenticates with its machine account at least once to complete enrollment. From that point on, the account keeps re-authenticating on a schedule, rotating its machine password roughly every thirty days and requesting Kerberos tickets for the services it consumes. A legitimate computer object therefore cannot carry a lastLogon of zero, because the act of becoming a real computer object is what writes that value in the first place.
That property makes a decoy computer trivial to expose. A defender can pre-stage an attractive machine account, give it a server-sounding name and a tempting servicePrincipalName, and drop it into the directory as bait — but they cannot manufacture authentication history without actually joining a host and exposing the very trap they are setting. Running the same enumeration against accounts ending in $ makes the decoy surface immediately, as the screenshot below shows.

lastLogon from a live secure channel; the two bait objects — one imitating a backup domain controller — report lastLogon = 0, a state that is essentially impossible for a working host. Source: original article.The genuine workstations and servers in that output all carry a recent lastLogon, which is produced only by a live secure channel renewing itself against the controller. The two bait objects present convincing names — one even imitating a backup domain controller — yet both report lastLogon set to zero, a state that is essentially impossible for a computer object in a working environment. An account that claims to be a domain-joined machine but has never authenticated never finished joining; an object that never joined is far more likely to be a tripwire than a real host. The same quiet collection technique used for user accounts therefore doubles as a way to map the decoy machines without ever authenticating against them.
Detection That Matters
Honeypots are a genuinely strong defensive control and they deserve respect from both sides of the engagement. Because any interaction with a true decoy is, by definition, illegitimate, the alerts they generate carry very high confidence and very low false-positive rates — a rare and valuable combination in detection engineering. A defender who deploys them well gets a tripwire that fires only when something is actually wrong.
From the offensive side they introduce real risk during reconnaissance and lateral movement, and the cheapest way to manage that risk is to read the signals decoys cannot hide. The lastLogon attribute, collected carefully and confirmed across controllers, is one of the clearest such signals — and it costs almost nothing to check.
None of this makes honeypots less worthwhile for the defender. They are one layer in a strategy that should also include directory-query monitoring, authentication-anomaly detection, and tight control over who can read activity attributes in the first place. A defender who assumes a single tripwire is enough will eventually meet an operator patient enough to map the environment without stepping on it; a defender who layers honeypots with behavioural monitoring forces that same operator to make a mistake somewhere else instead.
Key Takeaways
- Honeytoken accounts and decoy computer objects are detectable not by their identity but by their absence of behavioural history; the cheap-to-fake structure-vs-history asymmetry is the offensive lever.
- A
lastLogonof zero (or its cosmetic equivalent, a date rendered around12/31/1600/1/1/1601) on an apparently valuable account is a near-binary oracle for “decoy / never used.” - The non-replicated
lastLogonis sharper than the replicatedlastLogonTimestampfor decoy hunting, but only when checked across more than one domain controller. - Quiet enumeration via LSA + Net (SAMR) APIs over MS-RPC on port 445 sidesteps the LDAP detection baselines that catch broad directory sweeps.
- The same enumeration applies to machine accounts ending in
$; a legitimate computer object structurally cannot havelastLogon = 0, so any that does is almost certainly a tripwire. - Honeypots remain a high-fidelity defensive control — they just need to be layered with behavioural monitoring and access controls that prevent attackers from reading the activity attributes that expose them.
Defensive Recommendations
- Restrict read access to behavioural attributes. Lock down
lastLogon,logonCount,pwdLastSetand similar fields with ACLs that hide them from unprivileged principals; if attackers cannot read the absence-of-history signal, they cannot use it as an oracle. - Instrument SAMR / MS-RPC traffic, not just LDAP. Most baseline-driven detection focuses on directory queries. Enable Audit Directory Service / Audit SAM / RPC-channel logging and alert on bulk
SamEnumerateUsersInDomainandNetUserEnumpatterns hitting domain controllers from unusual source hosts. - Simulate plausible decoy activity. Schedule periodic authentication, password changes, and group policy refreshes against honeytoken accounts (from isolated, monitored sources) so they accumulate believable behavioural history. Treat the simulated logon source as a tripwire of its own.
- Pre-join decoy machines properly. If you place decoy
$accounts, complete the domain join from a sacrificial host solastLogonand Netlogon secure-channel attributes look normal. An unjoined “machine” account stands out instantly. - Layer honeypots with behavioural detection. Treat decoys as a high-confidence tripwire, not a complete control: pair them with authentication-anomaly detection (Tier-0 logons from unexpected hosts, unusual ticket requests, off-hour service-account use).
- Watch for the
12/31/1600read pattern. Detection rules that look for “lastLogondisplayed as ~1601 epoch” reads — not just literal zeros — catch tooling that has converted the value before exfiltration. - Audit cross-DC reads. An attacker checking
lastLogonacross multiple domain controllers to defeat the non-replication caveat produces a distinctive multi-controller probe pattern. Alert on per-principal SAMR reads spanning more than one DC in a short window.
Conclusion
Decoys remain one of the highest-quality detection tools an Active Directory team can deploy — but the work that makes them convincing has to extend past the schema. Behavioural attributes such as lastLogon are not just metadata: they are the evidence trail that an account has actually been used, and an attacker who can read them quietly through SAMR can answer the only question that matters before any interaction takes place. Hardening that read surface, simulating realistic activity on bait accounts, and layering honeypots with behavioural monitoring is what turns a single tripwire into a control that survives a patient adversary.
Original text: “Hunting Honey Pots as Red Teamers” by Charles F. Hamilton at CYPFER Offensive Practice.

