Commit 006cc3e5 authored by jan.koester's avatar jan.koester
Browse files

test

parent 577871f5
Loading
Loading
Loading
Loading
Loading
+4 −4
Changes for docs/ROADMAP.md: 4 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -59,12 +59,12 @@ The directory layer maps them onto AD objects and adds generic objects:
|---|---|---|
| 0 | Rename authdb -> FenrirDirectoryServer (`fenrirds`) | done |
| 1 | Core lib + crypto (MD4/MD5/HMAC-MD5/RC4/AES-CMAC/CFB8/SP800-108) + dsdb | |
| 2 | LDAP: all operations, controls (paged, SD flags, ...), rootDSE, schema entry, SASL GSSAPI/GSS-SPNEGO (Kerberos+NTLM, sign/seal), LDAPS, GC port, CLDAP netlogon ping | bind(simple+SASL GSSAPI)/search/add/modify/delete/modrdn + rootDSE done & wire-tested; SASL GSSAPI(raw)+GSS-SPNEGO binds, sign transport (SSF=1), CLDAP netlogon done. kinit/kvno/CLDAP interop-proven vs MIT+Samba. TODO: validate GSS-SPNEGO+sign vs Samba ldbsearch (harness flaky this round), seal(SSF=2), paged-results, schema subentry, LDAPS, GC port |
| 2 | LDAP: all operations, controls (paged, SD flags, ...), rootDSE, schema entry, SASL GSSAPI/GSS-SPNEGO (Kerberos+NTLM, sign/seal), LDAPS, GC port, CLDAP netlogon ping | bind(simple+SASL GSSAPI)/search/add/modify/delete/modrdn + rootDSE done & wire-tested; SASL GSSAPI(raw)+GSS-SPNEGO binds, sign transport (SSF=1), CLDAP netlogon done. kinit/kvno/CLDAP interop-proven vs MIT+Samba. Paged results (1.2.840.113556.1.4.319, cookie per session, page size capped at 1000), Global Catalog on 3268 (forest-wide, read-only, partial attribute set from the schema GC flag), LDAPS on 636 and GC over TLS on 3269 (server certificate /SSL/CERT + /SSL/KEY) done, tested with Samba ldbsearch. TODO: schema subentry |
| 3 | LDIF import/export (RFC 2849): library, `fenrirdsctl`, HTTP API, admin UI | |
| 4 | DNS server: AD zones and SRV records, dynamic update (unsigned + GSS-TSIG), forwarding | UDP+TCP server, SRV/A/NS/SOA for domain + _msdcs, generated from directory + configured DC host/IP; wire-tested (dig-style). TODO: directory-backed dnsNode records, dynamic update (RFC2136 + GSS-TSIG), forwarding, AAAA, reverse zones |
| 5 | Kerberos for AD: PAC (logon info, UPN/DNS info, signatures), lookups by sAMAccountName/SPN/UPN, AD salts, RC4-HMAC, kpasswd | KDC on dsdb::Directory (lookups SPN/sAMAccountName($)/UPN, keys from Secrets, krbtgt=TGS key, AD salts). PAC (logon info NDR, client info, UPN/DNS, server+KDC HMAC-SHA1-96 signatures, S-1-22-3 UUID SID in extra SIDs) in TGT + service ticket. AS+TGS wire-tested end to end. TODO: PAC verify on TGS, RC4-HMAC preauth, kpasswd, S4U |
| 6 | NTLM server, DCE/RPC (NDR, auth: NTLMSSP/Kerberos/netlogon schannel), endpoint mapper | |
| 7 | RPC services: lsarpc, samr, netlogon, drsuapi (bind/crack names), srvsvc/wkssvc | |
| 5 | Kerberos for AD: PAC (logon info, UPN/DNS info, signatures), lookups by sAMAccountName/SPN/UPN, AD salts, RC4-HMAC, kpasswd | KDC on dsdb::Directory (lookups SPN/sAMAccountName($)/UPN, keys from Secrets, krbtgt=TGS key, AD salts). PAC (logon info NDR, client info, UPN/DNS, server+KDC HMAC-SHA1-96 signatures, S-1-22-3 UUID SID in extra SIDs, nested groups) in TGT + service ticket. AS+TGS wire-tested end to end. kpasswd (464 TCP+UDP, RFC 3244 change + set) and "must change at next logon" (pwdLastSet=0 -> KEY_EXPIRED) done, tested with MIT/Heimdal. TODO: PAC verify on TGS, RC4-HMAC preauth, S4U |
| 6 | NTLM server, DCE/RPC (NDR, auth: NTLMSSP/Kerberos/netlogon schannel), endpoint mapper | DCE/RPC over SMB pipes and TCP (49152), endpoint mapper (135, ept_map), Kerberos/SPNEGO auth DCE style with sealing, netlogon schannel (AES sign+seal); tested with Samba clients |
| 7 | RPC services: lsarpc, samr, netlogon, drsuapi (bind/crack names), srvsvc/wkssvc | netlogon: secure channel, GetCapabilities, GetDomainInfo, ServerPasswordSet2, DsrGetDcName*, NTLM pass-through (LogonSamLogon/Ex/WithFlags, interactive + network, validation 2/3/6) for Windows clients and member servers (proven with adconnect incl. EPM lookup); drsuapi DsBind/DsCrackNames. TODO: srvsvc/wkssvc, SAMR password change fallback |
| 8 | SMB2/3 server: IPC$ pipes, NETLOGON/SYSVOL shares, signing | |
| 9 | Packaging for FenrirOS (spec, syslite unit, container), admin UI for AD objects/DNS/LDIF | |

