Aller au contenu

ExécutionActivité utilisateurAnti-forensique

Historique du shell : commandes bash, zsh et fish

Historique des commandes par utilisateur de bash, zsh et fish : contenu de chaque fichier, présence des horodatages et traces d'évasion de l'historique.

Emplacement
~/.bash_history
Prouve
Quelles commandes ont été saisies dans un shell interactif sous un compte donné, et parfois quand
Horodatages
Secondes epoch Unix (UTC) si enregistrées : bash seulement avec HISTTIMEFORMAT, zsh avec EXTENDED_HISTORY, fish toujours
Accès
Tout utilisateur pour ses propres fichiers ; root pour les répertoires personnels des autres
Rétention
Jusqu'à troncature par HISTFILESIZE/SAVEHIST ou suppression
Collecte
UAC, Velociraptor, cp -a, tar

Ce que c'est

Les shells interactifs conservent la liste des lignes de commande saisies par l'utilisateur et l'enregistrent dans un fichier propre à chaque utilisateur afin de pouvoir les rappeler lors de sessions ultérieures. Bash, zsh et fish utilisent chacun leur propre fichier et leur propre format. Ces fichiers sont en texte brut (fish utilise une structure proche du YAML), appartiennent à l'utilisateur qui peut y écrire, et sont écrits au rythme du shell et non à chaque exécution de commande.

Pour un enquêteur, c'est souvent la trace la plus directe de l'activité manuelle après la compromission d'un compte. Il est aussi incomplet par nature : ce que le shell n'a jamais écrit sur disque, ou ce que l'utilisateur a choisi de supprimer, n'y figure tout simplement pas.

Où il se trouve

ShellFichier par défautRemarques
bash~/.bash_historyChemin défini par HISTFILE ; si HISTFILE est non défini ou vide, rien n'est enregistré à la sortie
bash (root)/root/.bash_historyLà où arrivent les commandes après sudo -i, sudo su - ou su -
zshPas de valeur par défaut ; souvent ~/.zsh_history ou ~/.zhistoryÉcrit uniquement si HISTFILE est défini (et SAVEHIST non nul) dans un fichier de démarrage
fish~/.local/share/fish/fish_history$XDG_DATA_HOME/fish/ s'il est défini ; la variable fish_history change le nom de session en <name>_history, une valeur vide désactive l'enregistrement

Les chemins sont identiques sur Debian/Ubuntu, RHEL/Fedora et les autres familles. Ce qui diffère, c'est la configuration par défaut : par exemple, le ~/.bashrc du squelette Debian et Ubuntu définit HISTCONTROL=ignoreboth et active histappend, alors que d'autres distributions peuvent définir les variables d'historique dans /etc/profile, /etc/bashrc ou /etc/profile.d/. Lisez toujours les fichiers de démarrage réels de l'image (voir fichiers de démarrage du shell) avant d'interpréter des lacunes.

