Security incident? Suspected breach? 09 71 18 27 69csirt@synacktiv.com

Simulating legitimate Active Directory services on the network: the case of GPO exploitation

Written by Quentin Roland - 31/08/2026 - in Pentest - Download

Simulating legitimate Active Directory services on an internal network is a powerful and versatile capability that can be leveraged in various contexts. Many examples of exploits relying on the ability to simulate working LDAP and/or SMB services can be cited, such as Group Policy Object exploitation or, more recently, the Certighost (CVE-2026-54121) ADCS flaw.

This is something that we started working on several years ago in the context of GPO exploitation (see this article as well as this article). The present publication introduces follow-up research that provides a way to simulate authenticated LDAP and SMB services on an internal network, in a flexible way that can be adapted to various use-cases. The research builds upon scapy and the great work of Gabriel Potter. It will be presented through the prism of GPO exploitation, but the aim of the resulting tooling is to be flexible in order to facilitate future research in other areas.

Looking to improve your skills? Discover our trainings sessions! Learn more.

0. Introduction

We encountered the need to simulate working, authenticated Active Directory services a while ago, when we started working on Group Policy Objects exploitation. This need also highlighted the lack of adapted tooling allowing us to do so. 

More specifically, the exploitation of GPOs through the modification of their gPCFileSysPath attribute required a working and authenticated SMB service to serve poisoned GPO files (see the reminder below in part 1). A bit later, the exploitation of the gPLink attribute on containers required both an LDAP and SMB service to succeed (see the reminder below in part 2).

Over the years, other interesting use cases appeared for the simulation of legitimate AD services. We encountered some of them during our assessments – with the example of a specific software fetching executable files via a user-controlled URI but expecting a legit, authenticated SMB service on the other end. More recently, the Certighost (CVE-2026-54121) exploit also constitutes a good illustration, since it necessitates the simulation of LDAP and SMB services.

When we first released our GPO exploitation tooling GPOddity and OUned, we included some ways to simulate authenticated domain services. It resulted in a mix of more-and-less stable modified impacket scripts, and complex network forwarding setups leveraging Windows VMs. While the proposed solution worked in some cases, it also presented several limitations that will be detailed below. Other approaches such as the one adopted by the Certighost exploit script are tailored to one specific exploit case, but lack flexibility.

This is why we started working on a new implementation that would allow offensive security professionals to easily simulate authenticated LDAP and SMB protocols in a robust and flexible way, working even for specific edge-cases. We will first describe the simulation of an SMB service, with our implementation supporting session multiplexing (several SMB sessions within one TCP connection). This allows GPOddity to seamlessly handle user GPOs. We will then delve into the simulation of a functioning LDAP service and its use with OUned for AD containers exploitation.

In both cases, the scripts are based on scapy and the great work of Gabriel Potter on the project, that provided an extensive basis regarding the reimplementation, in python, of Windows network protocols.

1. SMB

The first attack vector requiring the simulation of a working authenticated Active Directory service resided in the exploitation of GPOs via NTLM relaying. The associated research resulted in a tool that we called GPOddity and that was originally released in September 2023.

a. Reminders and context

This section provides some generic reminders regarding GPO implementation in Active Directory, as well as the GPOddity attack vector (which is explained in depth in the original article). If you are already familiar with these topics, feel free to skip ahead to the next section.

In Active Directory, a GPO is composed of two components: the Group Policy Container (GPC), which is an LDAP object containing GPO metadata, and the Group Policy Template (GPT), which is an SMB share (by default located on the SYSVOL share of a Domain Controller) that contains the actual GPO configuration files.

When a user or computer object attempts to apply their GPOs, they will fetch the gPLink attribute of their containers (Organizational Units, Sites, and Domain objects). This attribute contains a list of GPCs, corresponding to the GPOs the object should apply. For each GPO, the user or computer first fetches the GPC via LDAP. It reads the GPO metadata, including the gPCFileSysPath attribute which indicates the location of the SMB share acting as the GPT. The user or computer then connects to said SMB share, authenticates, and retrieves the GPO files in order to apply the desired configurations.

Illustration of GPO exchanges
Illustration of GPO application in Active Directory.

