Version 2.23.3 · MIT · Python 3.8+ · zero third-party dependencies
RCEKit is an RCE detection & confirmation toolkit for authorised penetration testing, red teaming, and security research. Point it at a target you are allowed to test — a URL or a captured HTTP request — and it tells you what actually executed, backed by proof, not a "maybe".
A confirmed verdict means the target computed or executed a value only
execution could produce — random arithmetic, a template evaluation, a written
token — checked against a payload-free control. Reflection, coincidence, and
jitter can't fake it, so confirmed is something you can put in a report.
RCEKit confirms RCE through multiple methods under one CLI. Below it is pointed at real, publicly-documented CVEs in production software — each verdict differenced against a payload-free control:
| RCE class | --methods |
Real-world target | Verdict |
|---|---|---|---|
| OS command injection (results-based) | reflected |
Webmin 1.910 — CVE-2019-15107 | confirmed |
| Expression injection (OGNL) | eval |
Apache Struts2 — S2-001 | confirmed |
| Blind / out-of-band (Log4Shell/JNDI) | OOB listener | Log4Shell — CVE-2021-44228 | confirmed |
| Blind command injection (no output) | time |
Webmin 1.910 — CVE-2019-15107 | needs-review |
git clone https://github.com/kabiri-labs/rcekit.git
cd rcekit # Python 3.8+, standard library only — nothing to installOr take just the one file — the payload corpus is built in, so rcekit.py
runs on its own with nothing beside it. On a client jump box, an air-gapped host,
or anywhere pip install is not an option:
curl -O https://raw.githubusercontent.com/kabiri-labs/rcekit/main/rcekit.py
python rcekit.py --doctor # confirms the corpus it will run withPut a FUZZ marker where your input lands (or select a parameter with -p when
using a captured request), and ask RCEKit to prove RCE:
python rcekit.py --acknowledge-consent \
--verify-url "https://target.example/lookup?host=FUZZ" \
--methods reflected,eval[detect] methods: reflected, eval
[detect] sent 13 probes: confirmed=4, negative=9
[detect] CONFIRMED execution (4):
[reflected/unix/raw] ; echo RKYZRIP$((540141+314681))RKFWVFS$(echo RKBWOOC)RKYZRIP
(target computed 'RKYZRIP854822RKFWVFSRKBWOOCRKYZRIP' — random operands, absent from control)
No external infrastructure, no config file. If nothing is vulnerable you get a
clean negative, not a false alarm. That's the whole idea.
Don't take the GIFs on trust — reproduce them yourself against dockerised Webmin and Struts2 targets in about five minutes.
Next: the field guide walks the real situations — captured requests, WAFs, filtered separators, quoted sinks, blind and no-egress targets — one worked example each.
Finding an RCE candidate is easy. Proving it — reliably, without crying wolf — is the hard part:
- Scanners flag "possibly vulnerable" and bury the real finding under false positives.
- Blind RCE usually forces you to stand up interactsh/Collaborator just to confirm.
- Each RCE class (command injection, SSTI, code injection) needs a different confirmation trick.
- A report full of "maybe" findings wastes triage time and burns your credibility.
RCEKit answers with two verdict tiers that are never merged:
| Tier | Meaning |
|---|---|
confirmed |
Execution proven — the target returned a value it could only produce by executing your input, and that value is absent from a payload-free control. |
needs-review |
A real candidate worth a look (e.g. a blind timing signal), but not proof on its own. |
Anything else is negative or inconclusive. The moment "confirmed" and "maybe"
blur together, "confirmed" loses its meaning — so RCEKit keeps them apart, by design.
The other tools in this space are built to get you in. RCEKit is built so the finding survives someone else's scrutiny — the client's retest, the triage queue, the report review. That difference shows up three times.
You rarely know the class before you test. Covering an unknown sink with single-class tools means running each in turn and rebuilding the request for each one:
| Can confirm | RCEKit | commix | SSTImap | Nuclei |
|---|---|---|---|---|
| OS command injection | ✅ | ✅ (its whole scope) | — | per template |
| Expression injection / SSTI | ✅ | via its eval-based technique | ✅ (its whole scope) | per template |
| Blind — timing | ✅ as a separate tier | ✅ | ✅ | — |
| Blind — out-of-band | ✅ built-in listener | — | — | via interactsh |
| No-egress — write & fetch back | ✅ | ✅ | — | — |
| All of the above, one CLI, one run | ✅ | — | — | — |
Coverage per each project's own documented technique list. SSTImap is the maintained successor to tplmap, which its author has marked unmaintained.
# Command injection, expression injection and blind timing against the same
# parameter, in one pass, with zero infrastructure
python rcekit.py --acknowledge-consent -r request.txt -p host --methods reflected,eval,timeA tool reports what it found. RCEKit also reports what it refused to believe —
inconclusive is a verdict of its own, for evidence that showed up but could not
be attributed to execution:
[detect] methods: reflected, eval
[detect] sent 13 probes: confirmed=0, inconclusive=2, negative=11
Those two would have been someone else's finding. Four mechanisms produce that verdict, and they run on every confirmation:
- A payload-free control request. Evidence must be present with the payload
and absent without it. Anything in both is
inconclusive, not a finding. - A same-token inert control. A second request carries the identical random token in a non-executing form. A target that merely echoes input fails here — which is how a reflection is separated from an execution.
- Random operands, never fixed strings. The oracle is a tag-wrapped sum or a
boundary-fenced product computed fresh each run. Echoing the payload returns
the literal
$((a+b)); only execution returns the value. - Encoding-aware evidence search. A sink that base64-, hex-, URL-, HTML- or unicode-escapes its output still confirms — the raw body is checked first, so decoding only ever turns a missed hit into a hit, never the reverse.
And timing never self-confirms: a linear 0/N/2N regression is capped at
needs-review and reported in its own tier, because it produces no computed
value. confirmed and maybe are never merged into one word.
The controls a client's rules of engagement actually ask about, in the tool rather than in your notes:
| Consent gate | Nothing exploitative generates or fires without --acknowledge-consent. |
| Execution plan | Prints the exact payload count, safety tiers and any outbound callback destinations before the first request goes out. |
| Safe by default | Reverse shells, credential access, cloud metadata, lateral movement and container escape are held back until you raise --verify-active-risk; persistence and backdoors need a second flag on top. |
| Cleanup commands | The file method changes target state, so every finding prints the exact rm/del to undo it — paste it into the report. |
| Redacted audit trail | Every run lands in exploit_audit.log, recording that a credential header was sent, never its value. |
| Watermarking | --watermark stamps a traceable token into each payload, so a payload found in the client's logs months later is attributable to your run. |
| No third-party callbacks | The OOB listener is yours. Nothing is routed through a public interaction server, which some engagements forbid outright. |
| One stdlib file | rcekit.py runs alone — jump box, air-gapped host, anywhere pip install is not an option. |
Want a shell rather than a verdict? commix and SSTImap continue into
post-exploitation; RCEKit stops at proof by design. Sweeping thousands of hosts
for known CVEs? That is Nuclei's job — and RCEKit writes Nuclei templates
(--output-format nuclei), so it feeds your scanner instead of competing with it.
One CLI, one --methods flag, covering the main paths to RCE:
| RCE class | --methods |
How RCEKit proves it |
|---|---|---|
| OS command injection | reflected |
Makes the shell compute $((a+b)) on random operands and collapse $(echo TAG); confirms the result, never the literal expression. |
Code / expression injection — SSTI, SpEL, OGNL, Groovy, eval() (CWE-94) |
eval |
Injects a*b in every common template syntax (${…} {{…}} #{…} %{…} <%=…%> @(…), bare); confirms the product appears while the literal a*b does not. |
| Blind command injection (no output) | time |
Fires a controlled 0/N/2N delay series and confirms the response time tracks the delay linearly; reported needs-review (jitter can't fake it, but timing isn't a computed value). |
| Internal / no-egress targets | file |
Writes a random token to a web-reachable file and fetches it back — proving execution plus a write primitive, with no external listener. |
| Blind / out-of-band — Log4Shell/JNDI, exfil, async | (OOB listener) | Built-in HTTP/DNS listener receives callbacks and correlates each to the exact payload. |
Mix them freely: --methods reflected,eval,time runs all three and reports each
tier separately. If a combination builds no probes at all, RCEKit says so and
exits non-zero — a run that tested nothing is never reported as a clean result.
Honest scope. RCEKit confirms RCE that is reachable by injecting into a request and interpreted by a shell or an evaluator. It does not cover memory-corruption bugs (buffer overflow, UAF), argument injection into a no-shell
argvarray, or confirm deserialization gadget chains beyond a timing signal — those are different problems. It aims to be excellent at the injection-driven RCE classes above rather than mediocre at everything.
Each row is a worked example in the field guide — the command, what it sends, and how to read what comes back.
| Situation | Go to |
|---|---|
| I have a URL and a parameter | Point at a URL |
| I have a request saved from Burp | Point at a captured request |
| The app is JSON / the payload keeps getting mangled | Landing the payload intact |
| I don't know which class it is | Choosing methods |
The sink strips ; |
When the sink filters separators |
My input lands inside 'quotes' |
Injecting inside quotes |
| The sink runs my input as the whole command | Whole-command sinks |
| There's a WAF | Working around a WAF |
| No output comes back at all | Blind targets |
| No output and no egress | No-egress targets |
| The sink is behind a login or a file upload | Multi-step chains |
I got needs-review / inconclusive / error |
Reading the results |
| It says the corpus is unusable | Troubleshooting |
| Verify it yourself | Reproduce the confirmations above on your own machine, against dockerised vulnerable targets. Five minutes. |
| Field guide | Example-driven walkthrough of every real situation, from a first probe to multi-step chains. Start here. |
| Payload generation & exports | RCEKit as a payload generator: target profiles, and Burp / ffuf / Nuclei exports. |
| Reference | Every flag, environment, category, context, encoding and code-execution sink. |
| CHANGELOG.md | What changed in each release, and what to re-check when upgrading. |
| CONTRIBUTING.md | How to add sinks, categories, encodings and detection methods. |
| SECURITY.md | Reporting a vulnerability in RCEKit itself. |
- Consent gate — exploitation generation and verification require
--acknowledge-consent;--detection-onlyis benign and does not. - Safe by default — verification fires only low-impact proofs; reverse shells,
download-execute, credential access, lateral movement, container escape,
cloud-metadata and OOB payloads are held back until you raise
--verify-active-risk. Destructive payloads (persistence, backdoors) are never fired without--verify-allow-destructive. An execution plan prints exactly what will be sent before anything fires. - Safety tiers —
safe/intrusive/stateful, filtered by--max-safety. - Audit & logging — every exploitation/verification run is recorded in
exploit_audit.log;--watermarkembeds a traceable token; execution logs go torcekit.log. - Corpus integrity — a corpus that is corrupt, or an explicit
--template-filethat is missing, makes RCEKit refuse to run and exit non-zero rather than silently generate nothing (--doctorchecks it). Only an absent default corpus file falls back to the built-in copy, and it says so when it does.
This toolkit is intended for authorised penetration testing, security research, education, and defensive training only. Never use it against systems without explicit permission — unauthorized testing is illegal.
python -m unittest discover -s tests # dependency-free test suiteContributions welcome — new sinks/categories, encodings, environments, detection
methods, bug fixes, and docs. Payload bases live in editable JSON templates
(templates/payloads.json), so most coverage extends without touching the Python
source. After changing the corpus, refresh the built-in copy that ships inside
rcekit.py:
python tools/embed_corpus.py # --check verifies it is currentThe test suite fails if the two ever drift. See CONTRIBUTING.md.
MIT — see LICENSE.



