Skip to content

Commit

Permalink
add: auto-push source + sending hash id and exposing frontend source …
Browse files Browse the repository at this point in the history
…associated with the attack
  • Loading branch information
domysh committed Nov 13, 2024
1 parent 585ef53 commit 6d9d3e5
Show file tree
Hide file tree
Showing 20 changed files with 458 additions and 288 deletions.
1 change: 1 addition & 0 deletions backend/models/exploit.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class ExploitSubmitForm(BaseModel):
status: AttackExecutionStatus
output: bytes|None = None
executed_by: UnHashedClientID|None = None
source_hash: str|None = None
target: TeamID|None = None
flags: list[str]

Expand Down
30 changes: 27 additions & 3 deletions backend/routes/exploits.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from env import EXPLOIT_SOURCES_DIR
from fastapi.responses import FileResponse
from exploitfarm.utils.config import ExploitConfig, check_exploit_config_exists
from exploitfarm.utils import calc_hash
from db import Exploit, DBSession, AttackExecution, sqla, ExploitSource, Flag, MANUAL_CLIENT_ID, Service, redis_channels, redis_conn
from utils.query import get_exploits_with_latest_attack, get_exploit_status
from db import ExploitID, ExploitSourceID, UnHashedClientID
Expand Down Expand Up @@ -126,16 +125,38 @@ async def exploit_submit(exploit_id: ExploitID, data: List[ExploitSubmitForm], d
if current_exploit_status == ExploitStatus.disabled:
trigger_exploit_update = True

stmt = sqla.select(ExploitSource).where(ExploitSource.exploit_id == exploit_id).order_by(ExploitSource.pushed_at.desc())
sources = (await db.scalars(stmt)).all()

hash_to_commit = {}
for ele in sources:
if ele.hash not in hash_to_commit:
hash_to_commit[ele.hash] = ele.id

async def attack_exec_commit(parsed_data: Dict[str, str]) -> int:
# Flags are in a separate table
flags = extract_values_by_regex(config.FLAG_REGEX, parsed_data["flags"])
del parsed_data["flags"]

#Explicit exploit id passed in the path
parsed_data["exploit_id"] = exploit.id

# Get the source id from the hash (or None if not found)
if "source_hash" in parsed_data:
parsed_data["exploit_source_id"] = hash_to_commit.get(parsed_data["source_hash"], None)
del parsed_data["source_hash"]

# Insert the attack execution
attack_execution = (await db.scalars(
sqla.insert(AttackExecution)
.values(parsed_data)
.returning(AttackExecution)
)).one()

# Track edits on this object
db.add(attack_execution)

# Insert the flags
if len(flags) > 0:
flags = (await db.scalars(
sqla.insert(Flag)
Expand All @@ -144,10 +165,13 @@ async def attack_exec_commit(parsed_data: Dict[str, str]) -> int:
.returning(Flag)
)).all()

# If there are no flags and the attack is done, set the status to noflags
if len(flags) == 0 and attack_execution.status == AttackExecutionStatus.done:
attack_execution.status = AttackExecutionStatus.noflags

# return the real flags enqueued
return len(flags)

results = await asyncio.gather(*[attack_exec_commit(data) for data in [ele.db_data() for ele in data]])
await db.commit()
if trigger_exploit_update:
Expand Down Expand Up @@ -223,7 +247,7 @@ async def new_exploit_source(
if not exploit:
raise HTTPException(404, "Exploit not found")
try:
hash_id = calc_hash(temp_dir)
hash_id = expl_config.hash()
except Exception as e:
raise MessageResponseInvalidError("Cannot calculate hash of exploit source", str(e))

Expand Down
4 changes: 2 additions & 2 deletions client/exploitfarm/tui/exploitinit.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ def save(self):
if check_exploit_config_exists(self.exploit_name):
self.show_error(f"[bold red]The exploit named '{self.exploit_name}' already exists")
return
x_conf = ExploitConfig.new(self.exploit_name, Language(self.lang), self.service)
x_conf.write(None if not self.edit else ".", exists_ok=True)
x_conf = ExploitConfig.new(self.exploit_name, Language(self.lang), self.service, generate_folder=True)
x_conf.write(exists_ok=True)
try:
x_conf.publish_exploit(self.config)
except Exception:
Expand Down
34 changes: 21 additions & 13 deletions client/exploitfarm/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from exploitfarm.model import get_lang, get_interpreter, get_filename, get_default_file
from exploitfarm.model import ExploitStatus
from datetime import datetime, timedelta

from exploitfarm.utils import calc_hash

class _ClientServerConfig(BaseModel):
https: bool = False
Expand Down Expand Up @@ -128,20 +128,26 @@ class ExploitConfig(BaseModel):
service: UUID

__exploit_lock = None
__folder = "."

@classmethod
@validate_call()
def new(cls, name:str, lang: Language|str, srv:UUID) -> Self:
def new(cls, name:str, lang: Language|str, srv:UUID, generate_folder: bool = False, folder: str = ".") -> Self:
if isinstance(lang, str):
lang = get_lang(lang)
return cls(
obj = cls(
uuid=uuid4(),
name=name,
language=lang,
service=srv,
interpreter=get_interpreter(lang),
run=get_filename(lang)
run=get_filename(lang),
)
if generate_folder:
obj.__folder = generate_exploit_folder_name(name, obj.uuid)
else:
obj.__folder = folder
return obj

def model_dump_toml(self) -> str:
return toml.dumps(self.model_dump(mode="json"))
Expand All @@ -150,21 +156,18 @@ def model_dump_toml(self) -> str:
def model_validate_toml(cls, data:str) -> Self:
return cls(**toml.loads(data))

def write(self, path:str = None, exists_ok:bool = False):
def write(self, exists_ok:bool = False):
#Autogenerated path
if path is None:
path = generate_exploit_folder_name(self.name, self.uuid)

pathexists = os.path.exists(path)
pathexists = os.path.exists(self.__folder)
if not pathexists and not exists_ok:
raise FileExistsError("Exploit folder already exists, can't write exploit config: Consider to change the exploit name")

if not pathexists:
os.makedirs(path)
with open(os.path.join(path, get_filename(self.language)), "wt") as f:
os.makedirs(self.__folder)
with open(os.path.join(self.__folder, get_filename(self.language)), "wt") as f:
f.write(get_default_file(self.language))

with open(os.path.join(path, EXPLOIT_CONFIG_FILE_NAME), "w") as f:
with open(os.path.join(self.__folder, EXPLOIT_CONFIG_FILE_NAME), "w") as f:
f.write(self.model_dump_toml())

def publish_exploit(self, config: ClientConfig):
Expand All @@ -190,10 +193,15 @@ def release_exploit(self):
if self.lock.acquired:
return self.lock.release()

def hash(self):
return calc_hash(self.__folder)

@classmethod
def read(cls, path:str) -> Self:
with open(os.path.join(path, EXPLOIT_CONFIG_FILE_NAME), "r") as f:
return cls.model_validate_toml(f.read())
obj = cls.model_validate_toml(f.read())
obj.__folder = path
return obj


def check_exploit_config_exists(path:str) -> bool:
Expand Down
4 changes: 2 additions & 2 deletions client/exploitfarm/utils/reqs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from datetime import datetime as dt
from exploitfarm.model import AttackMode, SetupStatus
from posixpath import join as urljoin
from . import calc_hash, exploit_tar_filter
from . import exploit_tar_filter
from requests.models import Response
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor

Expand Down Expand Up @@ -238,7 +238,7 @@ def upload_exploit_source(self,

if not force:
#Check hash not exists already
calculated_hash = calc_hash(path)
calculated_hash = expl_conf.hash()
for log in logs:
if log["hash"] == calculated_hash:
raise ExploitFarmClientError(f"Hash {calculated_hash} already exists")
Expand Down
17 changes: 13 additions & 4 deletions client/exploitfarm/xfarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from exploitfarm.tui.startxploit import start_exploit_tui
from exploitfarm.utils.reqs import ReqsError
from requests.exceptions import Timeout as RequestsTimeout
from exploitfarm.utils import DEV_MODE, calc_hash, clear_exploit_folder
from exploitfarm.utils import DEV_MODE, clear_exploit_folder
import multiprocessing
from queue import Queue
from exploitfarm import __version__
Expand Down Expand Up @@ -118,7 +118,9 @@ def start(
submit_pool_timeout: PositiveInt = typer.Option(3, help="The timeout for the submit pool to wait for new attack results and send flags"),
test: Optional[str] = typer.Option(None, "--test", "-t", help="Test the exploit"),
test_timeout: PositiveInt = typer.Option(30, help="The timeout for the test"),
max_mem_usage: PositiveInt = typer.Option(95, help="The maximum memory percentage to use of the PC")
max_mem_usage: PositiveInt = typer.Option(95, help="The maximum memory percentage to use of the PC"),
no_auto_push: bool = typer.Option(False, "--no-auto-push", "-n", help="Don't push the exploit source before starting the exploit"),
push_message: Optional[str] = typer.Option(None, "--push-message", "-m", help="The message of the push"),
):
if max_mem_usage > 100:
print("[bold red]Max memory usage can't be greater than 100%[/]")
Expand All @@ -142,6 +144,13 @@ def start(
print("[bold red]Can't connect to the server! The server is needed to start the exploit! Configure with 'xfarm config'[/]")
return

# Auto push exploit (if not in test mode)
if not no_auto_push and not test:
try:
push(push_message, force=False)
except Exception:
pass

try:
exploit_config = ExploitConfig.read(path)
if exploit_config.service not in [UUID(ele["id"]) for ele in g.config.status["services"]]:
Expand Down Expand Up @@ -358,7 +367,7 @@ def info(
if raw:
print(data)
else:
this_hash = calc_hash(".")
this_hash = expl_conf.hash()
you_are_here = False
print(f"[bold]Exploit Source Log of [underline]{escape(expl_conf.name)}[/] ([grey62]{escape(str(expl_conf.uuid))}[/])[/]\n")
if len(data) == 0:
Expand Down Expand Up @@ -413,7 +422,7 @@ def move(
return
commit_info = commit_info[0]

current_hash = calc_hash(".")
current_hash = expl_conf.hash()
if commit_info["hash"] == current_hash:
print("[bold yellow]The current commit is the same as the requested one![/]")
return
Expand Down
3 changes: 3 additions & 0 deletions client/exploitfarm/xploit.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class g:
kernel32 = None
win_ignore_ctrl_c = None
__callback_exit_win = None
exploit_hash: str|None = None

class InstanceStorage:
"""
Expand Down Expand Up @@ -208,6 +209,7 @@ def add(self,
"executed_by": self.client_id,
"target": team,
"flags": flags,
"source_hash": g.exploit_hash
}
if stats:
team_info = g.memory["teams"].get(team, default_team_info())
Expand Down Expand Up @@ -732,6 +734,7 @@ def start_xploit(config: ClientConfig, shared_dict:dict, print_queue: Queue, poo
g.max_mem_usage = max_mem_usage
g.exit_event = exit_event if exit_event else threading.Event()
g.restart_event = restart_event if restart_event else threading.Event()
g.exploit_hash = g.exploit_config.hash()

g.config.skio.on("config", update_server_config)
g.config.skio.on("team", update_server_config)
Expand Down
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"preview": "vite preview",
"tsapi": "bunx openapi-typescript http://127.0.0.1:5050/openapi.json -o ./src/utils/backend_types.ts"
},
"dependencies": {
"@mantine/carousel": "^7.12.2",
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/components/elements/ExploitSourceCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Box, Card, Title } from "@mantine/core"
import { DeleteButton, DownloadButton, EditButton } from "../inputs/Buttons"
import { getDateFormatted } from "@/utils/time"
import { deleteExploitSource, triggerDownloadExploitSource, useClientSolver } from "@/utils/queries"
import { useState } from "react"
import { ExploitSource } from "@/utils/types"
import { YesOrNoModal } from "../modals/YesOrNoModal"
import { notifications } from "@mantine/notifications"
import { useQueryClient } from "@tanstack/react-query"
import { EditExploitSource } from "../modals/EditExploitSource"

export const ExploitSourceCard = ({ src, latest, viewOnly }: { src: ExploitSource, latest?:boolean, viewOnly?:boolean }) => {

const clientSolver = useClientSolver()
const [deleteSourceId, setDeleteSourceId] = useState<string|undefined>()
const [editSourceId, setEditSourceId] = useState<string|undefined>()
const queryClient = useQueryClient()

return <Card key={src.id} style={{width:"100%"}} radius="md" mt="md">
<Box display="flex" style={{ justifyContent: "space-between", alignItems:"center"}}>
<Title order={3}>📦 {src.id} {latest?"(LATEST)":""}</Title>
<Box display="flex" style={{gap:10}}>
{!viewOnly?<EditButton onClick={()=>{
setEditSourceId(src.id)
}} />:null}
<DownloadButton onClick={()=>{
triggerDownloadExploitSource(src.hash)
}} />
<DeleteButton onClick={()=>{
setDeleteSourceId(src.id)
}} />
</Box>
</Box>

<Title order={4}>💬 Message: {src.message == ""?"No commit message":src.message}</Title>
<Box>#️⃣ hash: {src.hash}</Box>
<Box>⏱️ Pushed at: {getDateFormatted(src.pushed_at)}</Box>
<Box>👤 By: {clientSolver(src.pushed_by)} ({src.arch} {src.distro} {src.os_type})</Box>
<YesOrNoModal
message="Are you sure you want to delete the exploit commit? This could trigger the stop of synced exploit executions!"
open={deleteSourceId != null}
onClose={()=>setDeleteSourceId(undefined)}
onConfirm={()=>{
deleteExploitSource(deleteSourceId??"").then(()=>{
notifications.show({ title: "Exploit commit deleted", message: "The exploit commit has been deleted successfully", color: "green" })
queryClient.refetchQueries({ queryKey: ["exploits", "sources", src.exploit], })
}).catch((err)=>{
notifications.show({ title: "Error deleting the exploit commit", message: `An error occurred while deleting the exploit commit: ${err.message}`, color: "red" })
}).finally(()=>{ setDeleteSourceId(undefined) })
}}
title={"Deleting exploit commit"}
/>
<EditExploitSource source_id={editSourceId} onClose={() => setEditSourceId(undefined)} exploit_id={src.exploit} />
</Card>
}
2 changes: 1 addition & 1 deletion frontend/src/components/elements/StatusIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const flagStatusTable = {
name: "All",
color: "white",
icon: FaAsterisk,
label: "All attacks"
label: "All flags"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { attacksQuery, useClientSolver, useExtendedExploitSolver, useTeamSolver } from "@/utils/queries";
import { attacksQuery, exploitsSourcesQuery, useClientSolver, useExtendedExploitSolver, useTeamSolver } from "@/utils/queries";
import { useGlobalStore } from "@/utils/stores";
import { Alert, Box, Modal, ScrollArea, Space, Title } from "@mantine/core"
import { showNotification } from "@mantine/notifications";
Expand All @@ -14,13 +14,17 @@ import { MdTimerOff } from "react-icons/md";
import { FaPersonRunning } from "react-icons/fa6";
import { calcAttackDuration } from "@/utils";
import { BsCardText } from "react-icons/bs";
import { ExploitSourceCard } from "../elements/ExploitSourceCard";

export const AttackExecutionDetailsModal = (props:{ opened:boolean, close:()=>void, attackId:number }) => {

if (!props.opened) return null

const attackQuery = attacksQuery(1, { id: props.attackId })
const attack = attackQuery.data?.items[0]??null
const sourceQuery = exploitsSourcesQuery(attack?.exploit??undefined)
const usedSource = sourceQuery.data?.find((src) => src.id == attack?.exploit_source)??null
const isUsedSourceLatest = sourceQuery.data?.length??0 > 0 ? sourceQuery.data?.[0]?.id == usedSource?.id : false
const setLoading = useGlobalStore((store) => store.setLoader)
const clientSolver = useClientSolver()
const teamSolver = useTeamSolver()
Expand Down Expand Up @@ -85,6 +89,8 @@ export const AttackExecutionDetailsModal = (props:{ opened:boolean, close:()=>vo
</ScrollArea.Autosize>
</Alert>

{usedSource?<ExploitSourceCard src={usedSource} latest={isUsedSourceLatest} viewOnly />:null}

</Box>:null}

</Modal>
Expand Down
Loading

0 comments on commit 6d9d3e5

Please sign in to comment.