+88 −0
Changes for src/rpc/samr.cpp: 88 added lines, 0 removed lines.
Original line number Diff line number Diff line
#include <cstring>
#include <iostream>
#include <map>
#include <mutex>

#include <netplus/crypto/des.h>

#include "backend.h"
#include "crypto/crypto.h"
#include "domains.h"
@@ -29,8 +32,37 @@ namespace {
        SAMR_GETUSERPWINFO = 44, SAMR_CONNECT = 0, SAMR_CONNECT2 = 57, SAMR_CONNECT4 = 62,
        SAMR_CONNECT5 = 64, SAMR_CREATEUSER2 = 50, SAMR_SETUSERINFO2 = 58,
        SAMR_GETDOMPWINFO = 56, SAMR_RIDTOSID = 65, SAMR_QUERYDISPLAYINFO = 40,
        SAMR_CHANGEPASSWORDUSER2 = 55, SAMR_CHANGEPASSWORDUSER3 = 63,
    };

    constexpr uint32_t ST_WRONG_PASSWORD = 0xC000006A;
    constexpr uint32_t ST_PASSWORD_RESTRICTION = 0xC000006C;

    /* 7 key bytes -> a DES key (parity bits unused) */
    std::vector<uint8_t> desKey7(const uint8_t *s) {
        std::vector<uint8_t> k(8);
        k[0] = s[0] >> 1;
        k[1] = uint8_t(((s[0] & 0x01) << 6) | (s[1] >> 2));
        k[2] = uint8_t(((s[1] & 0x03) << 5) | (s[2] >> 3));
        k[3] = uint8_t(((s[2] & 0x07) << 4) | (s[3] >> 4));
        k[4] = uint8_t(((s[3] & 0x0F) << 3) | (s[4] >> 5));
        k[5] = uint8_t(((s[4] & 0x1F) << 2) | (s[5] >> 6));
        k[6] = uint8_t(((s[5] & 0x3F) << 1) | (s[6] >> 7));
        k[7] = s[6] & 0x7F;
        for (auto &b : k) b = uint8_t(b << 1);
        return k;
    }
    /* E(data16) under a 14-byte key: two DES blocks (MS-SAMR 2.2.11.1.1) */
    crypto::Bytes des112(const crypto::Bytes &key, const crypto::Bytes &data) {
        crypto::Bytes out;
        for (int i = 0; i < 2; ++i) {
            netplus::des d(desKey7(key.data() + 7 * i));
            auto blk = d.encrypt(std::vector<uint8_t>(data.begin() + 8 * i, data.begin() + 8 * i + 8));
            out.insert(out.end(), blk.begin(), blk.end());
        }
        return out;
    }

    enum HKind { H_CONNECT, H_DOMAIN, H_USER, H_BUILTIN };
    struct HObj { HKind kind; uint32_t rid = 0; uuid::uuid guid; };

