mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-17 16:07:18 +02:00
add rewrite support for optional request path and method
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
+64
-16
@@ -553,6 +553,9 @@ func (m *ProxyHandler) prepareRequestWithoutSession(req *http.Request, reqCtx *R
|
||||
if hostConfig.Rewrite != nil {
|
||||
for _, replacement := range hostConfig.Rewrite {
|
||||
if replacement.From == "" || replacement.From == "request_body" || replacement.From == "any" {
|
||||
if !m.rewriteRuleMatchesRequest(replacement, req) {
|
||||
continue
|
||||
}
|
||||
engine := replacement.Engine
|
||||
if engine == "" {
|
||||
engine = "regex"
|
||||
@@ -949,7 +952,9 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithVariables(resp *
|
||||
if hCfg.Rewrite != nil {
|
||||
for _, replacement := range hCfg.Rewrite {
|
||||
if replacement.From == "response_header" || replacement.From == "any" {
|
||||
headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx, "")
|
||||
if m.rewriteRuleMatchesRequest(replacement, resp.Request) {
|
||||
headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -959,7 +964,9 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithVariables(resp *
|
||||
if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil {
|
||||
for _, replacement := range proxyConfig.Global.Rewrite {
|
||||
if replacement.From == "response_header" || replacement.From == "any" {
|
||||
headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx, "")
|
||||
if m.rewriteRuleMatchesRequest(replacement, resp.Request) {
|
||||
headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1011,7 +1018,7 @@ func (m *ProxyHandler) rewriteResponseBodyWithContext(resp *http.Response, reqCt
|
||||
|
||||
// build variables context for template interpolation
|
||||
varCtx := m.buildVariablesContext(resp.Request.Context(), reqCtx.Session, reqCtx.ProxyConfig)
|
||||
body = m.applyCustomReplacementsWithVariables(body, reqCtx.Session, reqCtx.TargetDomain, reqCtx.ProxyConfig, varCtx, contentType)
|
||||
body = m.applyCustomReplacementsWithVariables(body, reqCtx.Session, reqCtx.TargetDomain, reqCtx.ProxyConfig, varCtx, contentType, resp.Request)
|
||||
|
||||
// apply obfuscation if enabled
|
||||
if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") {
|
||||
@@ -1152,7 +1159,7 @@ func (m *ProxyHandler) rewriteResponseBodyWithoutSessionContext(resp *http.Respo
|
||||
|
||||
body = m.patchUrls(configMap, body, CONVERT_TO_PHISHING_URLS)
|
||||
body = m.applyURLPathRewritesWithoutSession(body, reqCtx)
|
||||
body = m.applyCustomReplacementsWithoutSession(body, configMap, reqCtx.TargetDomain, reqCtx.ProxyConfig, contentType)
|
||||
body = m.applyCustomReplacementsWithoutSession(body, configMap, reqCtx.TargetDomain, reqCtx.ProxyConfig, contentType, resp.Request)
|
||||
|
||||
// apply obfuscation if enabled
|
||||
if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") {
|
||||
@@ -2352,7 +2359,9 @@ func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Reque
|
||||
if hCfg.Rewrite != nil {
|
||||
for _, replacement := range hCfg.Rewrite {
|
||||
if replacement.From == "" || replacement.From == "request_body" || replacement.From == "any" {
|
||||
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, "")
|
||||
if m.rewriteRuleMatchesRequest(replacement, req) {
|
||||
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2362,7 +2371,9 @@ func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Reque
|
||||
if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil {
|
||||
for _, replacement := range proxyConfig.Global.Rewrite {
|
||||
if replacement.From == "" || replacement.From == "request_body" || replacement.From == "any" {
|
||||
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, "")
|
||||
if m.rewriteRuleMatchesRequest(replacement, req) {
|
||||
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2370,18 +2381,20 @@ func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Reque
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) applyCustomReplacements(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML) []byte {
|
||||
return m.applyCustomReplacementsWithVariables(body, session, targetDomain, proxyConfig, nil, "")
|
||||
func (m *ProxyHandler) applyCustomReplacements(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, req *http.Request) []byte {
|
||||
return m.applyCustomReplacementsWithVariables(body, session, targetDomain, proxyConfig, nil, "", req)
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, varCtx *VariablesContext, contentType string) []byte {
|
||||
func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, varCtx *VariablesContext, contentType string, req *http.Request) []byte {
|
||||
// apply rewrite rules for the current request's target domain
|
||||
if hostConfig, ok := session.Config.Load(targetDomain); ok {
|
||||
hCfg := hostConfig.(service.ProxyServiceDomainConfig)
|
||||
if hCfg.Rewrite != nil {
|
||||
for _, replacement := range hCfg.Rewrite {
|
||||
if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" {
|
||||
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, contentType)
|
||||
if m.rewriteRuleMatchesRequest(replacement, req) {
|
||||
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, contentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2391,7 +2404,9 @@ func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session
|
||||
if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil {
|
||||
for _, replacement := range proxyConfig.Global.Rewrite {
|
||||
if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" {
|
||||
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, contentType)
|
||||
if m.rewriteRuleMatchesRequest(replacement, req) {
|
||||
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, contentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2411,7 +2426,9 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithoutSession(resp
|
||||
if hostConfig.Rewrite != nil {
|
||||
for _, replacement := range hostConfig.Rewrite {
|
||||
if replacement.From == "response_header" || replacement.From == "any" {
|
||||
headers = m.applyReplacement(headers, replacement, "no-session", "")
|
||||
if m.rewriteRuleMatchesRequest(replacement, resp.Request) {
|
||||
headers = m.applyReplacement(headers, replacement, "no-session", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2421,7 +2438,9 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithoutSession(resp
|
||||
if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil {
|
||||
for _, replacement := range proxyConfig.Global.Rewrite {
|
||||
if replacement.From == "response_header" || replacement.From == "any" {
|
||||
headers = m.applyReplacement(headers, replacement, "no-session", "")
|
||||
if m.rewriteRuleMatchesRequest(replacement, resp.Request) {
|
||||
headers = m.applyReplacement(headers, replacement, "no-session", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2446,13 +2465,15 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithoutSession(resp
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config map[string]service.ProxyServiceDomainConfig, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, contentType string) []byte {
|
||||
func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config map[string]service.ProxyServiceDomainConfig, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, contentType string, req *http.Request) []byte {
|
||||
// apply rewrite rules for the current target domain
|
||||
if hostConfig, ok := config[targetDomain]; ok {
|
||||
if hostConfig.Rewrite != nil {
|
||||
for _, replacement := range hostConfig.Rewrite {
|
||||
if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" {
|
||||
body = m.applyReplacement(body, replacement, "no-session", contentType)
|
||||
if m.rewriteRuleMatchesRequest(replacement, req) {
|
||||
body = m.applyReplacement(body, replacement, "no-session", contentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2462,7 +2483,9 @@ func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config
|
||||
if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil {
|
||||
for _, replacement := range proxyConfig.Global.Rewrite {
|
||||
if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" {
|
||||
body = m.applyReplacement(body, replacement, "no-session", contentType)
|
||||
if m.rewriteRuleMatchesRequest(replacement, req) {
|
||||
body = m.applyReplacement(body, replacement, "no-session", contentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2470,6 +2493,28 @@ func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config
|
||||
return body
|
||||
}
|
||||
|
||||
// rewriteRuleMatchesRequest returns true if the rewrite rule's path/method constraints
|
||||
// match the given request. If no constraints are set the rule always matches.
|
||||
// If req is nil (no request context available), rules with path/method constraints are skipped.
|
||||
func (m *ProxyHandler) rewriteRuleMatchesRequest(rule service.ProxyServiceReplaceRule, req *http.Request) bool {
|
||||
if req == nil {
|
||||
return rule.Path == "" && rule.Method == ""
|
||||
}
|
||||
if rule.Method != "" && !strings.EqualFold(rule.Method, req.Method) {
|
||||
return false
|
||||
}
|
||||
if rule.Path != "" {
|
||||
if rule.PathRe == nil {
|
||||
// path was set but failed to compile — skip rule safely
|
||||
return false
|
||||
}
|
||||
if !rule.PathRe.MatchString(req.URL.Path) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) applyReplacement(body []byte, replacement service.ProxyServiceReplaceRule, sessionID string, contentType string) []byte {
|
||||
return m.applyReplacementWithVariables(body, replacement, sessionID, nil, contentType)
|
||||
}
|
||||
@@ -2898,6 +2943,9 @@ func (m *ProxyHandler) applyEarlyRequestHeaderReplacements(req *http.Request, re
|
||||
applyReplacements := func(replacements []service.ProxyServiceReplaceRule) {
|
||||
for _, replacement := range replacements {
|
||||
if replacement.From == "" || replacement.From == "request_header" || replacement.From == "any" {
|
||||
if !m.rewriteRuleMatchesRequest(replacement, req) {
|
||||
continue
|
||||
}
|
||||
engine := replacement.Engine
|
||||
if engine == "" {
|
||||
engine = "regex"
|
||||
|
||||
@@ -160,7 +160,7 @@ func GetValidProxyVariableNames() []string {
|
||||
// - "public": Allow all traffic (traditional proxy mode) - on_deny is ignored
|
||||
// - "private": Strict IP-based mode like evilginx2 - whitelist IP after lure access, deny all others (DEFAULT)
|
||||
|
||||
// CompilePathPatterns compiles regex patterns for all capture and response rules
|
||||
// CompilePathPatterns compiles regex patterns for all capture, response, and rewrite rules
|
||||
func CompilePathPatterns(config *ProxyServiceConfigYAML) error {
|
||||
// Compile global capture rule patterns
|
||||
if config.Global != nil && config.Global.Capture != nil {
|
||||
@@ -180,6 +180,15 @@ func CompilePathPatterns(config *ProxyServiceConfigYAML) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Compile global rewrite rule patterns
|
||||
if config.Global != nil && config.Global.Rewrite != nil {
|
||||
for i := range config.Global.Rewrite {
|
||||
if err := compileRewritePath(&config.Global.Rewrite[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compile host-specific capture rule patterns
|
||||
for _, hostConfig := range config.Hosts {
|
||||
if hostConfig != nil && hostConfig.Capture != nil {
|
||||
@@ -201,6 +210,30 @@ func CompilePathPatterns(config *ProxyServiceConfigYAML) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compile host-specific rewrite rule patterns
|
||||
for _, hostConfig := range config.Hosts {
|
||||
if hostConfig != nil && hostConfig.Rewrite != nil {
|
||||
for i := range hostConfig.Rewrite {
|
||||
if err := compileRewritePath(&hostConfig.Rewrite[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// compileRewritePath compiles the path pattern for a rewrite rule
|
||||
func compileRewritePath(rule *ProxyServiceReplaceRule) error {
|
||||
if rule.Path != "" {
|
||||
pathRe, err := regexp.Compile(rule.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid regex pattern for rewrite path '%s': %w", rule.Path, err)
|
||||
}
|
||||
rule.PathRe = pathRe
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -275,13 +308,16 @@ func (c *ProxyServiceCaptureRule) GetFindAsString() string {
|
||||
|
||||
// ProxyServiceReplaceRule represents a replacement rule
|
||||
type ProxyServiceReplaceRule struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Engine string `yaml:"engine,omitempty"` // "regex" (default) or "dom"
|
||||
Find string `yaml:"find,omitempty"` // regex pattern (regex engine) or css selector (dom engine)
|
||||
Replace string `yaml:"replace,omitempty"` // replacement value for both engines
|
||||
Action string `yaml:"action,omitempty"` // dom action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove
|
||||
Target string `yaml:"target,omitempty"` // target matching: "first", "last", "all" (default), "1,3,5", "2-4"
|
||||
From string `yaml:"from,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Engine string `yaml:"engine,omitempty"` // "regex" (default) or "dom"
|
||||
Find string `yaml:"find,omitempty"` // regex pattern (regex engine) or css selector (dom engine)
|
||||
Replace string `yaml:"replace,omitempty"` // replacement value for both engines
|
||||
Action string `yaml:"action,omitempty"` // dom action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove
|
||||
Target string `yaml:"target,omitempty"` // target matching: "first", "last", "all" (default), "1,3,5", "2-4"
|
||||
From string `yaml:"from,omitempty"`
|
||||
Path string `yaml:"path,omitempty"` // regex pattern to restrict rule to matching request paths
|
||||
Method string `yaml:"method,omitempty"` // restrict rule to this HTTP method (e.g. GET, POST)
|
||||
PathRe *regexp.Regexp `yaml:"-"` // compiled regex for path matching
|
||||
}
|
||||
|
||||
// ProxyServiceURLRewriteRule represents a URL rewrite rule for anti-detection
|
||||
|
||||
@@ -518,7 +518,16 @@
|
||||
function addGlobalRewriteRule() {
|
||||
configData.global.rewrite = [
|
||||
...configData.global.rewrite,
|
||||
{ _id: getRuleId(), name: '', engine: 'regex', find: '', replace: '', from: 'response_body' }
|
||||
{
|
||||
_id: getRuleId(),
|
||||
name: '',
|
||||
engine: 'regex',
|
||||
find: '',
|
||||
replace: '',
|
||||
from: 'response_body',
|
||||
path: '',
|
||||
method: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -576,7 +585,16 @@
|
||||
function addHostRewriteRule(hostIndex) {
|
||||
configData.hosts[hostIndex].rewrite = [
|
||||
...(configData.hosts[hostIndex].rewrite || []),
|
||||
{ _id: getRuleId(), name: '', engine: 'regex', find: '', replace: '', from: 'response_body' }
|
||||
{
|
||||
_id: getRuleId(),
|
||||
name: '',
|
||||
engine: 'regex',
|
||||
find: '',
|
||||
replace: '',
|
||||
from: 'response_body',
|
||||
path: '',
|
||||
method: ''
|
||||
}
|
||||
];
|
||||
configData.hosts = [...configData.hosts];
|
||||
}
|
||||
@@ -751,7 +769,7 @@
|
||||
}
|
||||
|
||||
function isRewriteRuleTouched(rule) {
|
||||
return !!(rule.find?.trim() || rule.replace?.trim());
|
||||
return !!(rule.find?.trim() || rule.replace?.trim() || rule.path?.trim());
|
||||
}
|
||||
|
||||
function isResponseRuleTouched(rule) {
|
||||
@@ -1896,6 +1914,27 @@
|
||||
Engine
|
||||
</TextFieldSelect>
|
||||
</div>
|
||||
<div class="field-wrapper">
|
||||
<TextField width="full" bind:value={rule.path} placeholder="^/login">
|
||||
Path (regex, optional)
|
||||
</TextField>
|
||||
<span class="form-hint"
|
||||
>Only apply this rule when the request path matches</span
|
||||
>
|
||||
</div>
|
||||
<div class="field-wrapper">
|
||||
<TextFieldSelect
|
||||
id={`host-${expandedHostIndex}-rewrite-${ruleIndex}-method`}
|
||||
bind:value={rule.method}
|
||||
options={[
|
||||
{ value: '', label: 'Any' },
|
||||
...methods.map((m) => ({ value: m, label: m }))
|
||||
]}
|
||||
size="normal"
|
||||
>
|
||||
Method (optional)
|
||||
</TextFieldSelect>
|
||||
</div>
|
||||
{#if rule.engine === 'dom'}
|
||||
<div class="field-wrapper">
|
||||
<TextFieldSelect
|
||||
@@ -2689,6 +2728,27 @@
|
||||
Engine
|
||||
</TextFieldSelect>
|
||||
</div>
|
||||
<div class="field-wrapper">
|
||||
<TextField width="full" bind:value={rule.path} placeholder="^/login">
|
||||
Path (regex, optional)
|
||||
</TextField>
|
||||
<span class="form-hint"
|
||||
>Only apply this rule when the request path matches</span
|
||||
>
|
||||
</div>
|
||||
<div class="field-wrapper">
|
||||
<TextFieldSelect
|
||||
id={`global-rewrite-${i}-method`}
|
||||
bind:value={rule.method}
|
||||
options={[
|
||||
{ value: '', label: 'Any' },
|
||||
...methods.map((m) => ({ value: m, label: m }))
|
||||
]}
|
||||
size="normal"
|
||||
>
|
||||
Method (optional)
|
||||
</TextFieldSelect>
|
||||
</div>
|
||||
{#if rule.engine === 'dom'}
|
||||
<div class="field-wrapper">
|
||||
<TextFieldSelect
|
||||
|
||||
Reference in New Issue
Block a user