Un administrator observă o conectare SSH într-un interval în care nu era programată nicio intervenție. Întrebările utile nu sunt doar „cine s-a autentificat?”, ci și „de la ce adresă?”, „cu ce amprentă de cheie?”, „cât a rămas sesiunea deschisă?” și „ce comenzi au fost lansate prin sudo?”.
În acest laborator vei produce un pachet local de dovezi și un raport Markdown din jurnalul systemd. Colectorul nu modifică configurația SSH și nu repornește servicii. El extrage evenimentele sshd și sudo, inventariază amprentele cheilor publice autorizate și calculează hash-uri SHA-256 pentru fișierele colectate.
Rezultatul este verificabil: directorul evidence/ va conține jurnalul brut, configurația efectivă raportată de sshd, inventarul cheilor, suma de control și patru rapoarte. Fișierul raport.md va lega autentificările acceptate de amprentele găsite în authorized_keys, iar fișierele CSV vor putea fi deschise într-un instrument de analiză.
Acesta este un exercițiu defensiv pentru un server propriu sau pentru un sistem pe care ai autorizare să-l investighezi. Dacă ai nevoie mai întâi de noțiunile despre conturi și privilegii, consultă auditul utilizatorilor, grupurilor și accesului sudo.
Fixăm mediul și limitele investigației
Varianta de mai jos este construită pentru Ubuntu Server 24.04 LTS, OpenSSH 9.6p1, systemd 255 și Python 3.12. Actualizările Ubuntu pot schimba sufixul versiunii pachetului, de aceea comenzile de verificare sunt mai importante decât un număr de build fix.
Sunt suficiente un procesor virtual, 1 GB RAM și aproximativ 100 MB liberi pentru proiect. Spațiul necesar dovezilor depinde însă de mărimea jurnalului. Rezervă 30–60 de minute și rulează comenzile dintr-un cont care poate folosi sudo.
cat /etc/os-release
ssh -V
systemctl --version | head -n 1
python3 --version
df -h .
sudo journalctl --disk-usage
sudo /usr/sbin/sshd -T | grep -E '^(loglevel|authorizedkeysfile) '
Pe o instalare compatibilă, rezultatul va semăna cu exemplul următor; revizia Ubuntu și spațiul ocupat de jurnal pot fi diferite:
PRETTY_NAME="Ubuntu 24.04.x LTS"
OpenSSH_9.6p1 Ubuntu-...
systemd 255 (...)
Python 3.12.x
loglevel INFO
authorizedkeysfile .ssh/authorized_keys .ssh/authorized_keys2
sshd -T afișează configurația efectivă, după evaluarea valorilor implicite. Dacă authorizedkeysfile indică altă locație sau o sursă externă, inventarul construit aici nu poate atribui automat cheia unui fișier local.
La nivelul INFO, OpenSSH înregistrează autentificările acceptate și, pentru autentificarea cu cheie publică, amprenta cheii. Nivelul VERBOSE poate oferi date suplimentare pentru evenimente viitoare, dar nu poate completa retroactiv un jurnal și nu îl activăm în timpul investigației.
Jurnalul permite reconstruirea activității observate, nu demonstrarea fiecărei acțiuni. sudo consemnează comenzile lansate prin mecanismul său, dar un shell pornit ca root, un alt instrument de escaladare sau ștergerea logurilor poate rupe lanțul. Raportul trebuie interpretat împreună cu alte dovezi, nu tratat drept o înregistrare completă a terminalului.
Instalăm strict uneltele folosite de laborator
openssh-server furnizează serverul și utilitarele OpenSSH, iar Python din depozitul Ubuntu este folosit doar cu biblioteca standard. Pregătirea se face într-un singur bloc:
sudo apt update
sudo apt install --yes openssh-server python3
mkdir -p "$HOME/ssh-forensics"
cd "$HOME/ssh-forensics"
Arborele final va fi:
ssh-forensics/
├── collect-ssh-evidence.sh # extrage jurnalul și inventariază cheile autorizate
├── build-report.py # transformă evenimentele în rapoarte CSV și Markdown
└── evidence/
├── events.log # evenimente sshd și sudo, normalizate în UTC
├── sshd-effective.txt # configurația efectivă OpenSSH
├── authorized_keys.tsv # conturi, amprente și comentarii ale cheilor
├── SHA256SUMS # hash-uri pentru fișierele colectate
├── chei_acceptate.csv # autentificările cu cheie publică
├── sesiuni_ssh.csv # deschiderea și închiderea sesiunilor
├── sudo.csv # comenzile sudo și sesiunea SSH corelată
└── raport.md # sinteza investigației
Directorul evidence/ va avea permisiunea 0700. Jurnalele pot dezvălui utilizatori, adrese IP, căi și comenzi administrative, deci nu publica fișierele fără anonimizare.
Pasul 1 — Colectăm evenimentele fără să atingem configurația SSH
Scopul este să obținem o copie de lucru stabilă pentru un interval explicit. Folosim UTC pentru ca ordinea evenimentelor să nu fie ambiguă la schimbarea fusului orar.
Fișierul $HOME/ssh-forensics/collect-ssh-evidence.sh trebuie să conțină integral:
#!/usr/bin/env bash
set -euo pipefail
umask 077
if [[ ${EUID} -ne 0 ]]; then
echo "Eroare: rulează colectorul cu sudo." >&2
exit 1
fi
if [[ $# -ne 3 ]]; then
echo "Utilizare: $0 START END DIRECTOR" >&2
echo "Exemplu: $0 '2026-09-25 08:00:00' '2026-09-25 18:00:00' evidence" >&2
exit 1
fi
START=$1
END=$2
OUT=$3
install -d -m 0700 "$OUT"
journalctl
--since="$START"
--until="$END"
--utc
--output=short-iso-precise
--no-hostname
--no-pager
--quiet
-t sshd
-t sudo > "$OUT/events.log"
/usr/sbin/sshd -T > "$OUT/sshd-effective.txt"
printf 'accounttfingerprinttbitstcomment_typetfilen'
> "$OUT/authorized_keys.tsv"
while IFS=: read -r account _ _ _ _ home _; do
key_file="${home}/.ssh/authorized_keys"
[[ -r "$key_file" ]] || continue
while read -r bits fingerprint comment_type; do
printf '%st%st%st%st%sn'
"$account"
"$fingerprint"
"$bits"
"$comment_type"
"$key_file"
>> "$OUT/authorized_keys.tsv"
done < <(ssh-keygen -E sha256 -lf "$key_file")
done < <(getent passwd)
if [[ ! -s "$OUT/events.log" ]]; then
echo "Eroare: nu există evenimente sshd sau sudo în intervalul cerut." >&2
exit 2
fi
find "$OUT"
-maxdepth 1
-type f
! -name SHA256SUMS
-printf '%f'
| sort -z
| xargs -0 sha256sum > "$OUT/SHA256SUMS"
echo "Colectare încheiată în: $OUT"
echo "Verifică integritatea cu: cd '$OUT' && sha256sum --check SHA256SUMS"
Fă scriptul executabil:
chmod 700 collect-ssh-evidence.sh
-t sshd -t sudo selectează mesajele care au identificatorul syslog sshd sau sudo. --output=short-iso-precise păstrează data, fusul și precizia temporală, iar --no-hostname simplifică parsarea fără să elimine datele sursă din mesaj.
Opțiunile -E sha256 -l cer utilitarului ssh-keygen să afișeze amprente SHA-256 pentru cheile publice. Nu sunt citite și nu sunt copiate chei private.
Trebuie să vezi doar revenirea la prompt după chmod; fișierul va avea permisiuni de execuție. Punct de control: rulează bash -n collect-ssh-evidence.sh. Lipsa unui mesaj înseamnă că sintaxa Bash este validă.
Pasul 2 — Înghețăm intervalul relevant și verificăm hash-urile
Scopul este să restrângi datele la fereastra incidentului. Orele următoare sunt valori de laborator; în investigația ta înlocuiește-le cu intervalul documentat și notează fusul orar folosit.
cd "$HOME/ssh-forensics"
sudo ./collect-ssh-evidence.sh
"2026-09-25 08:00:00"
"2026-09-25 18:00:00"
evidence
sudo sh -c 'cd evidence && sha256sum --check SHA256SUMS'
--since și --until includ numai mesajele din fereastra cerută. Colectorul cere trei argumente tocmai pentru a împiedica o extracție accidentală fără limite. sha256sum --check recalculează hash-ul fiecărui fișier inițial.
Output-ul așteptat va arăta astfel:
Colectare încheiată în: evidence
Verifică integritatea cu: cd 'evidence' && sha256sum --check SHA256SUMS
authorized_keys.tsv: OK
events.log: OK
sshd-effective.txt: OK
Hash-urile arată dacă fișierele s-au schimbat după colectare; nu demonstrează singure că jurnalul original era complet. Punct de control: toate cele trei fișiere trebuie să primească OK înainte de analiză.
Dacă investigația privește mai ales tentative eșuate, articolul despre analiza logurilor de autentificare Linux oferă contextul complementar. Aici urmărim autentificările reușite și activitatea administrativă asociată.
Pasul 3 — Construim cronologia sesiunilor și atribuim amprentele
Parserul caută exclusiv formele de mesaje produse pentru autentificări public-key acceptate, deschideri și închideri PAM și evenimente sudo. Corelarea principală folosește PID-ul procesului sshd; comenzile sudo sunt asociate celei mai recente sesiuni încă deschise a aceluiași utilizator.
Fișierul $HOME/ssh-forensics/build-report.py trebuie să conțină integral:
#!/usr/bin/env python3
import csv
import re
import sys
from pathlib import Path
LINE = re.compile(
r"^(?P<time>S+)s+(?P<program>sshd|sudo)"
r"(?:[(?P<pid>d+)])?:s(?P<message>.*)$"
)
ACCEPT = re.compile(
r"Accepted publickey for (?P<user>S+) from (?P<ip>S+) "
r"port (?P<port>d+) ssh2: (?P<algorithm>S+) "
r"(?P<fingerprint>SHA256:S+)"
)
OPEN = re.compile(
r"pam_unix(sshd:session): session opened for user (?P<user>S+)"
)
CLOSE = re.compile(
r"pam_unix(sshd:session): session closed for user (?P<user>S+)"
)
SUDO = re.compile(
r"(?P<actor>S+)s+:s+TTY=(?P<tty>[^;]+)s*;s*"
r"PWD=(?P<pwd>[^;]+)s*;s*USER=(?P<target>[^;]+)s*;s*"
r"COMMAND=(?P<command>.+)$"
)
def write_csv(path, fields, rows):
with path.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
def md(value):
return str(value).replace("|", r"|").replace("`", r"`")
if len(sys.argv) != 2:
print("Utilizare: build-report.py DIRECTOR_EVIDENCE", file=sys.stderr)
raise SystemExit(1)
evidence = Path(sys.argv[1])
events_file = evidence / "events.log"
keys_file = evidence / "authorized_keys.tsv"
if not events_file.is_file() or not keys_file.is_file():
print("Eroare: lipsesc events.log sau authorized_keys.tsv.", file=sys.stderr)
raise SystemExit(2)
key_owners = {}
with keys_file.open(encoding="utf-8", newline="") as handle:
for row in csv.DictReader(handle, delimiter="t"):
key_owners.setdefault(row["fingerprint"], []).append(row["account"])
accepted = []
sessions = {}
sudo_events = []
with events_file.open(encoding="utf-8", errors="replace") as handle:
for raw_line in handle:
line_match = LINE.match(raw_line.rstrip("n"))
if not line_match:
continue
timestamp = line_match["time"]
program = line_match["program"]
pid = line_match["pid"] or ""
message = line_match["message"]
if program == "sshd":
accepted_match = ACCEPT.search(message)
if accepted_match:
event = accepted_match.groupdict()
event.update(
time=timestamp,
pid=pid,
key_owner="|".join(
sorted(key_owners.get(event["fingerprint"], []))
) or "NEMAPAT",
)
accepted.append(event)
sessions[(pid, event["user"])] = {
"pid": pid,
"user": event["user"],
"ip": event["ip"],
"source_port": event["port"],
"fingerprint": event["fingerprint"],
"key_owner": event["key_owner"],
"accepted_at": timestamp,
"opened_at": "",
"closed_at": "",
}
continue
open_match = OPEN.search(message)
if open_match:
key = (pid, open_match["user"])
if key in sessions:
sessions[key]["opened_at"] = timestamp
continue
close_match = CLOSE.search(message)
if close_match:
key = (pid, close_match["user"])
if key in sessions:
sessions[key]["closed_at"] = timestamp
continue
if program == "sudo":
sudo_match = SUDO.search(message)
if not sudo_match:
continue
event = sudo_match.groupdict()
candidates = [
session
for session in sessions.values()
if session["user"] == event["actor"]
and session["opened_at"]
and not session["closed_at"]
]
linked = max(
candidates,
key=lambda item: item["opened_at"],
default=None,
)
event.update(
time=timestamp,
ssh_pid=linked["pid"] if linked else "NECORELAT",
source_ip=linked["ip"] if linked else "NECORELAT",
)
sudo_events.append(event)
session_rows = sorted(
sessions.values(),
key=lambda item: (item["accepted_at"], item["pid"]),
)
accepted_fields = [
"time", "pid", "user", "ip", "port",
"algorithm", "fingerprint", "key_owner",
]
session_fields = [
"pid", "user", "ip", "source_port", "fingerprint",
"key_owner", "accepted_at", "opened_at", "closed_at",
]
sudo_fields = [
"time", "actor", "target", "tty", "pwd",
"command", "ssh_pid", "source_ip",
]
write_csv(evidence / "chei_acceptate.csv", accepted_fields, accepted)
write_csv(evidence / "sesiuni_ssh.csv", session_fields, session_rows)
write_csv(evidence / "sudo.csv", sudo_fields, sudo_events)
report = [
"# Raport SSH Forensics",
"",
f"- Autentificări public-key acceptate: {len(accepted)}",
f"- Sesiuni SSH reconstruite: {len(session_rows)}",
f"- Evenimente sudo: {len(sudo_events)}",
"",
"## Sesiuni SSH",
"",
"| Acceptată UTC | Utilizator | IP sursă | PID | Cheie | Proprietar local | Închisă UTC |",
"|---|---|---|---|---|---|---|",
]
for row in session_rows:
report.append(
f"| {md(row['accepted_at'])} | {md(row['user'])} | "
f"{md(row['ip'])} | {md(row['pid'])} | "
f"`{md(row['fingerprint'])}` | {md(row['key_owner'])} | "
f"{md(row['closed_at'] or 'FĂRĂ EVENIMENT DE ÎNCHIDERE')} |"
)
report.extend([
"",
"## Comenzi sudo",
"",
"| Timp UTC | Actor | Țintă | TTY | Comandă | PID SSH | IP sursă |",
"|---|---|---|---|---|---|---|",
])
for row in sudo_events:
report.append(
f"| {md(row['time'])} | {md(row['actor'])} | "
f"{md(row['target'])} | {md(row['tty'])} | "
f"`{md(row['command'])}` | {md(row['ssh_pid'])} | "
f"{md(row['source_ip'])} |"
)
report.extend([
"",
"## Limite",
"",
"- NEMAPAT înseamnă că amprenta nu apare în fișierele locale inventariate.",
"- NECORELAT înseamnă că nu exista o sesiune SSH deschisă a actorului în datele analizate.",
"- Lipsa închiderii poate indica o sesiune activă, un jurnal incomplet sau oprirea necontrolată.",
"- Raportul arată comenzile sudo jurnalizate, nu toate comenzile executate într-un shell privilegiat.",
])
(evidence / "raport.md").write_text(
"n".join(report) + "n",
encoding="utf-8",
)
print(f"Raport creat: {evidence / 'raport.md'}")
print(f"Sesiuni SSH: {len(session_rows)}")
print(f"Evenimente sudo: {len(sudo_events)}")
Fă parserul executabil și verifică-i sintaxa:
chmod 700 build-report.py
python3 -m py_compile build-report.py
Expresiile regulate nu caută parole și nu încearcă să interpreteze tentativele eșuate. key_owner arată contul în al cărui authorized_keys a fost găsită amprenta; nu dovedește identitatea persoanei care controla cheia privată la momentul conectării.
Output-ul verificării de sintaxă este gol. Punct de control: dacă apare o excepție Python, compară fișierul integral, inclusiv indentarea, înainte de a continua.
Pasul 4 — Generăm raportul și citim corelațiile corect
Acum transformăm copia jurnalului în artefactele finale. Rularea cu sudo este necesară deoarece directorul de dovezi a fost creat cu permisiunea 0700 pentru root.
cd "$HOME/ssh-forensics"
sudo python3 build-report.py evidence
sudo sed -n '1,120p' evidence/raport.md
Trebuie să vezi un rezultat de forma:
Raport creat: evidence/raport.md
Sesiuni SSH: 1
Evenimente sudo: 2
# Raport SSH Forensics
- Autentificări public-key acceptate: 1
- Sesiuni SSH reconstruite: 1
- Evenimente sudo: 2
Valorile depind de jurnalul serverului. În tabel, NEMAPAT nu înseamnă automat cheie malițioasă: cheia poate să fi fost eliminată după incident, poate proveni din alt fișier indicat prin AuthorizedKeysFile sau dintr-un mecanism extern.
Înainte să atribui o acțiune unei persoane, verifică cine administra cheia, dacă era partajată și dacă aceeași amprentă apare la mai multe conturi. Pentru interpretarea corectă a proprietarilor și permisiunilor, folosește baza practică despre fișiere, permisiuni și procese în Linux.
Punct de control: accepted_at, utilizatorul, IP-ul și amprenta trebuie să corespundă rândului original Accepted publickey din events.log.
Pasul 5 — Facem testul de capăt fără să alterăm dovezile
Testul final verifică integritatea intrărilor colectate, existența rapoartelor și proveniența unei amprente. Nu recalcula SHA256SUMS după analiză: ai pierde valoarea de comparație inițială.
cd "$HOME/ssh-forensics"
sudo sh -c 'cd evidence && sha256sum --check SHA256SUMS'
sudo test -s evidence/chei_acceptate.csv
sudo test -s evidence/sesiuni_ssh.csv
sudo test -s evidence/sudo.csv
sudo test -s evidence/raport.md
sudo grep '^Accepted publickey' evidence/events.log || true
sudo head -n 5 evidence/chei_acceptate.csv
sudo head -n 5 evidence/sesiuni_ssh.csv
sudo head -n 5 evidence/sudo.csv
În funcție de prefixul produs de journalctl, grep '^Accepted publickey' poate să nu afișeze nimic deoarece linia începe cu timestamp-ul. Verificarea sigură a evenimentelor este:
sudo grep 'sshd.*Accepted publickey' evidence/events.log
Un eveniment relevant va semăna cu acest exemplu, în care timestamp-ul, PID-ul, contul, IP-ul, portul și amprenta diferă:
2026-09-25T09:14:22.123456+00:00 sshd[1842]: Accepted publickey for admin from 192.0.2.25 port 51422 ssh2: ED25519 SHA256:...
Testul este trecut dacă hash-urile sunt OK, cele patru rapoarte sunt nevide, iar rândul din chei_acceptate.csv păstrează aceleași valori ca evenimentul brut. Pentru folosirea și administrarea preventivă a accesului, continuă cu ghidul SSH configurat în siguranță pe Linux.
Mesajele care indică o problemă reală în acest laborator
Eroare: nu există evenimente sshd sau sudo în intervalul cerut. Intervalul este greșit, jurnalul nu mai păstrează perioada sau identificatorii nu apar în acea fereastră. Verifică limitele disponibile:
sudo journalctl --list-boots
sudo journalctl -t sshd --no-pager | tail -n 20
sshd: no hostkeys available -- exiting. sshd -T nu poate evalua instalația deoarece lipsesc cheile host. Confirmă starea pachetului și generează numai cheile host standard lipsă:
sudo ssh-keygen -A
sudo /usr/sbin/sshd -t
authorized_keys is not a public key file. Un fișier inventariat conține o linie pe care ssh-keygen nu o poate interpreta drept cheie publică. Nu-l șterge în timpul investigației; inspectează numerotat copia autorizată:
sudo nl -ba /home/UTILIZATOR/.ssh/authorized_keys
UTILIZATOR este contul indicat de mesaj și trebuie înlocuit explicit.
Eroare: lipsesc events.log sau authorized_keys.tsv. Parserul a primit directorul greșit sau colectarea nu s-a încheiat. Verifică:
sudo find evidence -maxdepth 1 -type f -printf '%fn' | sort
sha256sum: WARNING: 1 computed checksum did NOT match Cel puțin un artefact inițial s-a schimbat. Oprește analiza pe acea copie, păstrează toate fișierele și reia dintr-o copie verificată; nu suprascrie suma de control.
sudo sh -c 'cd evidence && sha256sum --check SHA256SUMS'
FĂRĂ EVENIMENT DE ÎNCHIDERE în raport. Nu este o eroare a parserului. Sesiunea putea fi încă activă la sfârșitul intervalului, jurnalul poate fi incomplet sau sistemul se putea opri fără închiderea PAM. Extinde doar limita END, colectează într-un director nou și păstrează prima copie.
Ștergem laboratorul sau păstrăm numai copia controlată
Într-o investigație reală, nu șterge dovezile înainte ca responsabilul de incident să stabilească politica de păstrare. Pentru laborator, curățenia elimină scripturile și directorul generat; nu dezinstalăm OpenSSH dacă serverul îl folosește pentru administrare.
Avertisment: următoarea comandă șterge definitiv proiectul local indicat.
cd "$HOME"
sudo rm -rf -- "$HOME/ssh-forensics"
Dacă ai instalat openssh-server exclusiv într-o mașină virtuală de laborator și ai confirmat că nu mai ai nevoie de acces SSH, îl poți elimina local:
sudo apt remove --yes openssh-server
sudo apt autoremove --yes
Nu executa eliminarea pe un server administrat exclusiv prin SSH: serviciul poate deveni indisponibil și îți poți pierde accesul.
Lista de control pentru raportul SSH
- ☐ Intervalul
START–ENDși fusul UTC sunt consemnate. - ☐
events.log,sshd-effective.txtșiauthorized_keys.tsvau hashOK. - ☐ Fiecare autentificare acceptată are utilizator, IP, port și amprentă.
- ☐ Amprentele
NEMAPATau fost verificate față de configurațiaAuthorizedKeysFile. - ☐ Deschiderile și închiderile PAM sunt corelate prin PID-ul
sshd. - ☐ Evenimentele
sudosunt atribuite doar când există o sesiune compatibilă. - ☐ Comenzile marcate
NECORELATnu sunt atribuite forțat unei sesiuni SSH. - ☐ Raportul menționează că jurnalul
sudonu reproduce toate comenzile unui shell. - ☐ Copia de lucru rămâne separată de artefactele originale.
- ☐ Fișierele cu IP-uri, utilizatori și comenzi nu sunt publicate neanonimizate.
Următorul proiect logic este activarea unei retenții persistente și dimensionate pentru systemd-journald, urmată de trimiterea copiilor către un colector separat. Pentru organizațiile care au nevoie de o urmă mai detaliată, sudo poate fi evaluat cu jurnalizare I/O și auditd poate urmări evenimente suplimentare, dar aceste mecanisme trebuie configurate înaintea incidentului și testate din perspectiva volumului, confidențialității și retenției.
Întrebări frecvente
Raportul poate identifica persoana care deținea cheia privată?
Nu. El identifică amprenta cheii acceptate și conturile locale în ale căror fișiere authorized_keys apare. Atribuirea unei persoane necesită evidența administrativă a cheilor și verificarea faptului că acestea nu erau partajate.
De ce apare NEMAPAT lângă o autentificare acceptată?
Amprenta nu a fost găsită în fișierele locale inventariate. Cheia putea fi eliminată ulterior, păstrată într-o altă locație configurată prin AuthorizedKeysFile sau furnizată de un mecanism extern.
Raportul arată toate comenzile executate în sesiunea SSH?
Nu. El include evenimentele sudo care au formatul analizat, dar nu înregistrează automat fiecare comandă dintr-un shell și nici activitatea care nu trece prin sudo.
Pot rula laboratorul dacă sesiunea suspectă este încă activă?
Colectorul citește jurnalul fără să repornească SSH, dar FĂRĂ EVENIMENT DE ÎNCHIDERE poate însemna că sesiunea este încă activă sau că intervalul se termină prea devreme. Păstrează prima colectare și extinde analiza într-un director nou.

















































