├── README.md └── fgpp.py /README.md: -------------------------------------------------------------------------------- 1 | # GetFGPP 2 | 3 | ## Get Fine Grained Password Policy 4 | 5 | Required: **pip3 install python-dateutil** 6 | 7 | ``` 8 | python3 fgpp.py -u Administrator -p Password -d n00py.local 9 | Attmepting to connect... 10 | 11 | Searching for Policy objects... 12 | 2 FGPP Objects found! 13 | 14 | Attempting to Enumerate objects with an applied policy... 15 | 16 | Object: CN=Domain Admins,CN=Users,DC=n00py,DC=local 17 | Applied Policy: CN=DA Policy,CN=Password Settings Container,CN=System,DC=n00py,DC=local 18 | 19 | Object: CN=Domain Users,CN=Users,DC=n00py,DC=local 20 | Applied Policy: CN=DU Policy,CN=Password Settings Container,CN=System,DC=n00py,DC=local 21 | 22 | Attempting to enumerate details... 23 | 24 | Policy Name: DU Policy 25 | Description: This Policy Applies to Domain Users 26 | Policy Applies to: CN=Domain Users,CN=Users,DC=n00py,DC=local 27 | Minimum Password Length: 6 28 | Lockout Threshold: 100 29 | Observation Window: 0 days 0 hours 30 minutes 0 seconds 30 | Lockout Duration: 0 days 0 hours 30 minutes 0 seconds 31 | Complexity Enabled: False 32 | Minimum Password Age 1 days 0 hours 0 minutes 0 seconds 33 | Maximum Password Age: 42 days 0 hours 0 minutes 0 seconds 34 | Reversible Encryption: True 35 | Precedence: 1 36 | 37 | Policy Name: DA Policy 38 | Description: This policy applied to Domain Admins 39 | Policy Applies to: CN=Domain Admins,CN=Users,DC=n00py,DC=local 40 | Minimum Password Length: 14 41 | Lockout Threshold: 3 42 | Observation Window: 0 days 0 hours 30 minutes 0 seconds 43 | Lockout Duration: 10675199 days 2 hours 48 minutes 5 seconds 44 | Complexity Enabled: True 45 | Minimum Password Age 1 days 0 hours 0 minutes 0 seconds 46 | Maximum Password Age: 42 days 0 hours 0 minutes 0 seconds 47 | Reversible Encryption: False 48 | Precedence: 2 49 | 50 | 51 | python3 fgpp.py -u lowpriv -p Password -d n00py.local 52 | Attmepting to connect... 53 | 54 | Searching for Policy objects... 55 | 2 FGPP Objects found! 56 | 57 | Attempting to Enumerate objects with an applied policy... 58 | 59 | Object: CN=Domain Admins,CN=Users,DC=n00py,DC=local 60 | Applied Policy: CN=DA Policy,CN=Password Settings Container,CN=System,DC=n00py,DC=local 61 | 62 | Object: CN=Domain Users,CN=Users,DC=n00py,DC=local 63 | Applied Policy: CN=DU Policy,CN=Password Settings Container,CN=System,DC=n00py,DC=local 64 | 65 | Attempting to enumerate details... 66 | 67 | Could not enumerate details, you likely do not have the privileges to do so! 68 | ``` 69 | 70 | You must have have read access to the FGPP Container and FGPP Objects to enumerate this. By default, only admins have this right. 71 | -------------------------------------------------------------------------------- /fgpp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import ssl 4 | import time 5 | from ldap3 import Server, Connection, NTLM, SASL, GSSAPI, ALL, SUBTREE, Tls 6 | from ldap3.core.exceptions import LDAPException 7 | from dateutil.relativedelta import relativedelta as rd 8 | 9 | parser = argparse.ArgumentParser(description='Dump Fine Grained Password Policies') 10 | parser.add_argument('-u', '--username', help='LDAP username', required=False) 11 | parser.add_argument('-p', '--password', help='LDAP password (or hash)', required=False) 12 | parser.add_argument('-l', '--ldapserver', help='LDAP server (hostname or IP)', required=True) 13 | parser.add_argument('-d', '--domain', help='AD domain (e.g. corp.local)', required=True) 14 | parser.add_argument('--use-ldaps', help='Use LDAPS (SSL/TLS)', action='store_true') 15 | parser.add_argument('--kerberos', help='Use Kerberos (GSSAPI)', action='store_true') 16 | parser.add_argument('--port', help='Custom port (default 389 or 636)', type=int) 17 | 18 | def base_creator(domain): 19 | return ','.join(f"DC={dc}" for dc in domain.split('.')) 20 | 21 | def clock(nano): 22 | sec = int(abs(nano / 10_000_000)) 23 | fmt = '{0.days} days {0.hours} hours {0.minutes} minutes {0.seconds} seconds' 24 | return fmt.format(rd(seconds=sec)) 25 | 26 | def connect(args): 27 | use_ssl = args.use_ldaps 28 | port = args.port or (636 if use_ssl else 389) 29 | 30 | tls_config = Tls(validate=ssl.CERT_NONE) 31 | server = Server(args.ldapserver, port=port, use_ssl=use_ssl, get_info=ALL, tls=tls_config if use_ssl else None) 32 | 33 | try: 34 | if args.kerberos: 35 | print("[*] Using Kerberos authentication (GSSAPI)...") 36 | conn = Connection( 37 | server, 38 | authentication=SASL, 39 | sasl_mechanism=GSSAPI, 40 | auto_bind=True 41 | ) 42 | else: 43 | if not args.username or not args.password: 44 | raise ValueError("Username and password required for NTLM authentication.") 45 | user = f"{args.domain}\\{args.username}" 46 | print(f"[*] Using NTLM authentication for user {user}...") 47 | conn = Connection( 48 | server, 49 | user=user, 50 | password=args.password, 51 | authentication=NTLM, 52 | auto_bind=True 53 | ) 54 | print("[+] LDAP bind successful.\n") 55 | return conn 56 | except LDAPException as e: 57 | print(f"[-] LDAP bind failed: {e}") 58 | exit(1) 59 | 60 | def enumerate_fgpp(conn, domain): 61 | base = base_creator(domain) 62 | fgpp_base = f"CN=Password Settings Container,CN=System,{base}" 63 | 64 | print("[*] Searching for Fine Grained Password Policies...\n") 65 | conn.search(search_base=fgpp_base, search_filter='(objectClass=msDS-PasswordSettings)', attributes=['*']) 66 | 67 | if not conn.entries: 68 | print("[-] No FGPP policies found.") 69 | return 70 | 71 | print(f"[+] {len(conn.entries)} FGPP policies found.\n") 72 | 73 | for entry in conn.entries: 74 | print("Policy Name:", entry['name']) 75 | if 'description' in entry and entry['description']: 76 | print("Description:", entry['description']) 77 | print("Minimum Password Length:", entry['msds-minimumpasswordlength']) 78 | print("Password History Length:", entry['msds-passwordhistorylength']) 79 | print("Lockout Threshold:", entry['msds-lockoutthreshold']) 80 | print("Observation Window:", clock(int(entry['msds-lockoutobservationwindow'].value))) 81 | print("Lockout Duration:", clock(int(entry['msds-lockoutduration'].value))) 82 | print("Complexity Enabled:", entry['msds-passwordcomplexityenabled']) 83 | print("Minimum Password Age:", clock(int(entry['msds-minimumpasswordage'].value))) 84 | print("Maximum Password Age:", clock(int(entry['msds-maximumpasswordage'].value))) 85 | print("Reversible Encryption:", entry['msds-passwordreversibleencryptionenabled']) 86 | print("Precedence (lower = higher priority):", entry['msds-passwordsettingsprecedence']) 87 | 88 | for target in entry['msds-psoappliesto']: 89 | print("Policy Applies to:", target) 90 | print("") 91 | 92 | def enumerate_applied_objects(conn, domain): 93 | print("[*] Enumerating objects with FGPP applied...\n") 94 | base = base_creator(domain) 95 | conn.search(search_base=base, search_filter='(msDS-PSOApplied=*)', 96 | attributes=['DistinguishedName', 'msDS-PSOApplied']) 97 | 98 | if not conn.entries: 99 | print("[-] No applied objects found.") 100 | return 101 | 102 | for entry in conn.entries: 103 | print("Object:", entry['DistinguishedName']) 104 | print("Applied Policy:", entry['msDS-PSOApplied']) 105 | print("") 106 | 107 | def main(): 108 | args = parser.parse_args() 109 | conn = connect(args) 110 | enumerate_fgpp(conn, args.domain) 111 | enumerate_applied_objects(conn, args.domain) 112 | 113 | if __name__ == "__main__": 114 | main() 115 | --------------------------------------------------------------------------------