mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-13 07:26:37 +02:00
Fix proxy bugs with domain rewrite outside scope, global rewrite more consistent, fix dublicate global rewrite, add global rewrite to body
Fix proxy replace without from too eager Fix redundant proxy header rewrite Fix multiline proxy yaml, do not format on backend
This commit is contained in:
+54
-63
@@ -539,46 +539,14 @@ func (m *ProxyHandler) prepareRequestWithoutSession(req *http.Request, reqCtx *R
|
||||
}
|
||||
}
|
||||
|
||||
// append global rewrite rules to host config for requests without session
|
||||
// note: header rewrite rules are already applied in applyEarlyRequestHeaderReplacements
|
||||
|
||||
// apply body rewrite rules (no capture)
|
||||
// append global rewrite rules to host config for body replacements
|
||||
if reqCtx.ProxyConfig != nil && reqCtx.ProxyConfig.Global != nil && reqCtx.ProxyConfig.Global.Rewrite != nil {
|
||||
hostConfig.Rewrite = append(hostConfig.Rewrite, reqCtx.ProxyConfig.Global.Rewrite...)
|
||||
}
|
||||
|
||||
// apply header rewrite rules (no capture)
|
||||
if hostConfig.Rewrite != nil {
|
||||
for _, replacement := range hostConfig.Rewrite {
|
||||
if replacement.From == "" || replacement.From == "request_header" || replacement.From == "any" {
|
||||
engine := replacement.Engine
|
||||
if engine == "" {
|
||||
engine = "regex"
|
||||
}
|
||||
if engine == "regex" {
|
||||
re, err := regexp.Compile(replacement.Find)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for headerName, values := range req.Header {
|
||||
newValues := make([]string, 0, len(values))
|
||||
for _, val := range values {
|
||||
fullHeader := headerName + ": " + val
|
||||
replaced := re.ReplaceAllString(fullHeader, replacement.Replace)
|
||||
if strings.HasPrefix(replaced, headerName+": ") {
|
||||
newVal := replaced[len(headerName)+2:]
|
||||
newValues = append(newValues, newVal)
|
||||
} else if replaced != fullHeader {
|
||||
newValues = append(newValues, val)
|
||||
} else {
|
||||
newValues = append(newValues, val)
|
||||
}
|
||||
}
|
||||
req.Header[headerName] = newValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply body rewrite rules (no capture)
|
||||
if req.Body != nil {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err == nil {
|
||||
@@ -1034,7 +1002,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, varCtx, contentType)
|
||||
body = m.applyCustomReplacementsWithVariables(body, reqCtx.Session, reqCtx.TargetDomain, reqCtx.ProxyConfig, varCtx, contentType)
|
||||
|
||||
// apply obfuscation if enabled
|
||||
if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") {
|
||||
@@ -1117,14 +1085,13 @@ func (m *ProxyHandler) createMinimalConfig(phishDomain, targetDomain string) map
|
||||
config[targetDomain] = service.ProxyServiceDomainConfig{To: phishDomain}
|
||||
}
|
||||
|
||||
// add global rules to all host configurations
|
||||
// add global capture rules to all host configurations
|
||||
// note: global rewrite rules are applied separately in applyCustomReplacementsWithoutSession
|
||||
if fullConfigYAML != nil && fullConfigYAML.Global != nil {
|
||||
for originalHost := range config {
|
||||
hostConfig := config[originalHost]
|
||||
// append global capture rules
|
||||
hostConfig.Capture = append(hostConfig.Capture, fullConfigYAML.Global.Capture...)
|
||||
// append global rewrite rules
|
||||
hostConfig.Rewrite = append(hostConfig.Rewrite, fullConfigYAML.Global.Rewrite...)
|
||||
config[originalHost] = hostConfig
|
||||
}
|
||||
}
|
||||
@@ -1175,7 +1142,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, contentType)
|
||||
body = m.applyCustomReplacementsWithoutSession(body, configMap, reqCtx.TargetDomain, reqCtx.ProxyConfig, contentType)
|
||||
|
||||
// apply obfuscation if enabled
|
||||
if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") {
|
||||
@@ -1412,13 +1379,12 @@ func (m *ProxyHandler) buildSessionConfig(targetDomain, phishDomain string, prox
|
||||
}
|
||||
}
|
||||
|
||||
// add global rules only to the target domain configuration
|
||||
// add global capture rules only to the target domain configuration
|
||||
// note: global rewrite rules are applied separately in applyCustomReplacementsWithVariables
|
||||
if proxyConfig.Global != nil && sessionConfig[targetDomain].To != "" {
|
||||
hostConfig := sessionConfig[targetDomain]
|
||||
// append global capture rules
|
||||
hostConfig.Capture = append(hostConfig.Capture, proxyConfig.Global.Capture...)
|
||||
// append global rewrite rules
|
||||
hostConfig.Rewrite = append(hostConfig.Rewrite, proxyConfig.Global.Rewrite...)
|
||||
sessionConfig[targetDomain] = hostConfig
|
||||
}
|
||||
|
||||
@@ -1494,7 +1460,7 @@ func (m *ProxyHandler) onRequestBody(req *http.Request, session *service.ProxySe
|
||||
|
||||
// build variables context using the proxy config passed from caller
|
||||
varCtx := m.buildVariablesContext(req.Context(), session, proxyConfig)
|
||||
m.applyRequestBodyReplacementsWithVariables(req, session, varCtx)
|
||||
m.applyRequestBodyReplacementsWithVariables(req, session, proxyConfig, varCtx)
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) onRequestHeader(req *http.Request, session *service.ProxySession) {
|
||||
@@ -2342,18 +2308,18 @@ func (m *ProxyHandler) createCookieBundle(cookieCaptures map[string]map[string]s
|
||||
return bundledData
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) applyRequestBodyReplacements(req *http.Request, session *service.ProxySession) {
|
||||
m.applyRequestBodyReplacementsWithVariables(req, session, nil)
|
||||
func (m *ProxyHandler) applyRequestBodyReplacements(req *http.Request, session *service.ProxySession, proxyConfig *service.ProxyServiceConfigYAML) {
|
||||
m.applyRequestBodyReplacementsWithVariables(req, session, proxyConfig, nil)
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Request, session *service.ProxySession, varCtx *VariablesContext) {
|
||||
func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Request, session *service.ProxySession, proxyConfig *service.ProxyServiceConfigYAML, varCtx *VariablesContext) {
|
||||
if req.Body == nil {
|
||||
return
|
||||
}
|
||||
|
||||
body := m.readRequestBody(req)
|
||||
|
||||
// only apply rewrite rules for the current host
|
||||
// apply rewrite rules for the current host
|
||||
if hostConfig, ok := session.Config.Load(req.Host); ok {
|
||||
hCfg := hostConfig.(service.ProxyServiceDomainConfig)
|
||||
if hCfg.Rewrite != nil {
|
||||
@@ -2365,16 +2331,25 @@ func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
// apply global rewrite rules
|
||||
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, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) applyCustomReplacements(body []byte, session *service.ProxySession) []byte {
|
||||
return m.applyCustomReplacementsWithVariables(body, session, nil, "")
|
||||
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) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, varCtx *VariablesContext, contentType string) []byte {
|
||||
// only apply rewrite rules for the current host
|
||||
if hostConfig, ok := session.Config.Load(session.TargetDomain); ok {
|
||||
func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, varCtx *VariablesContext, contentType string) []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 {
|
||||
@@ -2384,13 +2359,23 @@ func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply global rewrite rules
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
// applyCustomReplacementsWithoutSession applies rewrite rules for requests without session context
|
||||
func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config map[string]service.ProxyServiceDomainConfig, targetDomain string, contentType string) []byte {
|
||||
// apply rewrite rules from all host configurations (matches session behavior)
|
||||
for _, hostConfig := range config {
|
||||
func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config map[string]service.ProxyServiceDomainConfig, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, contentType string) []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" {
|
||||
@@ -2400,6 +2385,15 @@ func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config
|
||||
}
|
||||
}
|
||||
|
||||
// apply global rewrite rules
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
@@ -2887,13 +2881,10 @@ func (m *ProxyHandler) applyEarlyRequestHeaderReplacements(req *http.Request, re
|
||||
applyReplacements(reqCtx.ProxyConfig.Global.Rewrite)
|
||||
}
|
||||
|
||||
// then apply request_header replacements from all host configs
|
||||
// this ensures replacements work for all domains in the session (e.g., CDN domains)
|
||||
// apply request_header replacements only for the current target domain
|
||||
if reqCtx.ProxyConfig.Hosts != nil {
|
||||
for _, domainConfig := range reqCtx.ProxyConfig.Hosts {
|
||||
if domainConfig != nil && domainConfig.Rewrite != nil {
|
||||
applyReplacements(domainConfig.Rewrite)
|
||||
}
|
||||
if domainConfig, exists := reqCtx.ProxyConfig.Hosts[reqCtx.TargetDomain]; exists && domainConfig != nil && domainConfig.Rewrite != nil {
|
||||
applyReplacements(domainConfig.Rewrite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,12 +647,6 @@ func (m *Proxy) GetByID(
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
// apply defaults to Proxy configuration for display
|
||||
if err := m.applyConfigurationDefaults(proxy); err != nil {
|
||||
m.Logger.Errorw("failed to apply configuration defaults", "error", err)
|
||||
// don't fail the request, just log the error
|
||||
}
|
||||
|
||||
// no audit log on read
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
|
||||
import TextareaField from '$lib/components/TextareaField.svelte';
|
||||
import Search from '$lib/components/Search.svelte';
|
||||
import jsyaml from '$lib/components/yaml/index.js';
|
||||
import jsyaml, { dumpWithLiteralStrings } from '$lib/components/yaml/index.js';
|
||||
|
||||
export let config = null;
|
||||
// basic info fields passed from parent
|
||||
@@ -446,14 +446,8 @@
|
||||
output[host.domain] = cleanObject(hostObj);
|
||||
}
|
||||
|
||||
// serialize to YAML with js-yaml
|
||||
return jsyaml.dump(output, {
|
||||
indent: 2,
|
||||
lineWidth: 120, // allow block scalars for multiline strings
|
||||
quotingType: "'", // prefer single quotes
|
||||
forceQuotes: false, // only quote when necessary
|
||||
noRefs: true // don't use YAML references
|
||||
});
|
||||
// serialize to YAML with literal block style for replace/body fields
|
||||
return dumpWithLiteralStrings(output);
|
||||
}
|
||||
|
||||
// host management
|
||||
@@ -1057,14 +1051,8 @@
|
||||
output[host.domain] = cleanObject(hostObj);
|
||||
}
|
||||
|
||||
// serialize to YAML
|
||||
const yamlContent = jsyaml.dump(output, {
|
||||
indent: 2,
|
||||
lineWidth: 120, // allow block scalars for multiline strings
|
||||
quotingType: "'",
|
||||
forceQuotes: false,
|
||||
noRefs: true
|
||||
});
|
||||
// serialize to YAML with literal block style for replace/body fields
|
||||
const yamlContent = dumpWithLiteralStrings(output);
|
||||
|
||||
// create blob and download
|
||||
const blob = new Blob([yamlContent], { type: 'application/x-yaml' });
|
||||
|
||||
@@ -15,4 +15,93 @@ export const DEFAULT_SCHEMA = jsyaml.DEFAULT_SCHEMA;
|
||||
export const FAILSAFE_SCHEMA = jsyaml.FAILSAFE_SCHEMA;
|
||||
export const JSON_SCHEMA = jsyaml.JSON_SCHEMA;
|
||||
|
||||
// helper to dump yaml with literal block style for multiline strings in specific keys
|
||||
export function dumpWithLiteralStrings(obj, literalKeys = ['replace', 'body'], options = {}) {
|
||||
// store multiline strings and replace with markers
|
||||
const multilineStore = new Map();
|
||||
let markerIndex = 0;
|
||||
|
||||
function createMarker() {
|
||||
return `___LITERAL_${markerIndex++}___`;
|
||||
}
|
||||
|
||||
// recursively process object, replacing multiline strings with markers
|
||||
function extractMultilineStrings(data, currentKey = null) {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item) => extractMultilineStrings(item, null));
|
||||
}
|
||||
if (data && typeof data === 'object') {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
result[key] = extractMultilineStrings(value, key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// if this is a multiline string in a literal key, replace with marker
|
||||
if (typeof data === 'string' && literalKeys.includes(currentKey) && data.includes('\n')) {
|
||||
const marker = createMarker();
|
||||
// normalize line endings
|
||||
const normalized = data.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
multilineStore.set(marker, normalized);
|
||||
return marker;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// process object to extract multiline strings
|
||||
const processed = extractMultilineStrings(obj);
|
||||
|
||||
// dump to yaml - markers will be simple strings
|
||||
let yaml = jsyaml.dump(processed, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
noRefs: true,
|
||||
...options
|
||||
});
|
||||
|
||||
// replace each marker with literal block using simple string search
|
||||
for (const [marker, value] of multilineStore) {
|
||||
// find the marker in the yaml (it may be quoted or unquoted)
|
||||
const patterns = [`'${marker}'`, `"${marker}"`, marker];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const idx = yaml.indexOf(pattern);
|
||||
if (idx === -1) continue;
|
||||
|
||||
// find the start of this line to get indentation
|
||||
let lineStart = idx;
|
||||
while (lineStart > 0 && yaml[lineStart - 1] !== '\n') {
|
||||
lineStart--;
|
||||
}
|
||||
|
||||
// extract indentation
|
||||
const lineContent = yaml.substring(lineStart, idx);
|
||||
const indentMatch = lineContent.match(/^(\s*)/);
|
||||
const baseIndent = indentMatch ? indentMatch[1] : '';
|
||||
|
||||
// find end of line
|
||||
let lineEnd = idx + pattern.length;
|
||||
while (lineEnd < yaml.length && yaml[lineEnd] !== '\n') {
|
||||
lineEnd++;
|
||||
}
|
||||
|
||||
// get the key from the line (everything before the colon)
|
||||
const fullLine = yaml.substring(lineStart, lineEnd);
|
||||
const colonIdx = fullLine.indexOf(':');
|
||||
const key = fullLine.substring(baseIndent.length, colonIdx);
|
||||
|
||||
// build literal block
|
||||
const blockIndent = baseIndent + ' ';
|
||||
const literalLines = value.split('\n').map((line) => blockIndent + line);
|
||||
const replacement = `${baseIndent}${key}: |\n${literalLines.join('\n')}`;
|
||||
|
||||
// replace the entire line
|
||||
yaml = yaml.substring(0, lineStart) + replacement + yaml.substring(lineEnd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return yaml;
|
||||
}
|
||||
|
||||
export default jsyaml;
|
||||
|
||||
@@ -105,13 +105,8 @@
|
||||
}
|
||||
|
||||
// serialize back to YAML without _meta for the config
|
||||
const cleanYaml = jsyaml.default.dump(parsed, {
|
||||
indent: 2,
|
||||
lineWidth: 120,
|
||||
quotingType: "'",
|
||||
forceQuotes: false,
|
||||
noRefs: true
|
||||
});
|
||||
// use dumpWithLiteralStrings to preserve literal block style for replace/body fields
|
||||
const cleanYaml = jsyaml.dumpWithLiteralStrings(parsed);
|
||||
formValues.proxyConfig = cleanYaml;
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -121,11 +116,11 @@
|
||||
|
||||
// export configuration to YAML file with metadata (for YAML mode)
|
||||
function exportYamlConfig() {
|
||||
import('$lib/components/yaml/index.js').then((jsyaml) => {
|
||||
import('$lib/components/yaml/index.js').then((yamlModule) => {
|
||||
try {
|
||||
// parse current config
|
||||
const parsed = formValues.proxyConfig
|
||||
? jsyaml.default.load(formValues.proxyConfig) || {}
|
||||
? yamlModule.default.load(formValues.proxyConfig) || {}
|
||||
: {};
|
||||
|
||||
// build output with _general first
|
||||
@@ -146,14 +141,8 @@
|
||||
// merge rest of config
|
||||
Object.assign(output, parsed);
|
||||
|
||||
// serialize to YAML
|
||||
const yamlContent = jsyaml.default.dump(output, {
|
||||
indent: 2,
|
||||
lineWidth: 120,
|
||||
quotingType: "'",
|
||||
forceQuotes: false,
|
||||
noRefs: true
|
||||
});
|
||||
// serialize to YAML with literal block style for replace/body fields
|
||||
const yamlContent = yamlModule.dumpWithLiteralStrings(output);
|
||||
|
||||
// create blob and download
|
||||
const blob = new Blob([yamlContent], { type: 'application/x-yaml' });
|
||||
|
||||
Reference in New Issue
Block a user