Traverse
Challenge Lab (Medium) - by xjr
The following post by 0xb0b is licensed under CC BY 4.0
Scenario
Objective
You’ve been brought in to conduct an internal penetration test targeting a highly sensitive Linux server. To demonstrate the real-world business impact of a breach, the client has hidden two flags on the system. Your objective: compromise the server and extract both flags.
Initial Access
The client has provided you with VPN access to their environment, but no other information.
Summary
Recon
We use rustscan -b 500 -a 10.1.128.226 --top -- -sC -sV -Pn to enumerate all TCP ports on the target machine, piping the discovered results into Nmap which runs default NSE scripts -sC, service and version detection -sV, and treats the host as online without ICMP echo -Pn.
A batch size of 500 trades speed for stability, the default 1500 balances both, while much larger sizes increase throughput but risk missed responses and instability.

The target machine 10.1.128.226 is a Linux host resolving to traverse.hsm, with remote access available via SSH on port 22 (OpenSSH 9.6p1). An FTP server runs on port 21 (vsftpd 3.0.5) with anonymous login enabled, exposing a devops directory and an image file. A web server is running on port 80 (nginx 1.24.0) which redirects to http://traverse.hsm/.

We add the following entry to our /etc/hosts file.
FTP
First, we'll use the FTP service available, connect to it, and log in anonymously - maybe there are some senstive files for us available - In the devops folder, we find a ZIP file. We download it.

The zip file is password protected...

To crack the password, we generate a hash using zip2john.

Using john and the rockyou.txt file, along with the resulting hash, we are able to crack the password for the zip file.

We unzip the file and find a configuration file and a message.

The message indicates that a repository has apparently been created in /opt/app. We may have a .git folder available through the web service.

The config is also very interesting.
This looks like an nginx config that redirects all traffic to traverse.hsm and reverse-proxies requests to a backend application running on localhost:8000, while serving static assets directly from /opt/app/static/.
The location /assets section uses alias without a trailing slash on the location path, which is a classic nginx alias traversal vulnerability. By requesting something like /assets../ we might access files outside /opt/app/static/, potentially reaching out to /opt/app/ to read for example the .env file or other sensitive information.

WEB
We'll now move on to the web service. Even though we have a host, we are unable to list additional virtual hosts.We're performing a directory scan using common.txt. Among other things, this file includes an entry for .git, but we can't find the mentioned repository using a direct directory scan. In addition, we find a /deploy/ directory

We will now continue to enumerate the page manually. For now, it seems to be very static page.

We find a contact form, but we don't see any requests being made. Otherwise, this would have been a potential entry point via blind XSS to obtain session cookies.

We try to visist the deploy directory, but receive a 403.

We recall the config file in the zip file available on the FTP server. We then attempt another directory scan using the identified path-traversal approach to detect any additional directories and hidden files that may have been filtered out.
However, we could have find this misconfiguration even without the hint from the zip file, using, for example, Nuclei.

And we do find now a .git repository folder.

We dump the repository with the gitdumper.

Access as spencer
Next, we inspect the logs and find an intersing commit. The environment credentials have been moved to deployment.

We inspect the full commit history across all branches with complete diffs and do find the credentials of the user spencer. We try those to get a session via SSH but are unsuccessful. Furthermore we find another VHOST - see DOCS_BASE=http://docs-0eoyfsyajxs.traverse.hsm.

We adapt now the following entry of our /etc/hosts file as follows:
We visit the newly identified endpoint and are redirected to a login page. We immediately enter the credentials we found earlier...

... and we are able to log in. We have now access to the Employee operation workspace containing some documentary.

Shell as traverse-docs
The security baseline specifies that accounts must be strictly separated and that password reuse across systems and applications is not supported. We'll put that to the test later. At least we weren't able to log in via SSH earlier. But that wont stop us from continuing testing.
Another thing we noticed is that the pages are included using a page parameter as follows: page=pages/security.php. This is a strong indicator of a possible file inclusion.

Fuzzing using the Jason Haddix wordlist didn't yield any significant results at first. However, we'll take another look at it manually. We see we are able traverse back and forth. An LFI is certainly possible.

However, we cannot include files such as .env or /etc/passwd. We get an error message when we try to do so.

However, we can use filters. This would allows us to read the page sources. But since we can also use filters, let's try applying PHP filter chains, which would allow us to achieve remote code execution via the LFI.

We use the following project to create the phpinfo page as a PoC to show remote code execution by exploiting LFI2RCE via PHP filters.

We include the payload, but again, we receive errors like we already received by including .env or /etc/passwd.
The following code block does not display the entire payload. The same applies to the following code blocks, as they are too long.

The inclusion may be limited to .php files. We'll adapt the resulting payload and append a .php extension to it. And we are able to execute code through PHP filter chains.

We're now using the project again to create a very small webshell, which we obtained from revshells.com.

We issue the id command directly in the URL as follows ?0=id&page=php://filter/ and are able to identify the user running the web service, we are traverse-docs.

Now that we can use the webshell to execute commands on the system, we want to set up a reverse shell. First, we'll set up a listener using Penelope.

Next, we'll spawn a reverse shell using BusyBox. We receive a connection and are the user traverse-docs.

Shell as spencer
On the system, we see from the /etc/passwd file that there is also a user named spencer. Even though we can't connect via SSH, we still try to switch to that user using the su command and the password we found in the Git history, and we are able to successfully switch to spencer. It appears that the policy was not followed...
We find the user flag in /home/spencer/user.txt.

Access as webadmin@traverse.hsm
Host Enumeration
In spencer's home directory, we also find a note indicating that a Docker container is apparently running in the management dashboard on the 172.20.0.0/24 network. It does not appear to be in production yet.

