Hunting Abuse: Detecting Privilege Escalation Through the ADCS Database

BLOG

Active Directory Certificate Services (ADCS) has emerged as one of the most significant and under-monitored attack surfaces in enterprise Windows environments, as documented by SpecterOps. After observing certificate-based privilege escalation techniques being actively exploited in several cases that the GuidePoint Security DFIR (Digital Forensics and Incident Response) team has worked, specifically the abuse of misconfigured certificate templates to impersonate privileged identities, we decided to investigate what detection opportunities existed at the source: the Certificate Authority (CA) server itself.

In August 2026, CISA published Advisory AA26-237A, describing red team assessments where the ESC1 issue described in this post was used to compromise a domain. As CISA put it:

“The red team also queried the organization’s Active Directory Certificate Service (ADCS) certificate templates. Misconfigured ADCS templates are common and can allow low-privileged accounts to request a certificate on behalf of other users and computers, including highly privileged accounts. The team identified multiple templates with an ESC1 misconfiguration, which allows any user to request certificates for all users and computer accounts.”

CISA’s advice is to disable this feature, tighten who can enroll in these templates and clean up unnecessary permissions. What we described allows you to monitor/detect this type of abuse.

TL;DR – Though often overlooked as a forensic data source, the ADCS CA database stores evidence of privilege escalation requests. However, retrieving that data requires a fix to a timestamp parsing bug in the go-ese library.

  • The CA database records all certificate requests, including those that were denied, without requiring additional logging, agents or service modifications.
  • A bug in the go-ese library misinterpreted FILETIME bytes as OLE doubles, causing all timestamps to resolve to 1899-12-30.
  • The fix uses value-range inspection to select the correct decoder; this fix ships in Velociraptor 0.76.6+.

 

Does the ADCS CA Database Store Denied Certificate Requests?

The core question we wanted to answer was simple: if an attacker requests a certificate they are not entitled to, does the CA keep a record of that? The answer is yes and more importantly, it keeps a record even when the request fails or is denied. Every certificate request, successful or otherwise, is permanently logged in the CA database. This makes the CA a valuable forensic source that is often overlooked in favor of network or endpoint telemetry.

How Can Privilege Escalation be Detected Through ADCS?

Our approach was to go directly to that CA database and read it forensically. Without relying on the CA service itself. Without installing additional logging agents. And without requiring any changes to the CA configuration. We wanted something that could be deployed rapidly across multiple CA servers during an incident or as a routine hunt, returning actionable data that an analyst could immediately use to identify suspicious activity.

Problem Solving a Technical Issue

What seemed like a simple data collection task turned into a bigger technical problem. The tool we used had an unknown bug that made all timestamps in the CA databases unusable. To fix it, we had to patch the underlying library and rebuild the tool from source. In the end, we produced a reliable artifact that shows the complete certificate request history directly from the CA database, including the identity the attacker tried to impersonate, which is the key detail for investigating privilege escalation through certificate abuse.

What is Velociraptor and How Does it Help Collect Forensic Evidence?

Velociraptor is an open-source digital forensics and incident response platform that gives analysts the ability to write precise, repeatable collection logic that runs directly on endpoints; reading raw files, executing recovery utilities and joining data across multiple sources in a single artifact execution. Its Velociraptor Query Language (VQL) makes it well suited for exactly this kind of targeted forensic collection and its ability to access locked files through a raw NTFS (New Technology File System) accessor was a critical capability for this work given that the CA database is held open exclusively by the certificate service at all times.

The specific target is the ADCS CA database, an ESE (Extensible Storage Engine) database file located at C:\Windows\System32\CertLog\ by default on the Windows Certificate Authority server. This database contains the complete history of every certificate request the CA has ever processed, across four tables which are relevant for detection:

  • Requests: every certificate request with submitter identity, timestamps and disposition
  • Certificates: issued certificate metadata including validity period, serial number and subject
  • RequestAttributes: client-supplied attributes including the certificate template name and Subject Alternative Name (SAN), which is where attacker-supplied UPN values appear in ESC1 attacks
  • RequestAttributes inline text: a redundant copy of the same data embedded in the Requests table itself, used as a fallback when the separate table is incomplete

By reading this database directly from disk and replaying the ESE transaction log to capture any uncommitted recent activity, we can reconstruct a complete, timestamped record of certificate request attempts without touching the CA service. This includes modifying any configuration or relying on Windows Event Log entries, which are frequently incomplete or disabled in practice.

To safely extract data from the ADCS CA database and reconstruct the information required for analysis, we developed a custom VQL artifact for use with Velociraptor.

What Was the Timestamp Parsing Issue in the Go-ESE Library?

When we first ran the artifact against a live CA database, everything appeared to be working. Records were returned, requester names and certificate subjects were correct, but every single timestamp field showed 1899-12-30T00:00:00Z. This affected all DateTime columns across the Requests and Certificates tables: submission time, resolution time, revocation time, certificate validity start and certificate validity end. Without accurate timestamps, the artifact was effectively useless for any time-based analysis or correlation.

Our first assumption was that we were applying the wrong conversion in the VQL query. The Velociraptor parse_ese plugin returns DateTime columns as Go time.Time objects and we had been wrapping them in various timestamp conversion functions. We tried timestamp(winfiletime=…), timestamp(epoch=…) and passing the value through directly. None of these changed the output. The date remained 1899 regardless of what conversion was applied.

Replicating the Technique in a Lab Environment

In order to understand this better, we decided to replicate the attack pattern in a lab environment to generate some of these events.

The test scenario consists of a threat actor having access to a compromised system as a normal Domain User.

