diff --git a/backend/app/server.go b/backend/app/server.go index e3521c3..aa92798 100644 --- a/backend/app/server.go +++ b/backend/app/server.go @@ -88,6 +88,7 @@ func NewServer( services.Template, services.IPAllowList, repositories.Option, + services.Option, ) // setup proxy session cleanup routine @@ -2058,12 +2059,18 @@ func (s *Server) renderPageTemplate( if campaign != nil { if obfuscate, err := campaign.Obfuscate.Get(); err == nil && obfuscate { s.logger.Debugw("obfuscating page", "campaignID", campaign.ID.MustGet().String(), "pageID", page.ID.MustGet().String()) - obfuscated, err := utils.ObfuscateHTML(string(pageContent), utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := s.services.Option.GetObfuscationTemplate(c.Request.Context()) if err != nil { - s.logger.Errorw("failed to obfuscate page", "error", err) + s.logger.Errorw("failed to get obfuscation template", "error", err) } else { - s.logger.Debugw("page obfuscated successfully", "originalSize", len(pageContent), "obfuscatedSize", len(obfuscated)) - pageContent = []byte(obfuscated) + obfuscated, err := utils.ObfuscateHTML(string(pageContent), utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + s.logger.Errorw("failed to obfuscate page", "error", err) + } else { + s.logger.Debugw("page obfuscated successfully", "originalSize", len(pageContent), "obfuscatedSize", len(obfuscated)) + pageContent = []byte(obfuscated) + } } } else { s.logger.Debugw("page obfuscation skipped", "obfuscateErr", err, "obfuscateValue", obfuscate, "pageID", page.ID.MustGet().String()) diff --git a/backend/data/option.go b/backend/data/option.go index 1d04787..8e83a46 100644 --- a/backend/data/option.go +++ b/backend/data/option.go @@ -29,4 +29,18 @@ const ( OptionKeyDisplayMode = "display_mode" OptionValueDisplayModeWhitebox = "whitebox" OptionValueDisplayModeBlackbox = "blackbox" + + OptionKeyObfuscationTemplate = "obfuscation_template" + // OptionValueObfuscationTemplateDefault is the default HTML template for obfuscation + // the template receives {{.Script}} variable containing the obfuscated javascript + OptionValueObfuscationTemplateDefault = ` + +
+ + + + + + +` ) diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index a09b24d..9a37b26 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -113,6 +113,7 @@ type ProxyHandler struct { TemplateService *service.Template IPAllowListService *service.IPAllowListService OptionRepository *repository.Option + OptionService *service.Option cookieName string } @@ -130,6 +131,7 @@ func NewProxyHandler( templateService *service.Template, ipAllowListService *service.IPAllowListService, optionRepo *repository.Option, + optionService *service.Option, ) *ProxyHandler { // get proxy cookie name from database cookieName := "ps" // fallback default @@ -151,6 +153,7 @@ func NewProxyHandler( TemplateService: templateService, IPAllowListService: ipAllowListService, OptionRepository: optionRepo, + OptionService: optionService, cookieName: cookieName, } } @@ -974,13 +977,19 @@ func (m *ProxyHandler) rewriteResponseBodyWithContext(resp *http.Response, reqCt // apply obfuscation if enabled if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") { if obfuscate, err := reqCtx.Campaign.Obfuscate.Get(); err == nil && obfuscate { - obfuscated, err := utils.ObfuscateHTML(string(body), utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := m.OptionService.GetObfuscationTemplate(resp.Request.Context()) if err != nil { - m.logger.Errorw("failed to obfuscate html", "error", err) + m.logger.Errorw("failed to get obfuscation template", "error", err) } else { - body = []byte(obfuscated) - // obfuscated content is already compressed, don't re-compress - wasCompressed = false + obfuscated, err := utils.ObfuscateHTML(string(body), utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + m.logger.Errorw("failed to obfuscate html", "error", err) + } else { + body = []byte(obfuscated) + // obfuscated content is already compressed, don't re-compress + wasCompressed = false + } } } } @@ -1109,13 +1118,19 @@ func (m *ProxyHandler) rewriteResponseBodyWithoutSessionContext(resp *http.Respo // apply obfuscation if enabled if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") { if obfuscate, err := reqCtx.Campaign.Obfuscate.Get(); err == nil && obfuscate { - obfuscated, err := utils.ObfuscateHTML(string(body), utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := m.OptionService.GetObfuscationTemplate(resp.Request.Context()) if err != nil { - m.logger.Errorw("failed to obfuscate html", "error", err) + m.logger.Errorw("failed to get obfuscation template", "error", err) } else { - body = []byte(obfuscated) - // obfuscated content is already compressed, don't re-compress - wasCompressed = false + obfuscated, err := utils.ObfuscateHTML(string(body), utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + m.logger.Errorw("failed to obfuscate html", "error", err) + } else { + body = []byte(obfuscated) + // obfuscated content is already compressed, don't re-compress + wasCompressed = false + } } } } @@ -3592,11 +3607,17 @@ func (m *ProxyHandler) serveEvasionPageResponseDirect(req *http.Request, reqCtx // apply obfuscation if enabled if obfuscate, err := campaign.Obfuscate.Get(); err == nil && obfuscate { - obfuscated, err := utils.ObfuscateHTML(htmlContent, utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := m.OptionService.GetObfuscationTemplate(req.Context()) if err != nil { - m.logger.Errorw("failed to obfuscate evasion page", "error", err) + m.logger.Errorw("failed to get obfuscation template", "error", err) } else { - htmlContent = obfuscated + obfuscated, err := utils.ObfuscateHTML(htmlContent, utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + m.logger.Errorw("failed to obfuscate evasion page", "error", err) + } else { + htmlContent = obfuscated + } } } @@ -3673,11 +3694,17 @@ func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *Re // apply obfuscation if enabled if obfuscate, err := campaign.Obfuscate.Get(); err == nil && obfuscate { - obfuscated, err := utils.ObfuscateHTML(htmlContent, utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := m.OptionService.GetObfuscationTemplate(req.Context()) if err != nil { - m.logger.Errorw("failed to obfuscate deny page", "error", err) + m.logger.Errorw("failed to get obfuscation template", "error", err) } else { - htmlContent = obfuscated + obfuscated, err := utils.ObfuscateHTML(htmlContent, utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + m.logger.Errorw("failed to obfuscate deny page", "error", err) + } else { + htmlContent = obfuscated + } } } diff --git a/backend/seed/migrate.go b/backend/seed/migrate.go index f1727a5..008da78 100644 --- a/backend/seed/migrate.go +++ b/backend/seed/migrate.go @@ -239,6 +239,29 @@ func SeedSettings( } } } + { + // seed obfuscation template option + id := uuid.New() + var c int64 + res := db. + Model(&database.Option{}). + Where("key = ?", data.OptionKeyObfuscationTemplate). + Count(&c) + + if res.Error != nil { + return errs.Wrap(res.Error) + } + if c == 0 { + res = db.Create(&database.Option{ + ID: &id, + Key: data.OptionKeyObfuscationTemplate, + Value: data.OptionValueObfuscationTemplateDefault, + }) + if res.Error != nil { + return errs.Wrap(res.Error) + } + } + } { // seed display mode option // default to blackbox if option doesn't exist diff --git a/backend/service/option.go b/backend/service/option.go index 99a96ba..903fdf9 100644 --- a/backend/service/option.go +++ b/backend/service/option.go @@ -170,6 +170,8 @@ func (o *Option) SetOptionByKey( "display mode", ) } + case data.OptionKeyObfuscationTemplate: + // is allow listed default: o.Logger.Debugw("invalid settings key", "key", k) return validate.WrapErrorWithField( @@ -191,3 +193,22 @@ func (o *Option) SetOptionByKey( o.AuditLogAuthorized(ae) return nil } + +// GetObfuscationTemplate gets the obfuscation template from options or returns default +func (o *Option) GetObfuscationTemplate(ctx context.Context) (string, error) { + opt, err := o.OptionRepository.GetByKey(ctx, data.OptionKeyObfuscationTemplate) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // return default template if not found + return data.OptionValueObfuscationTemplateDefault, nil + } + o.Logger.Errorw("failed to get obfuscation template option", "error", err) + return "", errs.Wrap(err) + } + template := opt.Value.String() + if template == "" { + // return default if empty + return data.OptionValueObfuscationTemplateDefault, nil + } + return template, nil +} diff --git a/backend/utils/obfuscate.go b/backend/utils/obfuscate.go index 8f335f0..008bdbe 100644 --- a/backend/utils/obfuscate.go +++ b/backend/utils/obfuscate.go @@ -8,6 +8,7 @@ import ( "fmt" "math/big" "strings" + "text/template" ) // ObfuscationConfig controls how the obfuscation behaves @@ -46,7 +47,7 @@ func DefaultObfuscationConfig() ObfuscationConfig { // ObfuscateHTML obfuscates HTML content using compression, base64 encoding, // and random variable names to make it difficult to fingerprint -func ObfuscateHTML(html string, config ObfuscationConfig) (string, error) { +func ObfuscateHTML(html string, config ObfuscationConfig, htmlTemplate string, funcMap template.FuncMap) (string, error) { // generate random variable names varNames := generateRandomVariableNames(15, config) xorFuncName := varNames[9] @@ -119,19 +120,22 @@ func ObfuscateHTML(html string, config ObfuscationConfig) (string, error) { windowVar, documentSplit, writeSplit, varNames[5], windowVar, documentSplit, closeSplit) - // HTML5 template - template := fmt.Sprintf(` - - - - - - - - -`, deobfScript) + // render the html template with the deobfuscation script + tmpl, err := template.New("obfuscation").Funcs(funcMap).Parse(htmlTemplate) + if err != nil { + return "", fmt.Errorf("failed to parse obfuscation template: %w", err) + } - return template, nil + var buf bytes.Buffer + data := map[string]interface{}{ + "Script": deobfScript, + } + err = tmpl.Execute(&buf, data) + if err != nil { + return "", fmt.Errorf("failed to execute obfuscation template: %w", err) + } + + return buf.String(), nil } // getRandomWindowAccessor returns a random way to access the window object diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 8424f95..ef55303 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -2204,7 +2204,7 @@ export class API { /** * Get setting by key. * - * @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'} key + * @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key * @returns {Promise+ Customize the template used when obfuscation is enabled to. +
++ Internal obfuscation variable: +
++ {'{{.Script}}'} +
+
+ Example {"eval(atob('{{base64 .Script}}'))"}
+