Les comptes de service ont aussi des répertoires personnels : vérifiez /var/www, /var/lib/postgresql, /var/lib/mysql, /opt/* et tout répertoire listé dans /etc/passwd à la recherche de fichiers d'historique égarés.

Ce qu'il prouve

  • Quelles lignes de commande ont été saisies dans un shell interactif tournant sous ce compte.
  • L'ordre relatif dans lequel un shell a écrit ces lignes (pas toujours l'ordre d'exécution entre sessions parallèles).
  • Avec horodatages : quand chaque commande a été saisie (zsh et fish permettent aussi de borner l'activité d'une session).
  • L'usage d'outils, les chemins de fichiers, les hôtes distants, les URL et les identifiants passés en arguments, qui mènent vers d'autres artefacts.

Il ne prouve pas :

  • Qui était au clavier (les comptes partagés, volés ou changés via su cassent l'attribution).
  • Qu'une commande a réussi, ni même qu'elle a été exécutée si elle a été saisie puis annulée.
  • Quoi que ce soit sur l'exécution non interactive : scripts, tâches cron, web shells et ssh host 'cmd' ne laissent normalement rien ici.

Champs clés

FormatStructure de l'enregistrementSignification
bash, simplecommand lineUne entrée par ligne, sans heure
bash, HISTTIMEFORMAT défini#1727512345 puis command lineLe caractère de commentaire suivi de chiffres marque l'horodatage de l'entrée suivante ; il délimite aussi les entrées multilignes
zsh EXTENDED_HISTORY: <start>:<elapsed>;commandHeure de début en secondes epoch, durée écoulée en secondes, puis la commande
fish- cmd: ... / when: ... / liste paths: facultativewhen est en secondes epoch ; paths liste les arguments que fish a reconnus comme des chemins existants

Zsh stocke les octets non ASCII dans un encodage « métafié » : certains caractères paraissent corrompus dans une visionneuse texte classique.

Horodatages

Les trois formats utilisent des secondes epoch Unix, donc en UTC. Le mtime du fichier indique la dernière écriture par un shell, et le ctime le dernier changement de métadonnées (utile quand un fichier a été tronqué ou remplacé par un lien symbolique).

# bash with HISTTIMEFORMAT
grep -E '^#[0-9]{9,}$' .bash_history | head -1 | cut -c2- | xargs -I{} date -u -d @{}   # first timed entry
# zsh extended history
awk -F'[:;]' '/^: [0-9]+:/ {cmd="date -u -d @" ($2+0) " +%FT%TZ"; cmd | getline t; close(cmd); print t, $0}' .zsh_history

Rétention

  • bash : à la sortie, les HISTSIZE dernières entrées (500 par défaut) sont ajoutées au fichier (avec histappend) ou l'écrasent, puis le fichier est réduit à HISTFILESIZE lignes. L'activité ancienne disparaît par le haut.
  • zsh : SAVEHIST plafonne le fichier. APPEND_HISTORY est activé par défaut ; INC_APPEND_HISTORY ou SHARE_HISTORY écrivent chaque commande immédiatement au lieu d'attendre la sortie.
  • fish : history clear et history delete suppriment des entrées ; history merge intègre les modifications des autres sessions.
  • Les sessions tuées par SIGKILL, plantées ou encore ouvertes au moment de l'acquisition peuvent ne jamais atteindre le disque. Un bash en cours d'exécution les garde en mémoire du processus.

Collecte

Collectez depuis chaque répertoire personnel et /root, y compris les copies tournées ou de sauvegarde (.bash_history~, .bash_history.*). Les artefacts files/shell/* d'UAC (inclus dans ir_triage) couvrent bash, zsh, fish et d'autres shells, et analysent aussi les fichiers de démarrage pour trouver un HISTFILE non standard.

sudo ./uac -p ir_triage /mnt/usb/case42
# dead box, read-only mount
find /mnt/evidence/root /mnt/evidence/home -maxdepth 4 \
  \( -name '.*history*' -o -path '*/fish/fish_history' \) -print0 | tar --null -T - -czf history.tgz

Dans Velociraptor, Linux.Sys.BashHistory recherche /{root,home/*}/.*_history sur l'ensemble des postes. Si le shell suspect est peut-être encore actif, capturez d'abord la mémoire (LiME ou AVML) : l'acquisition du disque ne vide pas l'historique en mémoire, mais une déconnexion ultérieure écrasera ce que vous avez vu.

Analyse

  • Plaso : les plugins texte bash_history et zsh_extended_history font partie du preset linux (sélectionnez-les avec text/bash_history et text/zsh_extended_history) ; fish_history est un parser distinct. Seules les entrées horodatées deviennent des événements.
  • Volatility 3 : linux.bash.Bash récupère l'historique des processus bash en cours, y compris les commandes jamais écrites sur disque.
  • Les outils simples suffisent pour la revue :
log2timeline.py --parsers 'text/bash_history,text/zsh_extended_history,fish_history' \
  --storage-file hist.plaso /mnt/evidence/home
psort.py -o dynamic --output_time_zone UTC -w hist.csv hist.plaso

Conseils d'investigation

  • Considérez un historique vide, de zéro octet ou lié symboliquement à /dev/null sur un compte actif comme une piste. Vérifiez le ctime du lien symbolique et comparez le mtime du fichier avec les connexions dans wtmp et lastlog.
  • Recherchez les chaînes d'évasion dans les historiques d'autres shells, les fichiers de démarrage et la mémoire : unset HISTFILE, HISTFILE=/dev/null, HISTSIZE=0, set +o history, history -c, history -d, kill -9 $$.
  • L'absence des commandes saisies avec un espace initial est normale là où ignorespace/ignoreboth ou HIST_IGNORE_SPACE de zsh est défini, et fish écarte ces lignes par conception. C'est un réglage par défaut, pas une preuve d'intention.
  • Une couverture #epoch partielle dans un fichier bash signifie que HISTTIMEFORMAT a été activé à un moment donné ; les lignes sans horodatage sont plus anciennes ou proviennent d'un shell où il n'était pas défini.
  • Après sudo -i ou su -, poursuivez dans /root/.bash_history et corrélez avec les journaux sudo.
  • Quand auditd journalise EXECVE, il fournit des traces d'exécution indépendantes et horodatées pour confirmer les entrées d'historique (voir auditd).
  • Les éditeurs, pagers et REPL tiennent leurs propres historiques, que les attaquants nettoient rarement : voir viminfo et lesshst.

Voir aussi