custom obfuscation

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-11-22 12:10:49 +01:00
parent c0acb8c790
commit 2d5f5a4216
8 changed files with 269 additions and 35 deletions
+11 -4
View File
@@ -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())
+14
View File
@@ -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 = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<script>{{.Script}}</script>
</body>
</html>`
)
+43 -16
View File
@@ -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
}
}
}
+23
View File
@@ -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
+21
View File
@@ -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
}
+17 -13
View File
@@ -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(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<script>%s</script>
</body>
</html>`, 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
+2 -2
View File
@@ -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<ApiResponse>}
*/
get: async (key) => {
@@ -2214,7 +2214,7 @@ export class API {
/**
* Set setting by key and value.
*
* @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'} key
* @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key
* @param {string} value
* @returns {Promise<ApiResponse>}
*/
+138
View File
@@ -17,12 +17,14 @@
import PasswordField from '$lib/components/PasswordField.svelte';
import TextField from '$lib/components/TextField.svelte';
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
import SimpleCodeEditor from '$lib/components/editor/SimpleCodeEditor.svelte';
import { AppStateService } from '$lib/service/appState';
import { hideIsLoading, showIsLoading } from '$lib/store/loading';
import { addToast } from '$lib/store/toast';
import { onMount } from 'svelte';
import { onClickCopy } from '$lib/utils/common';
import { displayMode, DISPLAY_MODE } from '$lib/store/displayMode';
import ConditionalDisplay from '$lib/components/ConditionalDisplay.svelte';
const logLevels = ['debug', 'info', 'warn', 'error'];
const dbLogLevels = ['silent', 'info', 'warn', 'error'];
@@ -78,6 +80,12 @@
let importForCompany = false;
let contextCompanyID = null;
// obfuscation template editor
let isObfuscationTemplateModalVisible = false;
let obfuscationTemplate = '';
let obfuscationTemplateError = '';
let isObfuscationTemplateSubmitting = false;
$: {
isCompanyContext = appState.isCompanyContext();
importForCompany = isCompanyContext;
@@ -465,6 +473,65 @@
isImportSubmitting = false;
}
};
/**
* Open obfuscation template modal
*/
const openObfuscationTemplateModal = async () => {
try {
showIsLoading();
const response = await api.option.get('obfuscation_template');
if (response.success) {
obfuscationTemplate = response.data.value || '';
} else {
obfuscationTemplateError = 'Failed to load template';
}
} catch (error) {
console.error('Failed to load obfuscation template:', error);
obfuscationTemplateError = 'Failed to load template';
} finally {
hideIsLoading();
isObfuscationTemplateModalVisible = true;
}
};
/**
* Close obfuscation template modal
*/
const closeObfuscationTemplateModal = () => {
isObfuscationTemplateModalVisible = false;
obfuscationTemplateError = '';
};
/**
* Submit obfuscation template
*/
const onSubmitObfuscationTemplate = async (event) => {
const saveOnly = event?.detail?.saveOnly || false;
isObfuscationTemplateSubmitting = true;
obfuscationTemplateError = '';
try {
const response = await api.option.set('obfuscation_template', obfuscationTemplate);
if (response.success) {
addToast(
saveOnly ? 'Obfuscation template saved' : 'Obfuscation template updated',
'Success'
);
if (!saveOnly) {
isObfuscationTemplateModalVisible = false;
}
} else {
obfuscationTemplateError = response.error || 'Failed to update template';
}
} catch (error) {
console.error('Failed to update obfuscation template:', error);
obfuscationTemplateError = 'Failed to update template';
} finally {
isObfuscationTemplateSubmitting = false;
}
};
</script>
<HeadTitle title="Settings" />
@@ -764,6 +831,40 @@
</div>
</div>
</div>
<!-- Obfuscation Template Section -->
<ConditionalDisplay show="blackbox">
<div
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 h-[420px] flex flex-col transition-colors duration-200"
>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
Obfuscation Template
</h2>
<div class="flex flex-col h-full">
<div class="space-y-4">
<p class="text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
Customize the template used when obfuscation is enabled to.
</p>
<div
class="bg-gray-50 dark:bg-gray-700 p-3 rounded-md transition-colors duration-200"
>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">
<strong>Internal obfuscation variable:</strong>
</p>
<p class="text-xs text-gray-600 dark:text-gray-400 font-mono">
{'{{.Script}}'}
</p>
</div>
</div>
<div class="mt-auto pt-4">
<Button size={'large'} on:click={openObfuscationTemplateModal}>Edit Template</Button
>
</div>
</div>
</div>
</ConditionalDisplay>
</div>
</div>
@@ -1163,4 +1264,41 @@
</FormGrid>
</Modal>
{/if}
{#if isObfuscationTemplateModalVisible}
<Modal
bind:visible={isObfuscationTemplateModalVisible}
headerText="Edit Obfuscation Template"
onClose={closeObfuscationTemplateModal}
{isSubmitting}
>
<FormGrid
on:submit={onSubmitObfuscationTemplate}
isSubmitting={isObfuscationTemplateSubmitting}
modalMode="update"
>
<div
class="w-80vw col-start-1 col-end-4 row-start-1 py-8 px-6 flex flex-col bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
>
<SimpleCodeEditor
bind:value={obfuscationTemplate}
language="html"
height="large"
showVimToggle={true}
showExpandButton={false}
/>
<p class="text-sm text-gray-600 dark:text-gray-300 my-4">
Example <code class="bg-gray-200 dark:bg-gray-700 p-1 rounded text-xs"
>{"eval(atob('{{base64 .Script}}'))"}</code
>
</p>
<FormError message={obfuscationTemplateError} />
</div>
<FormFooter
isSubmitting={isObfuscationTemplateSubmitting}
closeModal={closeObfuscationTemplateModal}
/>
</FormGrid>
</Modal>
{/if}
</main>