@@ -392,6 +424,62 @@ DispatchResult samrDispatch(uint16_t opnum, const std::vector<uint8_t> &stub,
            NdrPush p; p.u32(ST_OK);
            return ok(p.buf);
        }
        case SAMR_CHANGEPASSWORDUSER2:
        case SAMR_CHANGEPASSWORDUSER3: {
            /* server [unique] lsa_String, account [ref] lsa_String,
             * nt_password [unique] 516, nt_verifier [unique] 16, lm_change,
             * lm_password [unique] 516, lm_verifier [unique] 16 (, password3) */
            NdrPull in(stub);
            if (in.u32()) readName(in);
            std::string account = readName(in);
            crypto::Bytes ntPw, ntVerifier;
            if (in.u32()) ntPw = in.takeVec(516);
            if (in.u32()) ntVerifier = in.takeVec(16);
            auto reply = [&](uint32_t st) {
                NdrPush p;
                if (opnum == SAMR_CHANGEPASSWORDUSER3) { p.ref(false); p.ref(false); }   /* dominfo, reject */
                p.u32(st);
                return ok(p.buf, st);
            };
            if (ntPw.size() != 516 || ntVerifier.size() != 16) return reply(ST_INVALID);
            Dom d;
            if (!openDom(d)) return reply(ST_NO_SUCH_USER);
            dsdb::Directory dir(*d.bck, d.meta);
            auto e = dir.bySamAccountName(account);
            if (!e) return reply(ST_NO_SUCH_USER);
            dsdb::Secrets old = dir.secretsOf(e->objectGuid);
            if (old.ntHash.size() != 16) return reply(ST_WRONG_PASSWORD);
            crypto::Bytes oldNt(old.ntHash.begin(), old.ntHash.end());
            /* the new password, RC4 with the old NT hash */
            crypto::Bytes plain = crypto::rc4(oldNt, ntPw);
            uint32_t len = uint32_t(plain[512]) | plain[513] << 8 | plain[514] << 16 | uint32_t(plain[515]) << 24;
            if (len == 0 || len > 512 || (len & 1)) {
                std::cerr << "[SAMR] ChangePassword '" << account << "': wrong old password" << std::endl;
                return reply(ST_WRONG_PASSWORD);
            }
            crypto::Bytes raw(plain.begin() + (512 - len), plain.begin() + 512);
            crypto::Bytes newNt = crypto::md4(raw);
            /* the verifier: the old NT hash encrypted with the new one */
            if (!crypto::equal(des112(newNt, oldNt), ntVerifier)) {
                std::cerr << "[SAMR] ChangePassword '" << account << "': verifier mismatch" << std::endl;
                return reply(ST_WRONG_PASSWORD);
            }
            int64_t minLen = 7;
            if (auto head = dir.byDn(d.meta.baseDn)) minLen = dsdb::intValue(head->attrs, "minPwdLength", 7);
            if (int64_t(len / 2) < minLen) return reply(ST_PASSWORD_RESTRICTION);
            std::string pw = crypto::fromUtf16le(raw);
            bool computer = dsdb::hasValue(e->attrs, "objectClass", "computer");
            std::string salt = dsdb::kerberosSalt(computer ? dsdb::PrincipalKind::Computer : dsdb::PrincipalKind::User,
                                                  d.meta.realm, e->str("sAMAccountName"), d.meta.dnsName);
            dsdb::Secrets sec = dsdb::deriveSecrets(pw, salt, old.kvno);
            sec.ntHash = crypto::str(newNt);
            dsdb::AttrMap a;
            dsdb::setInt(a, "pwdLastSet", sec.pwdLastSet);
            dsdb::setInt(a, "msDS-KeyVersionNumber", sec.kvno);
            try { dir.modify(e->objectGuid, a, &sec, true); } catch (std::exception &) { return reply(ST_INVALID); }
            std::cerr << "[SAMR] ChangePassword '" << account << "' -> OK" << std::endl;
            return reply(ST_OK);
        }
        case SAMR_GETUSERPWINFO: {
            NdrPush p;
            p.u16(7);         /* min password length */