The GPOddity attack vector works in the following way. If an attacker obtains write access to the GPC LDAP object, it is possible to modify the gPCFileSysPath attribute to point to an SMB server under the attacker’s control. It allows them to serve malicious GPT files containing for instance Scheduled Tasks, and compromise any object applying the poisoned GPO. Since this attack vector only relies on the modification of the GPC LDAP object, it is exploitable via NTLM relaying to the LDAP service.

b. Previous SMB server implementation and limits

In the context of the GPOddity attack described above, the malicious SMB server to which GPO clients are redirected could not simply provide guest access or accept any credentials without validation. Indeed, reasonably modern Windows SMB clients will by default refuse to connect anonymously to a remote SMB server, and the Secure Negotiate feature enabled since Windows Server 2012 / Windows 8 enforces the exchange of one signed packet, ensuring that the remote server properly authenticated the incoming client. For more information on this point, please refer to the GPOddity article, part 7 « After root: GPOddity’s inner workings ».

As a result, exploiting the gPCFileSysPath poisoning attack requires the implementation of an SMB server that properly authenticates the incoming clients by validating their domain identity. The first solution developed in GPOddity relied on a modified version of impacket’s SMB server, that was tweaked to leverage the Netlogon protocol and generate the appropriate session key. This implementation can be found in the original gpoddity_smbserver.py file, while additional details are again provided in the GPOddity article, part 7 « After root: GPOddity’s inner workings ».

