
Accès natif C++ à Active Directory via ADWS, pas de .NET, pas de WCF, pas de pile HTTP.
Accès C++ natif à Active Directory via ADWS, pas de .NET, pas de WCF, pas de pile HTTP.
BridgeHead est une bibliothèque statique C++20 implémentant la pile complète du protocole Active Directory Web Services (ADWS) directement sur TCP. Nommé d'après le serveur bridgehead AD, la passerelle par laquelle circule le trafic d'annuaire, il donne à votre code C++ le même accès bas niveau au port 9389 que les commandes PowerShell Get-ADUser et Get-ADComputer utilisent en interne.
Les couches de transport enveloppent celle du dessous via l'interface commune bridgehead::transport::IByteStream. NbfseCodec est un utilitaire de codec appelé par AdwsClient pour encoder/décoder les messages SOAP avant et après le tramage :```
AdwsClient WS-Enumeration + WS-Transfer [MS-ADDM]
├── NbfseCodec .NET Binary Format for SOAP [MC-NBFSE] (encode/decode)
└── NmfFramer .NET Message Framing [MC-NMF] (send/receive frames)
└── NnsSession .NET NegotiateStream [MS-NNS]
└── TcpSocket raw TCP/IP
Le point d'entrée pour les consommateurs est `bridgehead::adws::AdwsClient`.
---
## Démarrage rapide
### Interroger, énumérer tous les utilisateurs```cpp
#include "bridgehead/adws/AdwsClient.hpp"
// NTLM (works on any host)
auto client = bridgehead::adws::AdwsClient::EnumerationClient(
"192.168.1.10", // DC IP or hostname
"DC01.corp.local", // DC FQDN, used in NMF Via header and Kerberos SPN
"CORP", // NetBIOS domain name
"Administrator", // username
"Passw0rd" // password
);
// Kerberos (username hidden on the wire; requires DC reachable on port 88)
auto client = bridgehead::adws::AdwsClient::EnumerationClient(
"192.168.1.10", "DC01.corp.local", "CORP",
"Administrator", "Passw0rd",
bridgehead::adws::AuthPackage::Kerberos
);
auto users = client.Query(
"(objectClass=user)",
{"sAMAccountName", "distinguishedName", "memberOf"}
);
for (auto& obj : users)
std::cout << obj.FirstValue("sAMAccountName") << '\n';
client.Enumerate( "(objectClass=computer)", {"dNSHostName", "operatingSystem"}, "", // empty = domain root base DN [](const bridgehead::adws::LdapObject& obj) { std::cout << obj.FirstValue("dNSHostName") << '\n'; return true; // return false to stop early (sends wsen:Release) } );
### Portée et pagination```cpp
// OneLevel scope, 50 objects per Pull round-trip
client.Query(
"(objectClass=user)",
{"sAMAccountName"},
"OU=Admins,DC=corp,DC=local",
50,
bridgehead::adws::SearchScope::OneLevel
);
auto objs = client.Query("(objectClass=user)", {"objectGUID", "objectSid"}); for (auto& obj : objs) { if (auto* b = obj.FirstBytes("objectGUID")) std::cout << bridgehead::adws::ParseGuid(b) << '\n'; // {XXXXXXXX-...} if (auto b = obj.FirstBytes("objectSid")) std::cout << bridgehead::adws::ParseSid(*b) << '\n'; // S-1-5-... }
### Décodage du descripteur de sécurité```cpp
#include "bridgehead/adws/SecurityDescriptor.hpp"
auto objs = client.Query("(objectClass=user)", {"nTSecurityDescriptor"});
if (auto* raw = objs[0].FirstBytes("nTSecurityDescriptor")) {
auto sd = bridgehead::adws::ParseSecurityDescriptor(*raw);
std::cout << "Owner: " << sd.ownerSid << '\n';
for (auto& ace : sd.dacl.aces)
std::cout << " type=" << (int)ace.type
<< " mask=0x" << std::hex << ace.mask
<< " sid=" << ace.sid << '\n';
}
auto rc = bridgehead::adws::AdwsClient::ResourceClient( "192.168.1.10", "DC01.corp.local", "CORP", "Administrator", "Passw0rd");
// Modify attributes rc.Put("CN=Alice,OU=Users,DC=corp,DC=local", { {"description", {"managed by bridgehead"}}, {"telephoneNumber", {"555-1234"}}, });
// Clear an attribute (both values and bytes empty = delete) rc.Put("CN=Alice,OU=Users,DC=corp,DC=local", { {"telephoneNumber", {}}, });
// Read back auto obj = rc.Get("CN=Alice,OU=Users,DC=corp,DC=local", {"description", "telephoneNumber"});
// Delete object rc.Delete("CN=TempUser,OU=Users,DC=corp,DC=local");
### Types de modification LDAP, Ajouter / Remplacer / Supprimer
`Put` par défaut utilise `Replace` (remplace toutes les valeurs existantes). Utilisez `ModifyOperation` pour un contrôle fin sur les attributs à valeurs multiples :```cpp
using bridgehead::adws::LdapModification;
using bridgehead::adws::ModifyOperation;
rc.Put("CN=Alice,OU=Users,DC=corp,DC=local", {
// Append a value to an existing multi-valued attribute
LdapModification{"otherTelephone", {"555-9999"}, {}, ModifyOperation::Add},
// Remove one specific value (leave others intact)
LdapModification{"otherTelephone", {"555-0000"}, {}, ModifyOperation::Delete},
// Replace is the default, explicit here for clarity
LdapModification{"description", {"updated"}, {}, ModifyOperation::Replace},
});
// Move to a different OU rc.Move( "CN=Alice,OU=OldOU,DC=corp,DC=local", // current DN "CN=Alice,OU=NewOU,DC=corp,DC=local" // new DN );
// Rename in place (same parent, new CN) rc.Move( "CN=Alice,OU=Users,DC=corp,DC=local", "CN=AliceSmith,OU=Users,DC=corp,DC=local" );
### Écrire des attributs binaires
Fournissez des valeurs binaires dans le champ `bytes` de `LdapModification`. Ils sont encodés en base64 sur le fil automatiquement :```cpp
std::vector<uint8_t> thumbnail = loadFile("photo.jpg");
rc.Put("CN=Alice,OU=Users,DC=corp,DC=local", {
LdapModification{"thumbnailPhoto", {}, {thumbnail}},
});
auto rf = bridgehead::adws::AdwsClient::ResourceFactoryClient( "192.168.1.10", "DC01.corp.local", "CORP", "Administrator", "Passw0rd");
rf.Create( "CN=NewUser,OU=Users,DC=corp,DC=local", "user", {{"sAMAccountName", {"newuser"}}, {"userAccountControl", {"512"}}} );
### Opérations asynchrones```cpp
#include "bridgehead/adws/AdwsClientAsync.hpp"
auto ac = bridgehead::adws::AdwsClientAsync::EnumerationClient(
"192.168.1.10", "DC01.corp.local", "CORP", "Administrator", "Passw0rd");
auto future = ac.QueryAsync("(objectClass=user)", {"sAMAccountName"});
// ... do other work while the query runs ...
auto users = future.get(); // blocks until complete; re-throws any exception
Timeout sur un DC lent:```cpp if (future.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) { // query is still running }
Toutes les opérations ont des variantes asynchrones : `QueryAsync`, `EnumerateAsync`, `GetAsync`, `PutAsync`, `DeleteAsync`, `CreateAsync`, `MoveAsync`.
### Pool de connexions (charges de travail haute fréquence / multi-threads)```cpp
#include "bridgehead/adws/AdwsClientPool.hpp"
// Enumeration pool, Query / Enumerate
bridgehead::adws::AdwsClientPool pool({
.host = "192.168.1.10", .fqdn = "DC01.corp.local",
.domain = "CORP", .username = "Administrator", .password = "Passw0rd",
.maxSize = 4, // up to 4 concurrent authenticated sessions
});
auto users = pool.Query("(objectClass=user)", {"sAMAccountName"});
// Resource pool, Get / Put / Delete
bridgehead::adws::AdwsClientPool resPool({
.host = "192.168.1.10", .fqdn = "DC01.corp.local",
.domain = "CORP", .username = "Administrator", .password = "Passw0rd",
.maxSize = 4,
.endpoint = bridgehead::adws::PoolEndpoint::Resource,
});
auto obj = resPool.Get("CN=Alice,OU=Users,DC=corp,DC=local", {"mail"});
resPool.Put("CN=Alice,OU=Users,DC=corp,DC=local", {{"mail", {"[email protected]"}}});
// ResourceFactory pool, Create
bridgehead::adws::AdwsClientPool rfPool({
.host = "192.168.1.10", .fqdn = "DC01.corp.local",
.domain = "CORP", .username = "Administrator", .password = "Passw0rd",
.endpoint = bridgehead::adws::PoolEndpoint::ResourceFactory,
});
rfPool.Create("CN=NewUser,OU=Users,DC=corp,DC=local", "user",
{{"sAMAccountName", {"newuser"}}});
Tous les en-têtes publics se trouvent dans include/bridgehead/. La documentation complète de l'API peut être générée avec Doxygen (voir Build).
bridgehead::adws::AdwsClientbridgehead::adws::AdwsClientAsyncWrapper asynchrone basé sur std::future. Mêmes méthodes de fabrique que AdwsClient ; chaque opération retourne un std::future<T> :```cpp
std::future<std::vector> f = ac.QueryAsync(...);
std::future e = ac.EnumerateAsync(filter, attrs, baseDN, callback);
std::future g = ac.GetAsync(...);
std::future h = ac.PutAsync(...);
std::future i = ac.DeleteAsync(...);
std::future j = ac.CreateAsync(...);
std::future k = ac.MoveAsync(...);
Pour les requêtes concurrentes sur plusieurs connexions, utilisez `AdwsClientPool`.
### `bridgehead::adws::AdwsClientPool`
Pool thread-safe de sessions pré-authentifiées. Les connexions sont créées paresseusement et réutilisées entre les appels.```cpp
pool.IdleCount(); // sessions currently idle
pool.TotalCount(); // idle + checked-out
pool.MaxSize(); // configured maximum pool size
pool.Move(dn, newDn); // Move/rename (Resource pool)
enum class SearchScope { Base, OneLevel, Subtree /default/ };
### `bridgehead::adws::LdapObject` / `LdapAttribute````cpp
struct LdapAttribute {
std::string name;
std::string syntax; // LdapSyntax OID, empty if absent
std::vector<std::string> values; // text values (raw base64 for binary)
std::vector<std::vector<uint8_t>> bytes; // decoded binary; parallel to values
};
struct LdapObject {
std::vector<LdapAttribute> attributes; // all returned attributes
const LdapAttribute* Find(const std::string& name) const; // case-insensitive
std::string FirstValue(const std::string& name) const;
const std::vector<uint8_t>* FirstBytes(const std::string& name) const;
};
std::string ParseGuid(const std::vector<uint8_t>& bytes); // → "{XXXXXXXX-XXXX-...}" std::string ParseSid (const std::vector<uint8_t>& bytes); // → "S-1-5-..."
### `FilterValue` et les aides de construction de filtres
`FilterValue` est un wrapper de type sécurisé qui échappe les métacaractères de la RFC 4515 §3 (`\`, `*`, `(`, `)`, NUL) lors de la construction. Utilisez-le avec les aides de construction pour rendre l'injection LDAP structurellement impossible :```cpp
// UNSAFE, raw string concatenation, easy to forget escaping
client.Query("(sAMAccountName=" + username + ")", ...);
// SAFE, FilterValue escapes on construction; FilterEq composes the assertion
FilterValue user = username;
client.Query(FilterEq("sAMAccountName", user), ...);
// Compose complex filters
client.Query(
FilterAnd({
FilterEq("objectClass", FilterValue::Raw("user")), // Raw() for safe literals
FilterOr({
FilterEq("sAMAccountName", user),
FilterEq("mail", FilterValue(email)),
}),
FilterNot(FilterPresent("userAccountControl")),
}), {"sAMAccountName", "mail"}
);
EscapeLdapFilter(str) est toujours disponible comme primitive de bas niveau lorsque vous devez construire manuellement des chaînes de filtre.
DnValue et les assistants de construction DNMême modèle que FilterValue, mais pour les valeurs RDN de Distinguished Name (RFC 4514 §2.4). Échappe ,, +, ", \, <, >, ;, NUL, et les # et espaces en début/fin :```cpp
// UNSAFE, comma injection breaks the DN structure
rc.Get("CN=" + username + ",OU=Users,DC=corp,DC=local", attrs);
// SAFE DnValue cn = username; // auto-escaped auto dn = BuildDn({DnAttr("CN", cn), "OU=Users", "DC=corp", "DC=local"}); rc.Get(dn, attrs);
// DnValue::Raw() for values you control auto dn2 = BuildDn({DnAttr("CN", DnValue::Raw("Service Account")), "OU=SvcAccounts", "DC=corp", "DC=local"});
### `bridgehead::adws::SecurityDescriptor````cpp
SecurityDescriptor ParseSecurityDescriptor(const std::vector<uint8_t>& bytes);
struct SecurityDescriptor {
uint16_t control; // SdControl::* flags
std::string ownerSid;
std::string groupSid;
bool hasDacl; Acl dacl;
bool hasSacl; Acl sacl;
};
struct Acl { std::vector<Ace> aces; };
struct Ace {
uint8_t type; // AceType::*
uint8_t flags; // AceFlags::*
uint32_t mask;
std::string sid;
std::string objectType; // GUID string, object ACEs only
std::string inheritedObjectType; // GUID string, object ACEs only
};
Constant namespaces: AceType::*, AceFlags::*, SdControl::*.
bridgehead::ConnectionError / AuthenticationError / ProtocolErrorDéfinie dans include/bridgehead/Exceptions.hpp (incluse transitivement par AdwsClient.hpp). Toutes trois héritent de std::runtime_error:
| Type | Levée lors de |
|---|---|
ConnectionError | Échec au niveau TCP : refusé, délai d'attente dépassé, erreur E/S |
AuthenticationError | La négociation NTLM/Kerberos échoue |
ProtocolError |
auto client = bridgehead::adws::AdwsClient::EnumerationClient(...);
auto users = client.Query(...);
} catch (const bridgehead::ConnectionError& e) { // unreachable DC, wrong port, timeout } catch (const bridgehead::AuthenticationError& e) { // bad credentials, KDC unreachable } catch (const bridgehead::ProtocolError& e) { // unexpected server response } catch (const bridgehead::adws::SoapFault& e) { // DC returned a SOAP fault (e.g. invalid filter) } // or coarse-grained: // } catch (const std::runtime_error& e) { ... }
### `bridgehead::adws::SoapFault`
Lancée sur toute réponse `s:Fault` du DC au lieu d'un simple `std::runtime_error` :```cpp
struct SoapFault : std::runtime_error {
std::string code; // e.g. "Sender"
std::string subcode; // e.g. "InvalidEnumerationContext", empty if absent
std::string reason; // human-readable text
};
bridgehead::adws::LdapModificationUtilisé par Put et Create :```cpp
enum class ModifyOperation { Replace /default/, Add, Delete };
struct LdapModification { std::string name; std::vectorstd::string values; // text values std::vector<std::vector<uint8_t>> bytes; // binary values (base64-encoded on the wire) ModifyOperation operation = ModifyOperation::Replace; };
When both `values` and `bytes` are empty, the attribute is cleared/deleted regardless of `operation`.
### `bridgehead::adws::ProtocolLimits`
Optional last parameter on all factory methods, `AdwsClientAsync` factories, and `AdwsClientPool::Config::limits`. All fields have safe defaults, only change them if you have specific requirements:```cpp
struct ProtocolLimits {
uint32_t nmfMaxFrameBytes = 64 * 1024 * 1024; // max NMF frame (default 64 MiB)
uint32_t nnsMaxPayloadBytes = 16 * 1024 * 1024; // max NNS packet (default 16 MB)
int tcpKeepaliveIdleSec = 60; // idle seconds before first probe
int tcpKeepaliveIntervalSec = 10; // seconds between probes
int tcpKeepaliveProbeCount = 5; // probes before declaring dead (POSIX only)
};
Augmentez nmfMaxFrameBytes / nnsMaxPayloadBytes uniquement si vous lisez des objets avec des attributs binaires anormalement grands (par ex. nTSecurityDescriptor avec de nombreuses ACE, ou thumbnailPhoto grand). Réduisez les champs keepalive dans les environnements cloud/NAT où les connexions inactives sont interrompues de manière agressive.
Assistants de constructeurs nommés (fabriques statiques en ligne) :```cpp LdapModification::Replace(name, values) // Replace with text values (default) LdapModification::ReplaceBinary(name, bytes) // Replace with binary values LdapModification::Append(name, values) // Add to multi-valued attribute LdapModification::Remove(name, values={}) // Remove specific values (or all) LdapModification::Clear(name) // Delete the attribute entirely
---
## Compilation
**Exigences :** CMake 3.25+ et un compilateur compatible C++20 (MSVC 2022+, GCC 12+, Clang 15+).```bash
# Configure and build (unit tests only, no live AD needed)
cmake -B build -A x64
cmake --build build --config Release
# Run unit tests
ctest --test-dir build -C Release --output-on-failure
# With integration tests (requires a live Domain Controller)
cmake -B build -A x64 -DBRIDGEHEAD_INTEGRATION_TESTS=ON
cmake --build build --config Release
./build/tests/Release/bridgehead_integration_tests.exe
# With CLI tool (adws_list, browse AD objects from the command line)
cmake -B build -A x64 -DBRIDGEHEAD_BUILD_TOOLS=ON
cmake --build build --config Release --target adws_list
./build/tools/Release/adws_list.exe --host <dc-ip> --fqdn <dc-fqdn> --domain <domain> --user <user>
# Generate API reference docs (requires Doxygen)
cmake --build build --target docs
# or directly:
doxygen Doxyfile
# Output: docs/doxygen/html/index.html
add_subdirectory(bridgehead) target_link_libraries(my_target PRIVATE bridgehead::bridgehead)
### Option B, paquet installé (`find_package`)```bash
cmake --install build --prefix /usr/local # or any install prefix
_ryuk, ryuk, [email protected]$finish->startwininit.exe et winlogon.exe[screenshot], [keylog], disableAV, WorkingDirectory/upload et /downloadReadme.txt%username%/covid/mode, block, pid, [screenshot], [stealer]/api/blog/DTrump4POTUS`pugixml`, `ws2_32` et `secur32` (Windows) / `gssapi_krb5` (Linux/macOS) sont tous inclus transitivement.
---
## Support de plateforme
| Plateforme | Backend d'authentification | Statut |
|----------|-------------|--------|
| Windows | SSPI (`secur32.dll`), NTLM et Kerberos | **Fonctionne** |
| Linux / macOS | GSSAPI (`libgssapi_krb5`), SPNEGO / Kerberos | Compilé ; pas encore testé en intégration |
Les deux `AuthPackage::Ntlm` et `AuthPackage::Kerberos` fonctionnent sous Windows. Kerberos masque le nom d'utilisateur sur le réseau et est préféré en production ; NTLM est le recours lorsque le KDC (port 88) est inaccessible.
Sous Linux/macOS, le backend GSSAPI utilise SPNEGO avec Kerberos. Pour le support NTLM, installez le plugin `gss-ntlmssp` :```bash
apt install libgss-ntlmssp # Debian / Ubuntu
dnf install gssntlmssp # Fedora / RHEL
Sans le plugin, le serveur négociera Kerberos. Les identifiants explicites (utilisateur/mot de passe) utilisent gss_acquire_cred_with_password (MIT Kerberos 1.9+) ; passez des chaînes vides pour utiliser le cache d'identifiants par défaut (kinit). Le chemin GSSAPI se compile proprement et est correct par conception, mais n'a pas encore été validé de bout en bout avec un contrôleur de domaine en direct ; considérez le support Linux/macOS comme bêta.
Note sur le chiffrement : ADWS sur le port 9389 est chiffré au niveau de la couche NNS (clé de session NTLM ou AES-256 Kerberos), pas TLS. C'est intentionnel, le DC ne propose pas de TLS sur ce port. Si une authentification du serveur basée sur des certificats est requise, LDAPS (port 636) est l'alternative.
Récupérées automatiquement lors de la configuration via cmake/Dependencies.cmake, aucune installation manuelle nécessaire :
| Bibliothèque | Version | Objectif |
|---|---|---|
| Catch2 | v3.5.2 | Framework de tests unitaires (cibles de test uniquement) |
| pugixml | v1.14 | DOM XML (analyse des réponses ADWS uniquement) |
Pas d'OpenSSL. Pas d'Asio. Pas de Boost.
NTLM sur Linux/macOS nécessite le plugin GSSAPI gss-ntlmssp (voir Support de plateforme). Sans lui, le serveur négociera Kerberos à la place. NTLM sur Windows fonctionne nativement via SSPI.
DOM pugixml, chaque réponse SOAP est entièrement analysée dans un arbre XML en mémoire avant que les résultats ne soient renvoyés. Ceci est limité par la taille de page maxElements (256 objets par Pull par défaut), donc l'ensemble complet des résultats n'est jamais conservé en mémoire à la fois. La mémoire ne devient un problème qu'avec de très grandes tailles de page ou des objets portant de grands attributs binaires (par exemple nTSecurityDescriptor sur des objets avec de nombreuses ACEs). Une approche SAX/streaming éliminerait cela mais n'est pas actuellement prévue.
Toutes les spécifications sont disponibles dans la documentation des spécifications ouvertes de Microsoft.
Remerciements particuliers à IBM X-Force et au projet SoaPy pour leur inspiration, leurs recherches et leur travail partagé publiquement qui ont contribué à éclairer ce projet. Je souhaite également remercier Logan Goins, le créateur de SoaPy chez IBM X-Force Red, pour l'effort et la perspicacité qui ont rendu ce travail possible.
Voir LICENSE pour plus de détails.
| Méthode | Point de terminaison | Description |
|---|
EnumerationClient(host, fqdn, domain, user, pass [, auth, timeoutMs, opTimeoutMs, limits]) | /Enumeration | Fabrique, connexion et authentification |
ResourceClient(...) | /Resource | Fabrique pour Get / Put / Delete / Move |
ResourceFactoryClient(...) | /ResourceFactory | Fabrique pour Create |
Query(filter, attrs [, baseDN, maxElems, scope]) | Énumération | Collecte tous les résultats dans un vector |
Enumerate(filter, attrs, baseDN, callback [, maxElems, scope]) | Énumération | Diffuse les résultats via un callback |
Get(dn, attrs) | Ressource | Lit les attributs d'un seul objet |
Put(dn, modifications) | Ressource | Modifie les attributs |
Delete(dn) | Ressource | Supprime l'objet |
Create(dn, objectClass, attrs) | Fabrique de ressources | Crée un nouvel objet |
Move(dn, newDn) | Ressource | Déplace ou renomme un objet existant |
| Réponse serveur malformée à n'importe quelle couche de protocole |
| try { |
| Option | Par défaut | Description |
|---|
BRIDGEHEAD_BUILD_TESTS | ON | Compiler la suite de tests unitaires |
BRIDGEHEAD_INTEGRATION_TESTS | OFF | Compiler les tests d'intégration (nécessite un DC actif) |
BRIDGEHEAD_BUILD_TOOLS | OFF | Compiler l'outil CLI adws_list |