During the Discovery phase, a misconfigured certificate template is discovered on ADCS. The template allows Enrollment Rights to all Domain Users, can be used for Client Authentication and has the SubjectNameEnrolleeSupplies flag. This can be abused to request valid Client Authentication certificates as any user in the domain by specifying valid UPN as the Subject of the certificate. Ultimately, this could lead to domain privilege escalation.

Through the misconfigured template, a new certificate was requested by specifying the administrator user as the subject.

The certificate was then used to authenticate as the administrator user to the Domain Controller, receiving a valid Ticket-Granting-Ticket (TGT). This TGT can then be used to perform any operation as the high-privileged administrator user on the domain.

By importing the ticket in the current process, we are then able to move laterally to other hosts on the domain.

Searching for a Fix: Math to the Rescue

The issue was in the go-ese library, not our VQL. The library was misreading Windows FILETIME bytes as OLE variant doubles because of a metadata flag set to zero in the ADCS database schema. Direct binary analysis of the database pages confirmed the bytes on disk were correct FILETIMEs all along. A blanket fix broke other databases that legitimately use OLE doubles, so the final solution makes use of the fact that the two encodings produce completely different values, allowing the correct decoder to be selected automatically by inspecting the value itself.

After confirming that parse_ese was returning a time.Time object rather than a raw integer, we traced the issue into the underlying go-ese library. In parser/catalog.go, DateTime decoding depends on the column Flags value stored in the ESE catalog. When Flags=1, the library correctly decodes the value as a Windows FILETIME using WinFileTime64(). When Flags=0, it instead interprets the raw bytes as an OLE date value. Using esedbinfo (part of libesedb) to inspect the ADCS CA database schema, we found that every DateTime column was marked with Flags=0, causing go-ese to consistently take the wrong decode path. However, extracting the raw bytes directly from the database pages showed the values were actually stored as standard Windows FILETIMEs. Decoding the same bytes as a FILETIME produced timestamps that matched the output received from certutil exactly, while decoding them as OLE doubles resulted in near-zero floating point values that mapped to the incorrect date 1899-12-30. Cross-checking with ESEDatabaseView and esedbexport (part of libesedb) confirmed the issue: both tools ignored the Flags value and treated the columns as FILETIMEs, returning the correct timestamps. An initial fix that forced all Flags=0 DateTime columns to decode as FILETIMEs solved the ADCS issue but broke support for SRUDB.dat, which also uses Flags=0 but legitimately stores OLE date values. The final solution came from examining the decoded float ranges themselves. Valid OLE date values produce normal floating point numbers well above 1.0, while misinterpreted FILETIMEs produce extremely small near-zero values. Because the gap between the two formats is so large, the decoder can reliably determine the correct encoding directly from the value itself rather than relying solely on the incorrect catalog flag. The fix was a single additional conditional in the Flags=0 branch of the decoder: if the bytes interpreted as a float64 are greater than 1.0, treat them as an OLE double using the existing logic; otherwise treat them as a Windows FILETIME

All existing test fixtures continued to pass and the ADCS timestamps decoded correctly.

A pull-request was opened for the fix to the official Velocidex repository of go-ese and it was approved and merged into the codebase.

A portion of the technical work described in this post was carried out with the assistance of an AI coding agent. Tasks that would traditionally require hours of manual effort, like reading and cross-referencing library source code across multiple repositories, performing binary analysis of undocumented database page structures and identifying the root cause of a subtle encoding bug buried in a third-party Go library were completed in a fraction of the time. This allowed our team to significantly reduce the time required to identify the underlying issue, implement a fix for it and create a working VQL artifact.

Where Can I Find the VQL Artifact?

The VQL artifact developed to look for this behavior is available at the following gist:

https://gist.github.com/sec-pc/a134c4c0441c2e9c66d61488b1402459

The pull request for the DateTime decoder fix has been merged into the official go-ese repository and the Velociraptor go.mod has already been updated to reference the fixed version. This means anyone building Velociraptor from source right now will automatically get the corrected behavior with no additional steps required. The Go toolchain resolves the dependency at build time and the fix is compiled directly into the binary.

For users running the official pre-compiled Velociraptor releases, version 0.76.6 is confirmed to contain the updated go-ese module through which the VQL artifact runs correctly.

All previous versions of the published binaries were built before the go.mod update landed and will not include the fix. These binaries will continue to return 1899-12-30T00:00:00Z for all DateTime fields when parsing ADCS CA databases. 

With the underlying timestamp parsing issue now resolved, a pull request was created on the official Velociraptor Artifact Exchange repository:

https://github.com/Velocidex/velociraptor-docs/pull/1275

Once approved, this will allow investigators to directly leverage Velociraptor to detect, hunt for and analyze suspicious certificate request activity within ADCS environments, without requiring any manual installation or custom tooling.

Learn more about how GuidePoint Security can help you detect certificate-based attacks and improve threat hunting and incident response.

Paolo is a Senior DFIR Consultant with GuidePoint Security’s DFIR practice who helps organizations respond to complex cyber incidents with proven methodologies to give clients clear visibility into their environments, quickly uncover threats and contain intrusions from highly skilled adversaries targeting critical data and intellectual property. Before joining GuidePoint Security, Paolo worked at NetWitness as a Senior Incident Response Consultant, where he investigated a wide range of cyber threats—from opportunistic attacks to complex, nation-state–backed campaigns. His experience includes investigating incidents involving ransomware, advanced persistent threats (APTs), business email compromise and insider risks, across clients in multiple industries. Paolo holds a Master’s degree in Cyber Security and a Bachelor’s degree in Computer Science from the University of Trento, Italy.