add script hash field and calculate hash on script changes. Also removed storing scripts in DB as base64 strings. Should fix #634

This commit is contained in:
sadnub
2021-11-28 13:23:10 -05:00
parent d5ad85725f
commit 06ccee5d18
7 changed files with 78 additions and 70 deletions

View File

@@ -1,3 +1,4 @@
import base64
from django.core.management.base import BaseCommand
from logs.models import PendingAction
@@ -20,3 +21,12 @@ class Command(BaseCommand):
for user in User.objects.filter(is_installer_user=True):
user.block_dashboard_login = True
user.save()
# convert script base64 field to text field
user_scripts = Script.objects.exclude(script_type="builtin").filter(script_body="")
for script in user_scripts:
# decode base64 string
script.script_body = base64.b64decode(script.code_base64.encode("ascii", "ignore")).decode(
"ascii", "ignore"
)
script.hash_script_body() # also saves script

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2.9 on 2021-11-28 16:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0014_alter_script_filename'),
]
operations = [
migrations.AddField(
model_name='script',
name='script_body',
field=models.TextField(blank=True, default=''),
),
migrations.AddField(
model_name='script',
name='script_hash',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AlterField(
model_name='script',
name='code_base64',
field=models.TextField(blank=True, default=''),
),
]

View File

@@ -1,5 +1,7 @@
import base64
import re
import hmac
import hashlib
from typing import List
from django.contrib.postgres.fields import ArrayField
@@ -40,7 +42,9 @@ class Script(BaseAuditModel):
syntax = TextField(null=True, blank=True)
favorite = models.BooleanField(default=False)
category = models.CharField(max_length=100, null=True, blank=True)
code_base64 = models.TextField(null=True, blank=True, default="")
script_body = models.TextField(blank=True, default="")
script_hash = models.CharField(max_length=100, null=True, blank=True)
code_base64 = models.TextField(blank=True, default="") # deprecated
default_timeout = models.PositiveIntegerField(default=90)
def __str__(self):
@@ -48,12 +52,7 @@ class Script(BaseAuditModel):
@property
def code_no_snippets(self):
if self.code_base64:
return base64.b64decode(self.code_base64.encode("ascii", "ignore")).decode(
"ascii", "ignore"
)
else:
return ""
return self.script_body if self.script_body else ""
@property
def code(self):
@@ -78,6 +77,13 @@ class Script(BaseAuditModel):
else:
return code
def hash_script_body(self):
from django.conf import settings
msg = self.code.encode()
self.script_hash = hmac.new(settings.SECRET_KEY.encode(), msg, hashlib.sha256).hexdigest()
self.save()
@classmethod
def load_community_scripts(cls):
import json
@@ -130,24 +136,8 @@ class Script(BaseAuditModel):
i.filename = script["filename"] # type: ignore
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
script_bytes = (
f.read().decode("utf-8").encode("ascii", "ignore")
)
i.code_base64 = base64.b64encode(script_bytes).decode("ascii") # type: ignore
i.save( # type: ignore
update_fields=[
"name",
"description",
"category",
"default_timeout",
"code_base64",
"shell",
"args",
"filename",
"syntax",
]
)
i.script_body = f.read().decode('utf-8') # type: ignore
i.hash_script_body() # also saves script
# check if script was added without a guid
elif cls.objects.filter(
@@ -170,39 +160,17 @@ class Script(BaseAuditModel):
with open(
os.path.join(scripts_dir, script["filename"]), "rb"
) as f:
script_bytes = (
f.read().decode("utf-8").encode("ascii", "ignore")
)
s.code_base64 = base64.b64encode(script_bytes).decode(
"ascii"
)
s.save(
update_fields=[
"guid",
"name",
"description",
"category",
"default_timeout",
"code_base64",
"shell",
"args",
"filename",
"syntax",
]
)
s.script_body = f.read().decode('utf-8')
s.hash_script_body() # also saves the script
else:
print(f"Adding new community script: {script['name']}")
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
script_bytes = (
f.read().decode("utf-8").encode("ascii", "ignore")
)
code_base64 = base64.b64encode(script_bytes).decode("ascii")
script_body = f.read().decode('utf-8')
cls(
code_base64=code_base64,
new_script = cls(
script_body=script_body,
guid=script["guid"],
name=script["name"],
description=script["description"],
@@ -213,7 +181,8 @@ class Script(BaseAuditModel):
args=args,
filename=script["filename"],
syntax=syntax,
).save()
)
new_script.hash_script_body() # also saves script
# delete community scripts that had their name changed
cls.objects.filter(script_type="builtin", guid=None).delete()

View File

@@ -22,6 +22,7 @@ class ScriptTableSerializer(ModelSerializer):
class ScriptSerializer(ModelSerializer):
script_hash = ReadOnlyField()
class Meta:
model = Script
fields = [
@@ -32,7 +33,8 @@ class ScriptSerializer(ModelSerializer):
"args",
"category",
"favorite",
"code_base64",
"script_body",
"script_hash",
"default_timeout",
"syntax",
"filename",
@@ -41,10 +43,11 @@ class ScriptSerializer(ModelSerializer):
class ScriptCheckSerializer(ModelSerializer):
code = ReadOnlyField()
script_hash = ReadOnlyField
class Meta:
model = Script
fields = ["code", "shell"]
fields = ["code", "shell", "script_hash"]
class ScriptSnippetSerializer(ModelSerializer):

View File

@@ -1,4 +1,3 @@
import base64
import asyncio
from django.shortcuts import get_object_or_404
@@ -37,6 +36,8 @@ class GetAddScripts(APIView):
serializer.is_valid(raise_exception=True)
obj = serializer.save()
obj.hash_script_body()
return Response(f"{obj.name} was added!")
@@ -64,6 +65,8 @@ class GetUpdateDeleteScript(APIView):
serializer.is_valid(raise_exception=True)
obj = serializer.save()
obj.hash_script_body()
return Response(f"{obj.name} was edited!")
def delete(self, request, pk):

View File

@@ -82,7 +82,7 @@
/>
</div>
<v-ace-editor
v-model:value="code"
v-model:value="formScript.script_body"
class="col-8"
:lang="formScript.shell === 'cmd' ? 'batchfile' : formScript.shell"
:theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
@@ -111,7 +111,7 @@
dense
flat
label="Test Script"
:disable="!agent || !code || !formScript.default_timeout"
:disable="!agent || !formScript.script_body || !formScript.default_timeout"
@click="openTestScriptModal"
/>
</template>
@@ -177,11 +177,10 @@ export default {
// script form logic
const script = props.script
? ref(Object.assign({}, props.script))
: ref({ shell: "powershell", default_timeout: 90, args: [] });
? ref(Object.assign({}, { ...props.script, script_body: "" }))
: ref({ shell: "powershell", default_timeout: 90, args: [], script_body: "" });
if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
const code = ref("");
const maximized = ref(false);
const loading = ref(false);
const agentLoading = ref(false);
@@ -201,16 +200,13 @@ export default {
// get code if editing or cloning script
if (props.script)
downloadScript(script.value.id, { with_snippets: props.readonly }).then(r => {
code.value = r.code;
script.value.script_body = r.code;
});
async function submitForm() {
loading.value = true;
let result = "";
try {
// base64 encode the script text
script.value.code_base64 = btoa(code.value);
// edit existing script
if (props.script && !props.clone) {
result = await editScript(script.value);
@@ -231,7 +227,7 @@ export default {
$q.dialog({
component: TestScriptModal,
componentProps: {
script: { ...script.value, code: code.value },
script: { ...script.value },
agent: agent.value,
},
});
@@ -247,7 +243,6 @@ export default {
return {
// reactive data
formScript: script.value,
code,
maximized,
loading,
agentOptions,

View File

@@ -118,12 +118,12 @@ export default {
// base64 encode the script and delete file
const reader = new FileReader();
reader.onloadend = () => {
script.value.code_base64 = reader.result.replace(/^data:.+;base64,/, "");
script.value.script_body = reader.result;
};
reader.readAsDataURL(file.value);
reader.readAsText(file.value);
} else {
script.value.code_base64 = "";
script.value.script_body = "";
}
});