However, after some testing, we realized that while this impacket SMB server implementation worked correctly for the exploitation of computer Group Policy Objects, it failed for an unknown reason when it came to user Group Policy Objects. In this latter case, the Windows SMB client of the target host failed to retrieve the Group Policy Template files from our SMB server. We were able to confirm that something was lacking in our SMB server implementation, since everything worked fine with the SMB server of a Windows host in the domain. We could not however pinpoint exactly the issue with our SMB server implementation (this was mentioned in the GPOddity article, part 5 – b. « The special case of User Group Policy Objects ».

c. Creating a multiplexing SMB server with scapy

Coming back to the exploit a few years later, we were able to find out the issue with our previous Netlogon SMB server based on impacket. We then implemented a more resilient one, building upon scapy and the work of Gabriel Potter.

In order to understand the failure of the original GPOddity exploit implementation in the case of user GPOs, we must go back to 2016, and more precisely to the MS16-072 Microsoft security update. Before such an update, the retrieval of the Group Policy Template files when applying a GPO was rather straightforward:

  • For GPOs applying to the computer, the machine account associated with said computer authenticated to the Group Policy Template SMB server, and fetched the files.

  • For GPOs applying to a user, the identity of the user account was used to authenticate to the GPT SMB server and retrieve the files.

Such a configuration led to a privilege escalation vulnerability discovered in 2016 (CVE-2016-3223). Indeed, the Group Policy client executes with high administrative privileges under the NT AUTHORITY/SYSTEM identity when applying GPOs on a host. The vulnerability occurs when an attacker knows the credentials of a domain user that can connect to a machine, with low-privileges on the system. If the attacker can also adopt a Man-in-the-Middle position between the target machine and the network, they can create a dummy domain with the same user. They can then redirect all traffic from the target host to the dummy Active Directory domain controller. Upon user GPO refresh, the dummy domain will be able to properly validate the identity of the user (which has the same password on the dummy domain), and then deliver an arbitrary GPC and GPT. In this way, the attacker can trick the target system into applying arbitrary Group Policy Object configurations, which will be executed by the Group Policy client as SYSTEM, resulting in a local privilege escalation from a low-privileges domain user.

Under Microsoft’s own words, their proposed fix was the following: « Before MS16-072 is installed, user group policies were retrieved by using the user’s security context. After MS16-072 is installed, user group policies are retrieved by using the computer's security context ». This would indeed fix the aforementioned vulnerability, since an attacker knowing only the password of the low-privileged user would not be able to properly authenticate the machine account to deliver a malicious GPO. And if the attacker knows the password for the machine account, said host can already be considered as fully compromised.

How does this relate to the SMB server issue at hand ? The Microsoft communication on the fix implemented by the security update is not 100% accurate. When fetching the GPT files for a user GPO, two steps are actually taking place:

  1. First, the user authenticates to the SMB server to fetch the GPT.INI file, which contains some GPO metadata such as the version.

  2. Afterwards, the machine account authenticates to retrieve the other files containing the actual configurations to apply.

The reason why the user account still authenticates to fetch the GPT.INI file is unknown, but an important detail is that the same TCP connection is used for both the user and machine account. The SMB specification indicates that it is possible to spawn multiple sessions within a single TCP connection, through the mechanism of SessionID (see Microsoft’s documentation: « There could be multiple active sessions over a single SMB 2 Protocol connection. The SessionId field in the SMB2 packet header distinguishes the various sessions »).

The below two screenshots show a Wireshark capture for the SMB traffic generated by the application of a user GPO. A filter has been applied on TCP stream 7, so all the displayed messages are part of the same TCP connection. The captures show that a first session is created with ID 0x0000380000000071 for the user to which the GPO applies, adm-qroland. It is with this identity that the GPT.INI file is retrieved. Afterwards, within the same TCP connection, another SMB session is created, authenticated this time with the identity of the machine on which the user is connected, AD01-SRV1$. This second session has the ID 0x0000380000000075, and is used to fetch the XML files for the GPO, here the ScheduledTasks.xml file.

Wireshark capture SMB exchanges user GPO
First SMB session established with the identity of adm-qroland in TCP stream 7.
Wireshark capture user GPO application
Second SMB session established with the identity of AD01-SRV1$ in TCP stream 7.

The issue that was encountered in the case of user GPOs with the modified impacket SMB server was thus simply that said implementation was not built to handle multiple sessions (each of them having different session keys for message signing, among others) within a singular TCP connection. It assumed that one TCP connection could only be associated with one session. As a result, confusion arose when the only structure associated with the TCP connection was used both by the user account and the computer account, resulting in wrong signing keys being used, among others.

After looking at alternatives for an SMB server running on Unix systems, we only came upon the one implemented by Gabriel Potter in the scapy project. However, this SMB server also did not support multiplexing and handled a single session per TCP connection.

We then decided to build upon the existing scapy implementation, and modify its SMB server in order to support session multiplexing. We chose to leave impacket behind since the project has several distinctive markers associated with offensive actions.

The modified scapy version implementing the multiplexing SMB server can be found in the GPOddity project (as well as the OUned project). The main alterations compared to the previous implementation reside in the new SMBStreamSocketMultiplexing class (layers/smb2.py), as well as modifications in the SMBServer class (layers/smbserver.py) to handle multiple active sessions.

All in all, thanks to the work of Gabriel Potter on Windows network protocol implementation and to the aforementioned tweaks, we now have a python SMB server that accepts domain authentication (via Kerberos or Netlogon), and supports multiplexing in accordance with the SMB specification. This is a much cleaner and more robust implementation compared to the original impacket variant. And this will allow us to seamlessly exploit user Group Policy Object via GPOddity, among others.

Please note that the multiplexing-enabled SMB server is not yet merged into Scapy’s main branch. We plan to submit a pull request, but still have to perform regression tests on other features. In the meantime, it is available in the GPOddity and OUned repository, both projects providing a usage example.

d. Multiplexing SMB server demonstration with GPOddity

The following video illustrates the use of the SMB server via GPOddity. More specifically and as shown in the video, a domain user, adove, has control over a GPO called ADMINS_HARDENING_ANY. The GPO has user configurations, and applies to a domain admin, adm-qroland. The demonstration shows that an attacker with control over a machine account ATTACKER$ and that was able to relay the adove user to the LDAP service can then use GPOddity to compromise adm-qroland by spoofing the SMB location for the GPT of the ADMINS_HARDENING_ANY GPO, and running the multiplexing SMB server to deliver the malicious files.

Note that the commands to be executed through the spoofed GPO can be provided via a « module file ». These module files are common to the GroupPolicyBackdoor, GPOddity and OUned tools. They allow injecting malicious configurations into GPOs in a flexible way (see documentation in the GroupPolicyBackdoor project). Alternatively and for convenience, it is also possible to simply provide a command that will be executed via an Immediate Task as SYSTEM.

 

Video file

 

It is important to mention that in the present section, we focused on the use of the spoofed SMB service for GPO exploitation. However, this is just one example among many. Robust, authenticated spoofed SMB services can be used in various situations. For instance, during an internal engagement, we recently came upon an admin panel for a software that we accessed with default credentials. One of the features proposed by the admin panel was to provide a path to an executable file for software updates. It was then possible to specify an SMB path, redirect to an authenticated SMB server and gain command execution on the underlying server. This would not have been possible with an anonymous SMB server, since the Windows SMB client would have refused to connect to it. The Certighost flaw also recently demonstrated the point we are trying to make here, and it probably won’t be the last.

2. LDAP

Shortly after GPOddity, we started working on a second attack vector requiring to simulate another legitimate, authenticated Active Directory service. Such an attack vector was also related to Group Policy Objects, but allowed the exploitation of Active Directory containers such as Organizational Units, Domains and Sites objects. This resulted in the release of the OUned tool in April 2024.

a. Reminders and context

The OUned attack vector is based on the work of Petros Koutroumpis, and is explained in details in the associated article.

In a nutshell, the attack vector is exploitable when a controlled user has any kind of permission allowing them to write the gPLink attribute of either an Organizational Unit, a Site, or even a Domain object.

In such a case, the attacker would be able to add a malicious item to the gPLink list. Said element would indicate that the objects belonging to the target container should apply a GPO whose GPC is located on a computer controlled by an attacker. For instance, the following gPLink element would make all the objects of the container fetch a GPC at attacker.corp.com:

[LDAP://cn={7B7D6B23-26F8-4E4B-AF23-F9B9005167F6},cn=policies,cn=system,DC=attacker,DC=corp,DC=com;0]

b. Previous LDAP server implementation and limits

After poisoning the gPLink LDAP attribute, the attacker-controlled machine should serve a valid Group Policy Container as well as a Group Policy Template. The attack requirements are then one step further than for GPOddity: the attacker must not only act as a legitimate authenticated SMB service, but also as a legitimate authenticated LDAP service.

The first OUned implementation back in April 2024 used the same strategy as presented by Petros Koutroumpis in his original research for the GPC part. Instead of implementing an LDAP service, the LDAP trafic was simply forwarded to a dummy Active Directory domain controller.

 

OUned original implementation
Illustration of OUned original implementation.

In the dummy domain, the password for the DC was synchronized with the one of the machine account with the LDAP SPN on the target domain. This machine resolved to an attacker-controlled host, which forwarded the LDAP traffic to the dummy domain DC. The latter was able to verify the incoming Kerberos ticket due to the password synchronization, and act as an authenticated LDAP service.

While this solution could work in some cases, it was far from perfect and suffered multiple drawbacks:

  • First, the setup was pretty tedious, as it required setting up an Active Directory domain as well as ensuring the proper network access from the controlled machine to the dummy DC. Tedious setups are not only time-consuming, but also error-prone, something that should be avoided, especially for stealth considerations.

  • If the controlled machine account resolving to the attacker host was configured to use AES for the Kerberos tickets issued for their services, the proposed setup would not work. Indeed, synchronizing the password of the controlled machine account to the one of the dummy Domain Controller will only result in the same RC4 keys, but not the same AES keys which are calculated not only from the password itself, but including a salt built from the hostname and the domain name. The latter will necessarily be different for the dummy domain, since it has to match the name of the machine with the LDAP service.

    Granted, by controlling the machine account it would always be possible to modify the msDS-SupportedEncryptionTypes LDAP attribute (this can be altered with the identity of the machine itself) to switch to RC4. However, this is again not desirable from a stealth perspective.

  • Finally, synchronizing the passwords of the controlled machine account to the one of the dummy DC is feasible, but can be far from trivial depending on the situation.

c. Creating a versatile LDAP server with scapy

The solution to the various issues presented above would be to directly implement an LDAP server in the OUned tool, in python – i.e. implement a functional authenticated LDAP service in python.

We again came across the work of Gabriel Potter on scapy, which implemented not only the authentication layers, but also an LDAP client with various LDAP structures. We were able to take this work as a basis for the implementation of an LDAP server in python.

The scapy version including the LDAP server in question is the same as the one implementing the multiplexing SMB server, and is thus available in the OUned project (as well as in the GPOddity project). The server itself is implemented at layers/ldapserver.py, and some modifications have been performed on the common layers/ldap.py file.

Please note that, similarly to the multiplexing SMB server, we did not create a pull request to scapy yet, but we will do so in the near future.

The idea behind the LDAP server implementation was to provide a generic and flexible implementation that could be reused for future research, rather than only building LDAP functionalities specific to the OUned exploit. It takes as argument a JSON file that represents the LDAP data to be served. In the JSON file, the keys correspond to the Distinguished Name of the LDAP items to be served, and the values are dictionaries containing attributes. Nested elements can be provided. There is no check on Distinguished Name coherence or any kind of enforced schema (since this LDAP server is meant for research purposes, we want to leave as much freedom as possible regarding the data served). The JSON file may specify a custom DTO via the empty key at the root of the JSON object. It is also possible to specify non-printable attribute values by prefixing them with base64:.

Below is an example of a JSON file containing the data for the LDAP server. A custom DTO is specified, simulating a domain called corp.com. An Organizational Unit (FINANCE) is defined, containing some users and some nested other Organizational Units, themselves containing users with various attributes of various types.

$ cat ldap.json
{
    "": {
        "objectClass": [
            "top",
            "ScapyLDAProotDSE"
        ],
        "altServer": [],
        "namingContexts": [
            "DC=CORP,DC=COM",
            ""
        ],
        "supportedControl": [],
        "supportedExtension": [],
        "supportedFeatures": [],
        "supportedLDAPVersion": [
            3
        ],
        "supportedSASLMechanisms": [
            "GSSAPI",
            "GSS-SPNEGO"
        ],
        "defaultNamingContext": [
            "DC=CORP,DC=COM"
        ],
        "rootDomainNamingContext": [
            "DC=CORP,DC=COM"
        ]
    },

    
    "OU=FINANCE,DC=corp,DC=com": {
        "objectClass": [
            "top",
            "container"
        ],
        "CN=bmetzler,OU=FINANCE,DC=corp,DC=com": {
            "version": 5,
            "samaccountname": "bmetzler",
            "name": "Benjamin Metzler",
            "objectClass": [
                "top",
                "person",
                "organizationalPerson",
                "user"
            ],
            "description": "This is the user bmetzler",
            "canonicalName": "Some canonical name for bmetzler ..."
        },
        "CN=isoliman,OU=FINANCE,DC=corp,DC=com": {
            "version": 6,
            "samaccountname": "isoliman",
            "objectClass": [
                "top",
                "person",
                "organizationalPerson",
                "user"
            ],
            "description": "This is the user isoliman",
            "canonicalName": "Some canonical name for isoliman ..."
        },
        "OU=ACCOUNTING,OU=FINANCE,DC=corp,DC=com": {
            "CN=istach,OU=ACCOUNTING,OU=FINANCE,DC=corp,DC=com": {
                "version": "9",
                "samaccountname": "istach",
                "objectClass": [
                    "top",
                    "person",
                    "organizationalPerson",
                    "user"
                ],
                "description": "This is the user istach",
                "canonicalName": "Some canonical name for istach ..."
            },
            "OU=MadridOffices,OU=ACCOUNTING,OU=FINANCE,DC=corp,DC=com": {
                "CN=ccorea,OU=MadridOffices,OU=ACCOUNTING,OU=FINANCE,DC=corp,DC=com": {
                    "sAMAccountName": "ccorea",
                    "objectClass": [
                        "top",
                        "person",
                        "organizationalPerson",
                        "user"
                    ],
                    "description": [
                        "First description item",
                        "Second description item"
                    ],
                    "canonicalName": "Some canonical name for ccorea ..."
                }
            }
        }
      }
}

The LDAP server supports standard add, delete, modify and search operations (but no LDAP extensions). For operations that result in a modification of the LDAP data, the JSON data file is modified. When searching, the BASE, LEVEL and SUBTREE scopes can be specified. Standard filters are supported (present, less-or-equal, greater-or-equal, equal), while substring and extensible filters are not yet implemented – it is possible to specify if they should return True or False.

Below is a basic example of how to run the LDAP server. It is configured to use the SPNEGO Security Support Provider for authentication, as Windows LDAP clients will by default wrap their authentication communication in SPNEGO. The machine account used to simulate the LDAP server (and that resolves to our attacker machine) is SCAPY$ and is configured to use AES256 for Kerberos authentication. It is possible to calculate the associated AES key, that is provided to properly authenticate incoming clients. The LDAP server is then instantiated with the JSON data file that was just created.

$ cat srv.py
from scapy.layers.ldapserver import ldapserver
from scapy.layers.ntlm import NTLMSSP, NTLMSSP_DOMAIN, MD4le
from scapy.layers.spnego import SPNEGOSSP
from scapy.layers.kerberos import KerberosSSP, Key, EncryptionType

ssp = SPNEGOSSP([KerberosSSP(KEY=Key(
                                EncryptionType.AES256_CTS_HMAC_SHA1_96,
                                key=bytes.fromhex("c2e2bee9c44cdbb2be3d438be986e74ccb3d4e6b401e7e3cf6a83225cb841822"),
                        ),
                   SPN="ldap/scapy.corp.com")])

ldapserver(
        data="ldap.json",
        ssp=ssp,
        ACCEPT_EXTENSIBLE=False
    )

$ python3 srv.py 
Server LDAP_Server started listening on ('192.168.123.20', 389)

It is now possible to use standard Windows LDAP clients (such as the Powershell DirectorySearcher client) from a domain-joined machine to query the LDAP server. The screenshot below shows a standard search providing a scope, a filter, and the attributes to fetch.

Windows LDAP client interaction with scapy LDAP server
Windows LDAP client used to interact with the scapy LDAP server.

Running a network capture in parallel shows a standard LDAP exchange between the Windows LDAP client and the python server – a first rootDSE request, followed by a bind request (in which the user’s Kerberos ticket for the LDAP service of the scapy.corp.com server is integrated), and finally a search request to which the server responds. Note that the exchanges are in Integrity mode, which is the default for DirectorySearcher. The LDAP server will by default adapt to what is asked by the client (confidentiality, integrity or nothing). It is possible to require integrity or confidentiality modes in the server’s parameters.

Wireshark capture windows LDPA client
Wireshark capture of LDAP interaction between scapy LDAP server and Windows LDAP client.

The main focus was to allow the Windows LDAP clients to interact with the server. Some efforts were also made to allow some other Unix-based LDAP clients to work. The following is currently tested and supported:

  • ldapsearch using SPNEGO.

$ ldapsearch -H ldap://scapy.corp.com -Y GSS-SPNEGO -b "OU=FINANCE,DC=corp,DC=com" '(&(objectClass=user))' "*"
  • ldap3 using Kerberos and raw GSSAPI.

$ ldeep ldap -n -d corp.com -k -s ldap://scapy.corp.com --base 'OU=Finance,DC=corp,DC=com' search '(objectClass=user)'

Please note however that some Unix-based LDAP clients configurations will not work with the scapy LDAP server, due to some weird quirks. For instance, ldapsearch in any INTEGRITY modes while using GSSAPI and Kerberos places the krb5_sgn_cksum field at the end of the packet (after the GSSAPI payload). Similarly, when the ldap3 library asks for NTLM authentication, the bind request provides the flag 137, which is non-standard and not parsed by Wireshark. These specificities are mostly related to the authentication layer. They are not implemented in this current first version of the LDAP server since Unix-based LDAP clients were not a priority, but could be in the future.

d. LDAP server demonstration using OUned

As a demonstration, the following video shows the use of the LDAP server via OUned. More specifically, a user called anail has WriteGPLink permissions over an Organizational Unit SERVERS, containing a nested OU called PKI_SERVERS, which itself contains the AD01-PKI machine. Having compromised anail, OUned is used in order to compromise AD01-PKI by poisoning the gPLink attribute of the SERVERS OU, serving a malicious GPC via the LDAP server, and a malicious GPT via the SMB server.

 

Video file

 

3. Conclusion

Beyond Group Policy Objects exploitation, we do believe that there are still many attacks to be uncovered that rely on spoofing authenticated Active Directory services. This kind of attacks may have critical privilege escalation impacts in Active Directory environments – this was illustrated above through Group Policy Object exploitation, but also more recently by the Certighost flaw.

The motivation behind the presented research was thus to facilitate the discovery of this kind of attacks by providing robust and generic python implementations for LDAP and SMB services. If you are an Active Directory researcher, we hope that this could help you in your tests. In addition, the scripts are far from perfect, so do not hesitate to contribute and build upon it.