mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-07 12:57:54 +02:00
add support for proxy schema http and ports in proxy start URL
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
+37
-9
@@ -80,6 +80,7 @@ type RequestContext struct {
|
||||
SessionCreated bool
|
||||
PhishDomain string
|
||||
TargetDomain string
|
||||
TargetScheme string
|
||||
Domain *database.Domain
|
||||
ProxyConfig *service.ProxyServiceConfigYAML
|
||||
Session *service.ProxySession
|
||||
@@ -255,17 +256,43 @@ func (m *ProxyHandler) HandleHTTPRequest(w http.ResponseWriter, req *http.Reques
|
||||
return m.writeResponse(w, finalResp)
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) extractTargetDomain(domain *database.Domain) string {
|
||||
// extractTargetHostAndScheme extracts both the host and scheme from the target domain
|
||||
// it first checks the config for an explicit scheme, then falls back to parsing the URL
|
||||
// returns the host (with port if present) and scheme (defaults to "https")
|
||||
func (m *ProxyHandler) extractTargetHostAndScheme(domain *database.Domain, config *service.ProxyServiceConfigYAML) (string, string) {
|
||||
targetDomain := domain.ProxyTargetDomain
|
||||
if targetDomain == "" {
|
||||
return ""
|
||||
return "", "https"
|
||||
}
|
||||
|
||||
// extract the host first
|
||||
host := targetDomain
|
||||
schemeFromURL := ""
|
||||
|
||||
// check if it's a full URL with scheme
|
||||
if strings.Contains(targetDomain, "://") {
|
||||
if parsedURL, err := url.Parse(targetDomain); err == nil {
|
||||
targetDomain = parsedURL.Host
|
||||
host = parsedURL.Host
|
||||
schemeFromURL = parsedURL.Scheme
|
||||
}
|
||||
}
|
||||
return targetDomain
|
||||
|
||||
// determine the scheme to use
|
||||
// priority: 1. config scheme field, 2. scheme from URL, 3. default to https
|
||||
scheme := "https"
|
||||
|
||||
// check if there's a scheme specified in the config for this target domain
|
||||
if config != nil && config.Hosts != nil {
|
||||
if hostConfig, exists := config.Hosts[targetDomain]; exists && hostConfig.Scheme != "" {
|
||||
scheme = hostConfig.Scheme
|
||||
} else if schemeFromURL != "" {
|
||||
scheme = schemeFromURL
|
||||
}
|
||||
} else if schemeFromURL != "" {
|
||||
scheme = schemeFromURL
|
||||
}
|
||||
|
||||
return host, scheme
|
||||
}
|
||||
|
||||
// initializeRequestContext creates and populates the request context with all necessary data
|
||||
@@ -280,8 +307,8 @@ func (m *ProxyHandler) initializeRequestContext(ctx context.Context, req *http.R
|
||||
return nil, errors.Errorf("failed to parse Proxy config for domain %s: %w", domain.Name, err)
|
||||
}
|
||||
|
||||
// extract target domain
|
||||
targetDomain := m.extractTargetDomain(domain)
|
||||
// extract target domain and scheme
|
||||
targetDomain, targetScheme := m.extractTargetHostAndScheme(domain, proxyConfig)
|
||||
if targetDomain == "" {
|
||||
return nil, errors.Errorf("domain has empty Proxy target domain")
|
||||
}
|
||||
@@ -292,6 +319,7 @@ func (m *ProxyHandler) initializeRequestContext(ctx context.Context, req *http.R
|
||||
reqCtx := &RequestContext{
|
||||
PhishDomain: req.Host,
|
||||
TargetDomain: targetDomain,
|
||||
TargetScheme: targetScheme,
|
||||
Domain: domain,
|
||||
ProxyConfig: proxyConfig,
|
||||
CampaignRecipientID: campaignRecipientID,
|
||||
@@ -475,7 +503,7 @@ func (m *ProxyHandler) prepareRequestWithoutSession(req *http.Request, reqCtx *R
|
||||
// set host and scheme
|
||||
req.Host = reqCtx.TargetDomain
|
||||
req.URL.Host = reqCtx.TargetDomain
|
||||
req.URL.Scheme = "https"
|
||||
req.URL.Scheme = reqCtx.TargetScheme
|
||||
|
||||
// create a dummy session for header normalization (no campaign/session data)
|
||||
dummySession := &service.ProxySession{
|
||||
@@ -645,7 +673,7 @@ func (m *ProxyHandler) applySessionToRequestWithContext(req *http.Request, reqCt
|
||||
// use session's original target domain only for initial landing
|
||||
if reqCtx.CampaignRecipientID != nil && reqCtx.SessionCreated {
|
||||
req.Host = reqCtx.Session.TargetDomain
|
||||
req.URL.Scheme = "https"
|
||||
req.URL.Scheme = reqCtx.TargetScheme
|
||||
req.URL.Host = reqCtx.Session.TargetDomain
|
||||
// remove campaign parameters from query params
|
||||
q := req.URL.Query()
|
||||
@@ -668,7 +696,7 @@ func (m *ProxyHandler) applySessionToRequestWithContext(req *http.Request, reqCt
|
||||
}
|
||||
req.Host = targetDomain
|
||||
req.URL.Host = targetDomain
|
||||
req.URL.Scheme = "https"
|
||||
req.URL.Scheme = reqCtx.TargetScheme
|
||||
}
|
||||
|
||||
// apply request processing
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -1109,17 +1110,47 @@ func (d *Domain) validateProxyDomain(ctx context.Context, domain *model.Domain)
|
||||
return validate.WrapErrorWithField(errors.New("proxy target domain cannot be empty"), "proxyTargetDomain")
|
||||
}
|
||||
|
||||
// validate proxy target format - can be full URL or domain
|
||||
// validate proxy target format - can be full URL or domain (with optional port)
|
||||
if strings.Contains(targetDomainStr, "://") {
|
||||
// full URL format - basic validation
|
||||
if !strings.HasPrefix(targetDomainStr, "http://") && !strings.HasPrefix(targetDomainStr, "https://") {
|
||||
return validate.WrapErrorWithField(errors.New("proxy target domain URL must start with http:// or https://"), "proxyTargetDomain")
|
||||
}
|
||||
} else {
|
||||
// domain-only format - validate as domain
|
||||
if !isValidDomain(targetDomainStr) {
|
||||
return validate.WrapErrorWithField(errors.New("invalid domain format for proxy target domain"), "proxyTargetDomain")
|
||||
// domain-only format (with optional port) - validate as domain, ip address, or localhost
|
||||
domainPart := targetDomainStr
|
||||
|
||||
// try to split host and port using net.SplitHostPort for proper handling of ipv6
|
||||
host, port, err := net.SplitHostPort(targetDomainStr)
|
||||
if err == nil {
|
||||
// port was present and successfully split
|
||||
domainPart = host
|
||||
|
||||
// validate port is numeric and in valid range
|
||||
portNum, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return validate.WrapErrorWithField(errors.New("port must be numeric in proxy target domain"), "proxyTargetDomain")
|
||||
}
|
||||
if portNum < 1 || portNum > 65535 {
|
||||
return validate.WrapErrorWithField(errors.New("port must be between 1 and 65535"), "proxyTargetDomain")
|
||||
}
|
||||
}
|
||||
// if SplitHostPort fails, targetDomainStr has no port, which is fine
|
||||
|
||||
// check if it's localhost (special case for single-label hostname)
|
||||
if strings.ToLower(domainPart) == "localhost" {
|
||||
// localhost is always valid
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if it's an ip address (ipv4 or ipv6)
|
||||
if net.ParseIP(domainPart) == nil {
|
||||
// not an ip address, validate as domain
|
||||
if !isValidDomain(domainPart) {
|
||||
return validate.WrapErrorWithField(errors.New("invalid domain format for proxy target domain"), "proxyTargetDomain")
|
||||
}
|
||||
}
|
||||
// if it's a valid ip address, no further validation needed
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -42,6 +42,7 @@ type ProxyServiceConfig struct {
|
||||
// ProxyServiceDomainConfig represents configuration for a specific domain mapping
|
||||
type ProxyServiceDomainConfig struct {
|
||||
To string `yaml:"to"`
|
||||
Scheme string `yaml:"scheme,omitempty"` // http or https (defaults to https)
|
||||
TLS *ProxyServiceTLSConfig `yaml:"tls,omitempty"`
|
||||
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
|
||||
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
|
||||
@@ -1620,6 +1621,14 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
|
||||
}
|
||||
}
|
||||
|
||||
// validate domain-specific scheme
|
||||
if domainConfig.Scheme != "" && domainConfig.Scheme != "http" && domainConfig.Scheme != "https" {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New(fmt.Sprintf("scheme for domain '%s' must be either 'http' or 'https'", originalDomain)),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
|
||||
// validate domain-specific TLS config
|
||||
if err := m.validateTLSConfig(domainConfig.TLS); err != nil {
|
||||
return err
|
||||
|
||||
+2
-1
@@ -117,7 +117,8 @@ services:
|
||||
volumes:
|
||||
- ./api-test-server:/app
|
||||
networks:
|
||||
- default
|
||||
default:
|
||||
ipv4_address: 172.20.0.135
|
||||
ports:
|
||||
- 8107:80
|
||||
|
||||
|
||||
@@ -98,6 +98,9 @@ export class ProxyYamlCompletionProvider {
|
||||
if (linePrefix.match(/\s*target:\s*$/)) {
|
||||
return this.getTargetSuggestions(range);
|
||||
}
|
||||
if (linePrefix.match(/\s*scheme:\s*$/)) {
|
||||
return this.getSchemeSuggestions(range);
|
||||
}
|
||||
if (linePrefix.match(/\s*mode:\s*$/)) {
|
||||
// determine context for mode - could be access or tls
|
||||
const context = this.findParentSection(linesAbove, currentIndent);
|
||||
@@ -306,6 +309,13 @@ export class ProxyYamlCompletionProvider {
|
||||
documentation: 'Phishing domain (where victims will visit)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'scheme',
|
||||
kind: this.monaco.languages.CompletionItemKind.Field,
|
||||
insertText: 'scheme: ',
|
||||
documentation: 'Scheme for proxying to target (http or https, defaults to https)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'tls',
|
||||
kind: this.monaco.languages.CompletionItemKind.Module,
|
||||
@@ -382,6 +392,25 @@ export class ProxyYamlCompletionProvider {
|
||||
];
|
||||
}
|
||||
|
||||
getSchemeSuggestions(range) {
|
||||
return [
|
||||
{
|
||||
label: 'http',
|
||||
kind: this.monaco.languages.CompletionItemKind.Value,
|
||||
insertText: 'http',
|
||||
documentation: 'Use HTTP when connecting to target',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'https',
|
||||
kind: this.monaco.languages.CompletionItemKind.Value,
|
||||
insertText: 'https',
|
||||
documentation: 'Use HTTPS when connecting to target (default)',
|
||||
range
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getImpersonateSuggestions(range) {
|
||||
return [
|
||||
{
|
||||
@@ -1137,6 +1166,8 @@ export class ProxyYamlCompletionProvider {
|
||||
mode: 'Access control mode: "public" (allow all traffic) or "private" (IP whitelist after lure access, DEFAULT), OR TLS mode: "managed" (Let\'s Encrypt) or "self-signed"',
|
||||
on_deny:
|
||||
'Response when access is denied in private mode (e.g., "404", "https://example.com")',
|
||||
scheme:
|
||||
'Protocol scheme for proxying to target: "http" or "https" (defaults to https). Specifies whether to use HTTP or HTTPS when connecting to the target domain',
|
||||
capture: 'Rules for capturing data from requests/responses',
|
||||
name: 'Unique identifier for the rule',
|
||||
method: 'HTTP method to match (GET, POST, PUT, DELETE, etc.)',
|
||||
|
||||
@@ -84,6 +84,9 @@ global:
|
||||
|
||||
portal.example.com:
|
||||
to: "evil.example.com"
|
||||
# optional: specify scheme for proxying to target (defaults to https)
|
||||
# scheme: "http" # use http:// when connecting to target
|
||||
# scheme: "https" # use https:// when connecting to target
|
||||
# optional: override global TLS config for this specific host
|
||||
# tls:
|
||||
# mode: "self-signed"
|
||||
|
||||
Reference in New Issue
Block a user