blog
79 entries. Defensive, tested, no invented commands.
- beginner
The 10 commands that cover 90% of your admin day
Ten commands cover most of what a sysadmin does day to day: uptime, disk, memory, open ports, top processes, who is logged in, recent logins, recent logs, current user, kernel version.
- beginner
Ctrl+R: the search that changes your life
Ctrl+R is bash history search. Seed a few commands, search backward, and stop rebuilding long lines from memory.
- beginner
Remember tar flags for good (a mnemonic)
Remember tar as c/t/x plus z and f. Create, list, and extract one archive and the flags stop feeling random.
- beginner
SSH keys instead of passwords - do it right once
Generate an Ed25519 key, check its fingerprint, and set the only two permissions that usually matter for SSH key auth.
- intermediate
netshun auto-heals the firewall
netshun detects its nftables state was wiped and rebuilds every ban automatically within seconds.
- beginner
Ban SSH brute-forcers automatically
netshun watches your logs, spots brute-force attempts, and drops attackers into nftables automatically.
- beginner
Your .env is still in your git history
Adding .env to .gitignore stops future commits, but it does not remove the file from history. Anyone who clones the repository can still read the old secret with a single command.
- beginner
The account you forgot about still works
A leftover account with a usable password is a way in that ignores all your SSH hardening. Listing accounts by password status shows which ones can still log in, and locking them takes one command.
- beginner
kill -9 is not a fix
A process cleans up after itself when it receives TERM. Sending KILL skips that entirely, so lock files, sockets and temporary state survive and the next start finds a mess.
- intermediate
This bot waits 2.7 minutes between guesses
Real login attempts against an exposed server, spaced roughly 160 seconds apart. Slow enough that fail2ban's default of five retries in ten minutes never fires, so the attempts continue unnoticed.
- intermediate
A bot broke in and checked whether I was real
Captured on a server exposed this morning. After a successful login the bot profiled the hardware, then ran a sequence that writes a file, makes it executable and runs it, to work out whether the shell was genuine before committing.
- intermediate
What they actually installed
A payload uploaded to an exposed server after a successful login. It is UPX packed, so its contents stay hidden until it is unpacked. Unpacked, the strings identify it immediately as a Monero miner.
- intermediate
The malware cleaned my server before infecting it
A cleanup script uploaded to a compromised server before the actual payload. It disables a rival miner by name, strips cron of competing persistence, wipes the temporary directories and kills processes disguised as system tools.
- intermediate
They shipped the miner for five CPUs at once
After one successful login, five binaries arrived in the same second. Same payload, compiled for ARM, i386, ARM64, x86-64 and RISC-V, because the attacker has no idea what hardware the credentials will open.
- advanced
The ban that locks you out instead of them
A blackhole backend bans an address by null-routing it, which drops outbound packets to that address. It does not stop an inbound connection, and if the banned address is your own workstation the server can no longer answer you.
- beginner
They are not attacking your server
Login attempts recorded on an exposed Linux server. Several of the usernames are factory defaults for network hardware, not for servers, which shows the attempts are sprayed at every address rather than aimed at one machine.
- beginner
36 minutes from power on to break in
A fresh server with no DNS name and no links to it. Twelve minutes after going online the first scanner arrived, and thirty six minutes after boot an automated login succeeded.
- intermediate
Every scanner tells you its name
Every SSH client sends a version string before authentication. Collected from a few hours on an exposed server, those strings separate real clients from mass scanners and custom bruteforce tools, which makes them useful to alert on.
- beginner
What they actually guess
Every login attempt recorded on an exposed server, counted. Two account names account for most of the traffic, and the passwords are the short obvious ones rather than anything sophisticated.
- beginner
The quiet hour
Twenty-eight hourly SSH login buckets, sorted. One hour inside the peak activity dropped to seven attempts while the hours either side exceeded four hundred each.
- beginner
Not your validator
Three of the top six attempted usernames are Solana validator account names, with passwords to match. Not a generic wordlist — a purpose-built one aimed at a specific category of operator, sprayed at every reachable SSH port.
- beginner
No domain, nothing hosted
[Note: the compromised third-party domain shown in this video was redacted from this page after publication; the site operator is a victim, not the attacker.] A web server with no domain name and nothing hosted attracted scanners targeting four unrelated product ecosystems within two hours of starting. One scanner's User-Agent was a URL identifying the WordPress site it was hunting.
- beginner
The payload arrived over SSH
A seven-second session pushed 3,716,336 bytes into a file over an already-authenticated SSH channel, without a single outbound connection. Egress filtering that blocks outbound HTTP does nothing when the payload arrives over the channel the attacker already holds.
- beginner
TLS at port 22
Four sessions sent TLS ClientHello bytes to port 22 instead of an SSH banner, from four distinct /16 networks across two days. Port scanners apply their configured protocol to every open port without first checking whether the service matches.
- beginner
cat /bin/echo
Two sessions ran cat /bin/echo immediately after login to read the ELF machine-type field and identify the server's CPU architecture. No uname or compiler needed — the architecture byte sits at offset 18 in a file that exists on every Linux system.
- beginner
Service account dictionary
Two source networks tried sonar, hadoop, and esuser credentials in sequence against the honeypot, both succeeding on esuser. All three are factory defaults: SonarQube ships sonar, Apache Hadoop ships hadoop, and Elasticsearch ships esuser.
- beginner
Discord webhook C2
Strings analysis of the thirty-percent-complete Go binary capture reveals a discord.com webhook address. The binary uses Discord as its command-and-control channel rather than a dedicated server, making it invisible to egress rules aimed at known-bad infrastructure.
- beginner
Two servers, one filename
Two sessions hours apart pulled a file named linux from two unrelated networks, each trying three delivery methods in turn and falling back to a raw shell socket that needs nothing installed. The binary itself carries no settings. Its configuration arrives as an encoded command-line argument, which is why taking one server offline changes nothing.
- beginner
It manages two miners
Ten payloads from this honeypot were small, single-purpose miners with the pool address compiled in. The eleventh is nine megabytes of Go, and it carries pool configuration for two different mining programs, libraries for SSH and file transfer, and the PAM authentication stack. It is not a miner. It is the process that installs and supervises one.
- beginner
Someone else's DNS
A fresh cloud address received 157 web requests carrying the host header of a shop domain it does not own, from 15 networks, most with no user agent at all. The DNS record still points at the new tenant. The paths requested are not pages but PHP files by name, which is what people knock on when they are looking for backdoors they left on the old site.
- beginner
They guess where you installed it
A bare web address logged 543 requests across 268 distinct paths in a single day. The pattern that repeats is one WordPress REST endpoint requested under six install prefixes, three forms each, exactly twelve times per path. Hiding an application in an unusual subdirectory does not defeat that, because the list of usual subdirectories is short.
- beginner
The key you cannot delete
Thirty-six sessions on one honeypot appended an SSH key to authorized_keys and then set the immutable and append-only attributes on the file, so deleting the line fails even for root. All thirty-six planted the same public key, whose comment dates it to June 2023. Detection is one command: lsattr.
- beginner
They broke in and did nothing
Fifteen days on a single honeypot: 61,751 failed logins, 36,978 sessions that got in, and only 556 sessions that typed even one command. Most credential guessing is not an attack in progress. It is inventory being collected, to be used or sold later.
- beginner
A CPU from 1984
Twelve distinct ELF architectures were dropped on one honeypot in two weeks, from Motorola 68020 and Renesas SH to ARM, MIPS in both byte orders, and RISC-V. Nobody profiles a target this carefully. They compile for everything because they have no idea what they are landing on.
- beginner
Your host keys are readable
CVE-2026-46333, known as ssh-keysign-pwn, is a race in the Linux kernel process-exit path that lets an unprivileged local user capture open file descriptors from an exiting setuid process, including SSH host private keys and /etc/shadow. Patch the kernel; until then, raising kernel.yama.ptrace_scope closes the window. Red Hat tracks it as RHSB-2026-004.
- beginner
The version number lies
Comparing your installed version against the upstream release that fixed a bug is the wrong check on a distribution package. Debian's openssh-server reports 10.0p2 while its changelog states it backported security fixes from 10.3p1, and references twenty CVE identifiers. The version number is deliberately held still.
- beginner
The version that no longer exists
The public npm registry still records when keyv 6.0.0 was published — 2026-08-04 at 09:35 UTC — but the version itself is gone and the latest tag points back at 5.6.0. That is the ChainDrop supply-chain incident seen from the outside, in three queries anyone can run.
- beginner
Ubuntu will not upgrade you
Ubuntu 26.04 LTS removed cgroup v1 from the kernel and systemd no longer mounts the legacy hierarchy, so hosts whose container runtime still depends on v1 are blocked from upgrading. One stat command tells you which hierarchy you are on before you start.
- beginner
A label is attacker input
containerd's CRI plugin passed labels from an image config straight into the container without validation, so a LABEL instruction written by whoever built the image could end up executed on the host by a plugin that consumes labels. Tracked as CVE-2026-53488 at CVSS 8.3; it does not require checkpoint or restore to be enabled.
- beginner
Read it before you build it
Arch disabled AUR package adoption on 30 July 2026 and then blocked all AUR pushes on 1 August, after attackers took over orphaned packages and pushed malicious commits. It was the third supply-chain wave against the AUR since June. An AUR entry is a build recipe that runs on your machine, so the defence is to read it first.
- beginner
Ten out of ten, no login
CVE-2026-21858 in the n8n workflow automation platform scores 10.0 and needs no authentication. The advisory database gives the exact affected range, 1.65.0 up to 1.121.0, and the version that fixes it. Querying it directly beats reading a headline, and works for anything you self-host.
- beginner
It assumes nothing is installed
A profiling script seen in nine sessions from four networks asks every question four or five different ways: uname, then two absolute paths, then busybox, and failing all of that it greps the CPU file directly. It also reads the device tree, a file that only exists on single-board hardware. It was not written for servers.
- beginner
Fourteen years and still knocking
A bare web address logged 14,141 requests. Three hundred were POSTs and seventy-seven carried a body, nearly all of them the same query-string trick against PHP running as CGI, a flaw disclosed in May 2012. The rest of the traffic is not hunting an application at all: it asks for dotfiles, git config and cloud credentials.
- beginner
They don't have a password list
One source network tried 390 distinct usernames and gave 329 of them the identical treatment: the name as the password, then the name with 1, 123, 1234 and 12345 appended. Five guesses a second apart, then it moves on. No dictionary is being carried at all — the password is derived from the account name.
- beginner
They were already knocking
A bare address with no WordPress on it logged 1,371 WordPress-shaped requests from 70 networks, 38 of them against wp-login.php. CVE-2026-64638 is a pre-authentication flaw on exactly that page, scored 8.9, affecting every release from 4.7 to 7.0 and fixed in 7.0.3.
- beginner
The file it leaves behind
Twenty-two sessions wrote a hidden file into /tmp containing one of four passwords. No password change was observed in those sessions and nothing read the file back, so its purpose is unclear — but it is consistent, and that makes it something you can search for.
- beginner
Nine, out of everything
The Known Exploited Vulnerabilities catalog is a public JSON file listing flaws confirmed to be under active attack rather than merely severe. It holds 1,665 entries, and only nine were added in the first eleven days of August. That is a queue you can actually work through, unlike the thousands of advisories published in the same period.
- beginner
The scanner is a parser
Seven ClamAV vulnerabilities were disclosed in August 2026, two of them with public proof-of-concept code, both in the zip parser. The vendor gives one root cause for all seven: unsafe parsing of content an attacker fully controls. A scanner understands twelve file formats, and each one is a parser pointed at hostile input by design.
- beginner
They checked for a graphics card
Fifteen SSH sessions on the honeypot pasted the same fingerprint script before doing anything else. It harvests nine facts about the host - kernel, architecture, processor model, core count, uptime, last logins - and one field the attacker's own code labels as the graphics card, filled by listing PCI hardware and filtering for display adapters, with a busybox fallback for stripped-down systems. Hardware decides the payload: a machine with a real card earns more as a miner than as another node in a botnet.
- beginner
Most of your traffic is not a browser
One week of web honeypot traffic, grouped by the client that sent it. A hundred and eighty-five distinct user agent strings arrived, but a single Go HTTP library accounts for more than half of every request logged. Named scanning tools sit further down the list, and a large block sends no user agent at all. On an address with nothing hosted and no domain pointing at it, this is the resting state of the internet, not an attack.
- beginner
They are asking for your cloud keys
Five hundred and thirty-two web requests in one week carried a link-local cloud metadata address inside a query parameter, url-encoded so it reads as noise in a log. Decoded, it is an instruction to the server: fetch this address and hand back what it returns. On a cloud instance that address answers with temporary identity credentials. Off one it answers with nothing, which is why the same probe is sprayed at every address that responds.
- beginner
Two thousand guesses for one file
In one week the web honeypot logged five thousand three hundred and sixty requests hunting for an environment file, spread across two thousand one hundred and eleven distinct paths. The scanner does not know which framework is running, so it walks every layout it has ever seen - the plain path, the stale backup, the production copy, the framework subdirectory. It costs nothing to guess wrong and everything to guess right, because that one file usually holds database passwords and API keys in plain text.
- beginner
The scanner that announces its own address
Seventy-nine requests in one week arrived with a url in the client name field instead of a client name, and the url points at the setup page of a content management system on somebody else's site. The scan is running from a machine that was taken over during its own installation and never cleaned up, and a misconfigured tool leaks that address into every request it sends. The site name is masked here because the operator is a victim, not the attacker.
- beginner
It is pretending to be your SSH daemon
Three times in one week an intruder dropped a binary named sshd into a hidden directory named after a 19-digit random number, and launched it in the background with 51 target addresses as command-line arguments. The name is the disguise: every Linux box runs a real sshd, so a process by that name draws no attention in a process listing. The targets are masked to /16 here. The machine was not the goal, it was the next hop.
- beginner
The exploit is in the User-Agent
Forty-one requests in one week carried a Log4j lookup expression in the User-Agent header instead of a client name. Twenty-seven of them wrap an environment variable holding Amazon cloud credentials, so a vulnerable logger would resolve a hostname built out of the secret itself and hand it to whoever runs that domain. No response is needed - the lookup is the exfiltration. The remaining fourteen point at the loopback address and only test whether the expression is evaluated at all.
- beginner
They brought their own key
Seven sessions in one week wrote a private key to disk and used it to pull their payload with scp, from an account on the attacker's own server. Only if that fails do they fall back to wget and curl. The key material is redacted here and the host is masked to /16. Most egress rules are written around web downloads, and a file copy over an SSH channel you already permit does not look like one.
- beginner
They are hunting for a wallet
Thirty-six requests across twenty distinct paths went looking for cryptocurrency material on a server that hosts nothing: seed phrases, mnemonic files, Ethereum keystores. Several of them are prefixed with a repository directory, which is the whole idea - the scanner is betting that somebody committed a wallet file into version control and then published the repository folder along with the site. There is no exploit in any of this. It is a filename guess against a file that should never have been reachable.
- beginner
Eighteen ways to write one filename
Nineteen requests arrived for a web server configuration file, written eighteen different ways. Trailing tabs, vertical tabs, form feeds, file and record separators, a non-breaking space, a semicolon, and long hex strings glued to the end. Every variant is a different string, so a rule matching the exact filename does not fire, while a permissive path handler may still resolve them to the same file. The gap between what the filter compares and what the filesystem opens is the entire technique.
- beginner
Two scanners in one request
Ten requests arrived carrying a repository directory, a colon, a port number and then the path of an internal service - a cluster manager on one port, a dashboard login on another. As a request it is meaningless: a port in the middle of a path is just text. Two probe lists were concatenated instead of being tried separately, which means the tool was never checked against a single response. It is a small window into how much scanning runs completely unattended.
- beginner
The timestamp that does not lie
Changing a file's permissions does not touch its modification time, because the contents did not change. It does update the change time, which records inode changes: permissions, ownership, link count. Most tools show you the modification time by default, and an attacker can set that one to whatever they like with a single command. There is no ordinary way to set the change time, which is what makes it worth comparing when a file looks suspicious.
- beginner
Eleven programs that run as root
On a minimal Debian install, eleven programs carry the setuid bit, which means they run as the file's owner rather than the user who started them - and that owner is root. It is not a misconfiguration; changing your own password or mounting a filesystem needs privilege a normal user does not have. The value of the list is that it is short and stable. An entry you do not recognise on a machine you did not change is worth more attention than most alerts.
- beginner
Your config is not what is running
In a stock SSH configuration the password authentication line is commented out. Reading the file, it looks disabled. It is not: a commented line means the compiled-in default applies, and that default is on. Asking the daemon for its effective configuration prints what it will actually enforce, which is also where you find that the effective setting name does not always match the one documented in the file. Verify hardening against the running service, never against the file you edited.
- beginner
Tor hid the address, not the key
The same SSH public key fingerprint was offered for authentication on three separate days from three different source addresses. Checked against the exit list the Tor Project publishes itself, all three addresses are Tor exit nodes. The circuit changed every time; the key did not, because the key is what the operator carries and the exit is only where the traffic left the network. Addresses are masked to /16 here; the match against the exit list is run against the unmasked values without printing them.
- beginner
A quarter of them are already post-quantum
One week of SSH key exchange negotiations, grouped by the algorithm each attacking client asked for first. Second place goes to a hybrid of a classical elliptic curve and a lattice scheme standardised in 2024, chosen first by 6,751 clients, with a second post-quantum option further down at 43. Nobody picked these deliberately - they are attacking from whatever their distribution ships, and their distribution already ships post-quantum key exchange.
- beginner
Somebody is counting the AI servers
Twelve POST bodies this week decoded to well-formed handshakes for the Model Context Protocol, the standard AI assistants use to reach tools and data. Ten are generic; two identify their sender as an internet-census scanner and speak a newer revision of the protocol than the other ten. Nothing here runs an AI backend, so every one of them fails. The point is that a scan category which did not exist a year ago is now routine background traffic.
- beginner
The username is hiding in the packet
Five sessions completed a normal SSH handshake and then sent an authentication request the server refused to parse. The raw bytes are still logged, and an SSH auth request begins with a length-prefixed username, so the name is recoverable directly from the hex. Two names come out, both random strings, spread across five different source addresses - which makes this one tool generating a throwaway identity per run, not five unrelated attackers.
- beginner
They are already looking for your GraphQL
Ninety-one requests in one week went hunting for a GraphQL endpoint on an address that hosts nothing, spread across nineteen distinct paths. The plain endpoint, the versioned variants, and then the developer tools - a console, an explorer, a playground, a schema browser - none of which belong on a public address. Several combine the hunt with a repository directory, betting both mistakes were made together. It matters this week because a critical unauthenticated flaw in a widely self-hosted platform was published days ago, in exactly that layer.
- beginner
The file is gone, the space is not
The disk reports 54% used and 4.0G taken, but the directory you just cleared lists nothing at all. Both readings are correct. On Linux a file is only truly released when the last process holding it open lets go - until then the name is gone from the directory while the data stays on disk. Listing open files whose name is already deleted finds the holder, here one process sitting on 536870912 bytes that no longer has a name. Stopping that process, or restarting the service, returns the space immediately. This is why deleting more files does not help when a log was rotated out from under a running daemon.
- beginner
Every service has a security score
Systemd can score how much of the machine a service could reach if it were compromised, and on a stock install most units come back UNSAFE. Asking about a single service prints every available protection one per line and marks the ones that are switched off - here ssh scores 9.6, where a higher number means more exposure. Two of those switches, NoNewPrivileges and PrivateTmp, can be read directly and both return no. None of this is a misconfiguration: these protections are simply not the default, which is exactly why the marked list works as a hardening checklist rather than an alert. Note this is systemd-analyze security, which scores exposure, not systemd-analyze verify, which only checks unit syntax.
- beginner
Your package manager remembers
Every installed Debian package records a checksum for each file it ships, written at install time into /var/lib/dpkg/info. Verifying the whole system walks that record and prints only the files that no longer match - on a clean machine it prints nothing at all. Here it names one file, and the 5 in the flag column means the checksum is wrong. Asking which package owns the file, then comparing the checksum on disk against the one recorded at install, shows the two do not agree. No agent, no external database and no internet lookup were involved: the reference was already on the machine, put there by the installer. This needs nothing extra installed, which makes it the first check to run on a box you have inherited.
- beginner
Not setuid, still root
Auditing which programs can run as root usually means searching for the setuid bit, and on a clean Debian install that list is short and stable. It is also incomplete. Linux capabilities split root's power into individual pieces that can be attached to a binary directly, with no setuid bit involved - so a file with ordinary rwxr-xr-x permissions, invisible to a setuid search, can still hold cap_setuid and change its user id. Compare the permissions against a genuine setuid program such as passwd, which shows an s where the execute flag would be, then list file capabilities separately. On most machines the capability list is empty, which is what makes any entry in it worth reading carefully.
- beginner
Two root accounts, one name
Searching the account file for the word root returns a single line, which looks like confirmation that only one account has full privilege. It is not. On Linux privilege comes from the numeric user id, not the name, and any account carrying id 0 is root as far as the kernel is concerned. Filtering on the third field instead of the name reveals a second entry that the name search never showed, and asking the system to resolve it returns uid=0(root). A normal service account such as daemon returns uid=1 and stays there. An extra id 0 account is one of the quietest persistence mechanisms there is, because every name-based check walks straight past it - so audit the id field, not the label.
- beginner
Not in the process list
Passing a database password to a service as an environment variable keeps it out of the process list - the full command line shows only the program and its argument, with no secret in sight. It does not keep it out of the process itself. Every running process exposes its environment as a file in /proc, and reading that file prints every variable the process was started with, including the password. The file is mode 400, readable by the user the process runs as and by root, which sounds restrictive until you remember that is exactly the account an attacker occupies after compromising that service. Environment variables are fine for configuration and poor for secrets; prefer a file with tight permissions, or a secret store the process reads at startup.
- beginner
The endpoint that prints your config
In one week 168 requests went hunting for the built-in management endpoints a widely used Java framework ships for health checks and metrics, spread across 32 different spellings of the path. 40 of them targeted the endpoint that lists the application's configuration properties. Three did not request it directly: they placed a traversal sequence in front of the path, so a proxy rule blocking the endpoint by name never matches while the application behind still receives the request. None of this is an exploit. Recent framework versions expose only health and info over HTTP by default and mask values that look like credentials, so what the scan is really testing is whether yours was configured to expose more than that, without authentication in front of it.
- beginner
One file anyone can rewrite
Searching the whole filesystem for files any user can write should come back empty on a clean machine. Here one file comes back: a configuration file owned by root whose permissions end in write-for-everyone, mode 666. That combination is the dangerous one - a service running as root reads the file, and any account on the machine can change what it reads, which turns an ordinary user into whatever that service does. The directory holding it is unremarkable. The temporary directories are world-writable too, but they carry a sticky bit and display drwxrwxrwt, which means a user can only remove their own files there; that is deliberate and not a finding. The distinction worth learning is sticky shared scratch space versus a writable file something privileged depends on.
- beginner
The account with no password
A locked account and an account with an empty password look similar in a listing and are not the same thing. Filtering the shadow file on an empty second field finds the difference: the raw line shows two colons with nothing between them where every other account carries a hash, and the status command reports NP for no password while everything else reports L for locked. Whether that is exploitable depends on the authentication stack, and on a stock Debian install /etc/pam.d/common-auth carries nullok, which tells the password module to accept an empty password. Remote logins over SSH still refuse empty passwords by default, so this is a local door rather than a remote one - but it is a door, and it is not usually there on purpose.
- beginner
Every file you create is readable
The shell carries a mask that decides which permission bits a newly created file does not get, and the stock value on Debian is 0022. Under it, every file a process creates comes out readable by the owner, the group and everyone else - which is harmless for a note in a home directory and considerably less harmless for a database dump, a backup archive or an exported key. Those are usually written by a script that never considered permissions at all. Tightening the mask to 077 and running the same touch produces a file only its owner can read, with no chmod anywhere. The difference is a single line placed before the job runs, and it applies to everything that job creates rather than to the one file somebody remembered to fix afterwards.
- beginner
The plus sign at the end
A long listing shows -rw-rw----+ on this file, and both halves of that are misleading. The trailing plus sign is the only hint that an access control list is attached, and it is easy to miss. Asking for the list shows an entry the listing never named: a second user granted read and write individually. Worse, the middle triplet that everyone reads as the group permission is not the group permission once an ACL exists - it is the mask, an upper bound on what the named entries may receive, while the real group permission here is read-only. So the familiar output understates who has access and overstates what the group gets. Recursively listing only the files that carry ACLs turns this from a trap into a two-second audit.
- beginner
The program that no longer exists
Deleting the executable immediately after starting it is standard behaviour for anything that does not want to be analysed: the process keeps running while the file is gone before anyone looks. The process list still reports the path it was started from, and that directory is empty. The kernel, however, keeps a link to the executable for as long as the process lives, and that link still resolves - it names the original path and marks it deleted. Unlinked is not destroyed. Reading through that link copies the binary back out byte for byte, ready to hash or analyse, with nothing installed and no forensic toolkit involved. The one irreversible mistake is killing the process first: the last reference goes with it, and only then is the file actually gone.