Unfortunatley our user is not in docker group. We have access to the docker binary...

... but cannot inspect the containers running.

Before we dive into the container, we also perform a comprehensive enumeration using Linpeas and find a few interesting findings.

We can see that it's listening on port 389, which corresponds to LDAP. It's also interesting that it's listening on 0.0.0.0, but we didn't see that in our nmap scan. It appears that firewall rules are active; perhaps this is only for internal use and a Docker container is using this.

Furthermore, it is noticeable that the tcpdump binary has some intersting capabilities set.
It has the cap_net_raw set, which lets any user capture network traffic without being root, so it would allows us sniff any traffic that goes through the machine.

We will now continue with the Docker enumeration. We know that there are likely one or more instances on the 172.20.0.0/24 network. We use a small ping sweep script to identify running hosts and find a container at 172.20.0.10.

Ligolo-ng setup
For the subsequent phases, we use ligolo to relay traffic between the target machine and our attacker machine to make the internal reachable networks of the target machine accessible to our attacker machine. Especially to reach out to the docker container and be also able to enumerate the LDAP service running on port 389.
Ligolo-ng is a simple, lightweight and fast tool that allows pentesters to establish tunnels from a reverse TCP/TLS connection using a tun interface (without the need of SOCKS).
On our attack machine, we start the proxy server.

Inside that proxy we create an interface called traverse and set up the routes. We add the routes for 240.0.0.1/32 as a virtual listener IP and 172.20.0.0/24 to reach the internal network through the compromised host.

Next, we get the agent to the target machine and connect to our proxy.

After the connection has been made we should see in our proxy that an agent has joined.

We can list and interact with the session by calling session and then chosing the session.
After chosing the session, we can start the tunnel.
To confirm our tunnel and routes we can issue the following commands:

We should now be able to reach out to 172.20.0.0/24.
Docker Enumeration & Further Host Enumeration
First, we perform a port scan on 172.20.0.10.
We use rustscan -b 500 -a 172.20.0.10 --top -- -sC -sV -Pn to enumerate all TCP ports on the target machine, piping the discovered results into Nmap which runs default NSE scripts -sC, service and version detection -sV, and treats the host as online without ICMP echo -Pn.
A batch size of 500 trades speed for stability, the default 1500 balances both, while much larger sizes increase throughput but risk missed responses and instability.

The Docker container 172.20.0.10 is a linux machine accessible via SSH on port 22 (OpenSSH 8.9p1) and is running a Python-based web server on port 8080 (Werkzeug 3.1.8 / Python 3.10.12) serving the Traverse HSM · Internal Login page.

We visit the mentioned page on port 8080 and see that you can log in via LDAP. Interseting, we've already seen LDAP running internally on the host.


So, let's try enumerating the LDAP service on the host first, maybe we'll find something interesting.
We'll use ldapsearch and first try to get the base naming context first, since without knowing the naming context we cannot search any entries. We identify the namingContext as dc=nodomain.

Using the base DN obtained from the previous query, we dump all objects in the directory tree anonymously. This may reveal any users, groups, and sensitive attributes like for example passwords or password hashes. And we do find the userPassword for spencer and the webadmin user as well as their corresponding mails that are required from the login page on 8080. Those entries are base64 encoded.

We decode them and see that those are SSHA (Salted SHA-1) password hashes. This is the standard format that OpenLDAP uses for userPassword attributes.

We try to crack the hashes using hashcat and successfully crack the hash for the webadmin user.

Next, we try tp log in to the dashboard as webadmin@traverse.hsm...

... and are successful. We have a Management Dashboard infront of us.

Shell as admin
On that page we see that we are able to perfom LDAP conneciton tests.

We issue such a test and see an authentication as admin being done.

We recall our Linpeas results and we know that we are able to sniff the data from the host. Since we have not found any other docker containers in the network and the Host is listening to 389 we try to sniff for that traffic. We may be able to capture the credentials being issued.

In our Penelope session we stop our ligolo agent if not already run in background and run the agent again in background...

Next, we run a tcpdump on port 389 on any interface...

... and issue another LDAP connection test.

We see the connection being made and also spot something that looks like credentials.

We can also create a capture file to inspect the traffic in a more human readable way.

We issue another connection test.

And after the connection test we abort the tcpdump process and gather the captured result.

To retreive the data we run a simple python webserver on the target machine...

... and download the PCAP file.

Inspecting the PCAP file we see that LDAP simple binds send credentials in cleartext on port 389, any bind request will expose the DN and password directly in the capture.

We try to reuse the credentials to log in via SSH to the container possible to our Ligolo setup and we see we are able to log in. Here, too, the policy was not followed, and there was no strict separation of user credentials between application users and system users.

Shell as root
As the admin user, we can run everything as root using sudo, so let's switch to the root user right away.

Now that we have root privileges on the Docker container, we can try to escape from it. To enumerate such possibilities, we'll use the deepce.sh script.

Among other things, we see that a share has been mounted. Perhaps it's located on the host. If that's the case, we could use it to escalate privileges by placing a Bash binary there and setting the SUID bit.

We see that we have write permissions on the share. But first we need to figure out where this share is eventually on the host so we insert a file we will look for from the hosts perspective.

We search for our file and do find it at /var/share.

The container's /mnt/share and the host's /var/share are the same directory via a bind mount. Unfortunately we cannot write the bash binary from the host to the share due to missing permissions and it would be owned by spencer... But we can copy a SUID root bash there from inside the container, so it appears on the host owned by real root. And it's compatible with the system of the host. Since there's no nosuid on the mount and no user namespace remapping, the host kernel honors the SUID bit and gives spencer euid=0 when running it with -p.

We run the bash SUID binary with -p and become root and are able to read the final flag at /root/root.txt.

Last updated