├── .dockerignore ├── .github └── workflows │ ├── docker-image.yml │ ├── python-publish-xfarm.yml │ └── python-publish.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── backend ├── app.py ├── db.py ├── env.py ├── models │ ├── __init__.py │ └── config.py ├── requirements.txt ├── routes │ ├── clients.py │ ├── exploits.py │ ├── flags.py │ ├── groups.py │ ├── services.py │ ├── submitters.py │ └── teams.py ├── sqla.py ├── utils │ ├── __init__.py │ ├── auth.py │ ├── binkey.py │ ├── query.py │ └── redis_pipe.py └── workers │ ├── __init__.py │ ├── group_manager.py │ ├── skio.py │ ├── stats.py │ └── submitter.py ├── client ├── MANIFEST.in ├── README.md ├── check_py12_str.sh ├── exploitfarm │ ├── __init__.py │ ├── __main__.py │ ├── _init_exposed.py │ ├── models │ │ ├── __init__.py │ │ ├── client.py │ │ ├── dbtypes.py │ │ ├── enums.py │ │ ├── exploit.py │ │ ├── flags.py │ │ ├── groups.py │ │ ├── response.py │ │ ├── service.py │ │ ├── submitter.py │ │ └── teams.py │ ├── tui │ │ ├── __init__.py │ │ ├── config.py │ │ ├── exploitdownload.py │ │ ├── exploitinit.py │ │ ├── group_prejoin.py │ │ ├── group_select.py │ │ ├── login.py │ │ ├── shared_attack_group.py │ │ └── startxploit.py │ ├── utils │ │ ├── __init__.py │ │ ├── cmd.py │ │ ├── config.py │ │ ├── exploit.py │ │ ├── reqs.py │ │ └── windows_close_fix.py │ ├── xfarm.py │ └── xploit.py ├── requirements.txt ├── setup.py ├── xfarm └── xfarm-pip │ ├── README.md │ ├── setup.py │ └── xfarm │ ├── __init__.py │ └── __main__.py ├── db.dev.yml ├── docs ├── .gitignore ├── Architecture.png ├── Business Model Canvas.png ├── ExploitFarmDB.png ├── RoadToExploitFarm1.0.0.pdf ├── SharedAttacksDesign.png ├── architecture.puml ├── attack_assign.png ├── attack_assign.puml ├── attack_sequence.png ├── attack_sequence.puml ├── backlog.png ├── db.puml ├── demo-video.md ├── documentation.pdf ├── documentation.tex ├── exploitfarm-web.png ├── general_layout.png ├── general_use_case.png ├── general_use_case.puml ├── logo.png ├── scheduling.png ├── scheduling.puml ├── shared_attack_sequence.png ├── shared_attack_sequence.puml └── xfarm-start-cmd.png ├── frontend ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── bun.lockb ├── index.html ├── package.json ├── postcss.config.cjs ├── public │ ├── .gitkeep │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── logo.png │ ├── mstile-150x150.png │ └── site.webmanifest ├── src │ ├── App.tsx │ ├── components │ │ ├── LoginProvider.tsx │ │ ├── MainLayout.tsx │ │ ├── charts │ │ │ ├── LineChartAttackView.tsx │ │ │ ├── LineChartFlagView.tsx │ │ │ └── LineChartTeamsView.tsx │ │ ├── elements │ │ │ ├── CustomMonacoEditor.tsx │ │ │ ├── ExploitSourceCard.tsx │ │ │ ├── ExploitsBar.tsx │ │ │ ├── StatusIcon.tsx │ │ │ └── WelcomeTitle.tsx │ │ ├── inputs │ │ │ ├── Buttons.tsx │ │ │ ├── Controllers.tsx │ │ │ ├── KArgsInput.tsx │ │ │ └── TeamSelector.tsx │ │ ├── modals │ │ │ ├── AddEditServiceModal.tsx │ │ │ ├── AttackExecutionDetailModal.tsx │ │ │ ├── AttackModeHelpModal.tsx │ │ │ ├── DeleteExploitModal.tsx │ │ │ ├── DeleteGroupModal.tsx │ │ │ ├── EditExploitModal.tsx │ │ │ ├── EditExploitSource.tsx │ │ │ ├── EditGroupModal.tsx │ │ │ ├── ExploitDetailModal.tsx │ │ │ ├── ManualSubmissionModal.tsx │ │ │ ├── SubmitterHelpModal.tsx │ │ │ ├── SubmitterModal.tsx │ │ │ ├── TeamEditModal.tsx │ │ │ ├── TestSubmissionModal.tsx │ │ │ └── YesOrNoModal.tsx │ │ ├── screens │ │ │ ├── AttackScreen.tsx │ │ │ ├── FlagsScreen.tsx │ │ │ ├── HomePage.tsx │ │ │ ├── OptionScreen.tsx │ │ │ ├── SetupScreen.tsx │ │ │ └── TeamsScreen.tsx │ │ └── tables │ │ │ ├── ClientTable.tsx │ │ │ ├── ExploitTable.tsx │ │ │ ├── GroupTable.tsx │ │ │ └── ServiceTable.tsx │ ├── index.scss │ ├── main.tsx │ ├── monaco-theme │ │ └── OneDarkProDarker.json │ ├── svg │ │ ├── bomb.svg │ │ ├── connected-server.svg │ │ ├── flag.svg │ │ ├── joystick.svg │ │ ├── laptop.svg │ │ └── server.svg │ ├── utils │ │ ├── backend_types.ts │ │ ├── env.ts │ │ ├── index.ts │ │ ├── net.ts │ │ ├── queries.tsx │ │ ├── stores.ts │ │ ├── time.ts │ │ └── types.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── scripts ├── ccit_auto_setup.py └── submitters │ └── ccit_submitter.py ├── start.py └── tests ├── test_setup.py └── xploit_test ├── config.toml └── main.py /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | db-data 3 | data 4 | clinet/** 5 | .github/** 6 | .git/** 7 | clinet/node_modules/** 8 | .venv/** 9 | backend/.locks/** 10 | .venv 11 | /backend/backend.egg-info 12 | /backend/build 13 | exploitfarm-compose-tmp-file.yml -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@master 25 | with: 26 | platforms: all 27 | 28 | - name: Set up Docker Buildx 29 | id: buildx 30 | uses: docker/setup-buildx-action@master 31 | 32 | - name: Log in to the Container registry 33 | uses: docker/login-action@v3 34 | with: 35 | registry: ${{ env.REGISTRY }} 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Extract metadata (tags, labels) for Docker 40 | id: meta 41 | uses: docker/metadata-action@v5 42 | with: 43 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 44 | - name: Extract tag name 45 | id: tag 46 | run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT 47 | - name: Update version in setup.py 48 | run: >- 49 | sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/env.py; 50 | sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" frontend/src/utils/env.ts; 51 | sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" client/setup.py; 52 | sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" client/exploitfarm/__init__.py; 53 | - name: Build and push Docker image 54 | uses: docker/build-push-action@v5 55 | with: 56 | context: . 57 | builder: ${{ steps.buildx.outputs.name }} 58 | platforms: linux/amd64,linux/arm64 59 | push: true 60 | tags: ${{ steps.meta.outputs.tags }} 61 | labels: ${{ steps.meta.outputs.labels }} 62 | cache-from: type=gha 63 | cache-to: type=gha,mode=max 64 | -------------------------------------------------------------------------------- /.github/workflows/python-publish-xfarm.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package (xfarm alias) 10 | 11 | on: 12 | release: 13 | types: 14 | - published 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | deploy: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.x' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install build 34 | - name: Extract tag name 35 | id: tag 36 | run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT 37 | - name: Update version in setup.py 38 | run: >- 39 | sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" client/xfarm-pip/setup.py; 40 | - name: Build package 41 | run: cd client/xfarm-pip && python -m build && mv ./dist ../../ 42 | - name: Publish package 43 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 44 | with: 45 | user: __token__ 46 | password: ${{ secrets.PYPI_API_TOKEN_XFARM }} 47 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: 14 | - published 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | deploy: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.x' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install build 34 | - name: Extract tag name 35 | id: tag 36 | run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT 37 | - name: Update version in setup.py 38 | run: >- 39 | sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" client/setup.py; 40 | sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" client/exploitfarm/__init__.py; 41 | - name: Build package 42 | run: cd client && python -m build && mv ./dist ../ 43 | - name: Publish package 44 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 45 | with: 46 | user: __token__ 47 | password: ${{ secrets.PYPI_API_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | /db-data 3 | /data 4 | docs/pdflatex7620.fls 5 | .venv/ 6 | backend/.locks/** 7 | **/__pycache__/ 8 | **/__pycache__/** 9 | client/exploitfarm.egg-info/** 10 | client/exploitfarm.egg-info 11 | client/build/** 12 | client/build/ 13 | client/dist/** 14 | client/dist/ 15 | client/xfarm-pip/xfarm.egg-info/** 16 | client/xfarm-pip/xfarm.egg-info 17 | client/xfarm-pip/build/** 18 | client/xfarm-pip/build/ 19 | client/xfarm-pip/dist/** 20 | client/xfarm-pip/dist/ 21 | Pipfile 22 | Pipfile.lock 23 | tests/xploit_test/.flag_queue.json 24 | *.lock 25 | !uv.lock 26 | /backend/backend.egg-info 27 | /backend/build 28 | exploitfarm-compose-tmp-file.yml 29 | exploitfarm-volumes.tar.gz 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": [ 3 | "./client" 4 | ] 5 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | # Exploitfarm Dockerfile UUID signature 3 | # c9ce2441-d842-44d7-9178-dd1617efb8f6 4 | # Needed for start.py to detect the Dockerfile 5 | 6 | FROM --platform=$BUILDPLATFORM oven/bun AS frontend 7 | ENV NODE_ENV=production 8 | WORKDIR /build 9 | COPY ./frontend/package.json ./frontend/bun.lockb /build/ 10 | RUN bun install 11 | COPY ./frontend/ . 12 | RUN bun run build 13 | 14 | 15 | #Building main conteiner 16 | FROM --platform=$TARGETARCH python:3.13-slim AS base 17 | RUN pip install uv 18 | RUN apt-get update && apt-get install -y --no-install-recommends libcapstone-dev build-essential 19 | WORKDIR /execute 20 | ADD ./backend/requirements.txt /execute/requirements.txt 21 | RUN uv pip install --system --no-cache -r requirements.txt 22 | COPY ./client/ /tmp/client 23 | RUN uv pip install --system --no-cache /tmp/client && rm -rf /tmp/client 24 | 25 | FROM --platform=$TARGETARCH base AS final 26 | 27 | COPY ./backend/ /execute/ 28 | COPY --from=frontend /build/dist/ ./frontend/ 29 | 30 | CMD ["python3", "/execute/app.py"] 31 | -------------------------------------------------------------------------------- /backend/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" 4 | DEBUG = os.getenv("DEBUG", "").lower() in ["true", "1", "t"] 5 | CORS_ALLOW = os.getenv("CORS_ALLOW", "").lower() in ["true", "1", "t"] 6 | RESET_DB_DANGEROUS = os.getenv("RESET_DB_DANGEROUS", "").lower() in ["true", "1", "t"] 7 | PRINT_SQL = os.getenv("PRINT_SQL", "").lower() in ["true", "1", "t"] 8 | 9 | JWT_ALGORITHM = "HS256" 10 | #JWT_EXPIRE_H = 3 11 | 12 | POSTGRES_USER = os.getenv("POSTGRES_USER", "exploitfarm") 13 | POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "exploitfarm") 14 | POSTGRES_DB = os.getenv("POSTGRES_DB", "exploitfarm") 15 | DB_HOST = os.getenv("DB_HOST", "127.0.0.1" if DEBUG else "database") 16 | DB_PORT = os.getenv("DB_PORT", "5432") 17 | NTHREADS = int(os.getenv("NTHREADS", "3")) 18 | POSTGRES_URL = os.getenv("POSTGRES_URL", f"postgresql+asyncpg://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{DB_HOST}:{DB_PORT}/{POSTGRES_DB}") 19 | 20 | DATA_DIR = os.path.abspath( 21 | os.path.join(os.path.dirname(__file__), ".." if DEBUG else ".", "data") 22 | ) 23 | EXPLOIT_STORES_DIR = os.path.join(DATA_DIR, "exploit-stores") 24 | EXPLOIT_SOURCES_DIR = os.path.join(DATA_DIR, "exploit-sources") -------------------------------------------------------------------------------- /backend/models/__init__.py: -------------------------------------------------------------------------------- 1 | from exploitfarm.models.response import * # noqa: F403 -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi[all]==0.115.3 2 | httpx==0.27.2 3 | passlib[bcrypt]==1.7.4 4 | pwntools>=4.13.1 5 | requests>=2.32.3 6 | uvicorn[standard]==0.32.0 7 | asyncpg==0.30.0 8 | uvloop==0.21.0 9 | fastapi-pagination==0.12.31 10 | pyjwt==2.9.0 11 | dirhash==0.5.0 12 | python-multipart==0.0.19 13 | sqlalchemy[asyncio]==2.0.36 14 | sqlmodel==0.0.22 15 | redis>=6 16 | python-socketio==5.11.4 17 | exploitfarm 18 | orjson 19 | -------------------------------------------------------------------------------- /backend/routes/clients.py: -------------------------------------------------------------------------------- 1 | from exploitfarm.models.client import ClientDTO, ClientAddForm, ClientEditForm 2 | from exploitfarm.models.response import MessageResponse 3 | from typing import List 4 | from fastapi import APIRouter, HTTPException 5 | from utils import json_like 6 | from db import Client, DBSession, sqla, MANUAL_CLIENT_ID 7 | from exploitfarm.models.dbtypes import client_id_hashing, check_client_id_hashing 8 | from db import redis_channels, redis_conn 9 | from db import ClientID, UnHashedClientID 10 | 11 | router = APIRouter(prefix="/clients", tags=["Clients"]) 12 | 13 | @router.get("", response_model=List[ClientDTO]) 14 | async def client_get(db: DBSession): 15 | return (await db.scalars(sqla.select(Client))).all() 16 | 17 | @router.post("", response_model=MessageResponse[ClientDTO]) 18 | async def client_new_or_edit(data: ClientAddForm, db: DBSession): 19 | client = (await db.scalars( 20 | sqla.insert(Client) 21 | .values(json_like(data)) 22 | .on_conflict_do_update( 23 | index_elements=[Client.id], 24 | set_=json_like(data) 25 | ).returning(Client) 26 | )).one() 27 | await db.commit() 28 | await redis_conn.publish(redis_channels.client, "update") 29 | return { "message": "Client created/updated successfully", "response": client } 30 | 31 | @router.delete("/{client_id}", response_model=MessageResponse[ClientDTO]) 32 | async def client_delete_hashed_or_uuid(client_id: ClientID, db: DBSession): 33 | if client_id == MANUAL_CLIENT_ID: 34 | raise HTTPException(400, "You cannot delete the manual client") 35 | try: 36 | if not check_client_id_hashing(client_id): 37 | client_id = UnHashedClientID(client_id) 38 | client_id = client_id_hashing(client_id) 39 | except Exception: 40 | raise HTTPException(400, "Invalid hash or UUID identifier (use sha256- before the hash hex-digest, otherwise use a valid UUID)") 41 | 42 | result = (await db.scalars( 43 | sqla.delete(Client) 44 | .where(Client.id == client_id) 45 | .returning(Client) 46 | )).one_or_none() 47 | 48 | if not result: 49 | raise HTTPException(404, "Client not found") 50 | 51 | await db.commit() 52 | await redis_conn.publish(redis_channels.client, "update") 53 | return { "message": "Client deleted successfully", "response": json_like(result, unset=True) } 54 | 55 | @router.put("/{client_id}", response_model=MessageResponse[ClientDTO]) 56 | async def client_edit(client_id: UnHashedClientID, data: ClientEditForm, db: DBSession): 57 | client = (await db.scalars(sqla.update(Client).values(json_like(data)).where(Client.id == client_id).returning(Client))).one_or_none() 58 | if not client: 59 | raise HTTPException(404, "Client not found") 60 | await db.commit() 61 | await redis_conn.publish(redis_channels.client, "update") 62 | return { "message": "Client updated successfully", "response": json_like(client, unset=True) } -------------------------------------------------------------------------------- /backend/routes/groups.py: -------------------------------------------------------------------------------- 1 | from exploitfarm.models.groups import GroupDTO, AddGroupForm, EditGroupForm 2 | from exploitfarm.models.response import MessageResponse 3 | from exploitfarm.models.enums import GroupStatus 4 | from typing import List 5 | from fastapi import APIRouter, HTTPException 6 | from utils import json_like 7 | from db import DBSession, sqla 8 | from db import redis_channels, redis_conn, AttackGroupID 9 | from db import AttackGroup 10 | from typing import Tuple 11 | from utils.query import get_groups_with_latest_attack 12 | import asyncio 13 | from db import AttackExecution 14 | from models.config import Configuration, SetupStatus 15 | 16 | router = APIRouter(prefix="/groups", tags=["Attack Groups"]) 17 | 18 | @router.get("", response_model=List[GroupDTO]) 19 | async def group_get(db: DBSession): 20 | groups = await get_groups_with_latest_attack(db) 21 | async def result(result: sqla.Row[Tuple[AttackGroup, AttackExecution]]): 22 | group, latest_attack = result.tuple() 23 | members = await redis_conn.smembers(f"group:{group.id}:members") 24 | status = GroupStatus.active 25 | if members is None or len(members) == 0: 26 | status = GroupStatus.inactive 27 | members = set() 28 | return GroupDTO( 29 | **json_like(group, mode="python", unset=True), 30 | last_attack_at=latest_attack.received_at if latest_attack else None, 31 | members=members, 32 | status=status 33 | ) 34 | return await asyncio.gather(*[result(ele) for ele in groups]) 35 | 36 | @router.post("", response_model=MessageResponse[GroupDTO]) 37 | async def new_group(data: AddGroupForm, db: DBSession): 38 | config = await Configuration.get_from_db() 39 | # This is useful to avoid unexpected errors causing to start a group without a setup 40 | if config.SETUP_STATUS == SetupStatus.SETUP: 41 | raise HTTPException(400, "Setup is not completed, can't create groups") 42 | group = (await db.scalars( 43 | sqla.insert(AttackGroup) 44 | .values(data.db_data()) 45 | .returning(AttackGroup) 46 | )).one() 47 | await db.commit() 48 | await redis_conn.publish(redis_channels.attack_group, f"add:{group.id}") 49 | return { "message": "Group created successfully", "response": json_like(group, unset=True) } 50 | 51 | @router.delete("/{group_id}", response_model=MessageResponse[GroupDTO]) 52 | async def delete_group(group_id: AttackGroupID, db: DBSession): 53 | result = (await db.scalars( 54 | sqla.delete(AttackGroup) 55 | .where(AttackGroup.id == group_id) 56 | .returning(AttackGroup) 57 | )).one_or_none() 58 | if not result: 59 | raise HTTPException(404, "Group not found") 60 | await db.commit() 61 | await redis_conn.publish(redis_channels.attack_group, f"delete:{result.id}") 62 | return { "message": "Client deleted successfully", "response": json_like(result, unset=True) } 63 | 64 | @router.put("/{group_id}", response_model=MessageResponse[GroupDTO]) 65 | async def client_edit(group_id: AttackGroupID, data: EditGroupForm, db: DBSession): 66 | group = (await db.scalars( 67 | sqla.update(AttackGroup) 68 | .values(json_like(data)) 69 | .where(AttackGroup.id == group_id) 70 | .returning(AttackGroup) 71 | )).one_or_none() 72 | if not group: 73 | raise HTTPException(404, "Group not found") 74 | await db.commit() 75 | await redis_conn.publish(redis_channels.attack_group, f"update:{group.id}") 76 | return { "message": "Group updated successfully", "response": json_like(group, unset=True) } -------------------------------------------------------------------------------- /backend/routes/services.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import APIRouter, HTTPException 3 | from db import Service, DBSession, sqla, redis_conn, redis_channels, ServiceID 4 | from exploitfarm.models.service import ServiceDTO, ServiceAddForm, ServiceEditForm 5 | from exploitfarm.models.response import MessageResponse 6 | from utils import json_like 7 | 8 | router = APIRouter(prefix="/services", tags=["Services"]) 9 | 10 | @router.get("", response_model=List[ServiceDTO]) 11 | async def service_get(db: DBSession): 12 | return (await db.scalars(sqla.select(Service))).all() 13 | 14 | @router.post("", response_model=MessageResponse[ServiceDTO]) 15 | async def service_new(data: ServiceAddForm, db: DBSession): 16 | service = (await db.scalars( 17 | sqla.insert(Service) 18 | .values(json_like(data)) 19 | .returning(Service) 20 | )).one() 21 | await db.commit() 22 | await redis_conn.publish(redis_channels.service, "update") 23 | return { "message": "Service created successfully", "response": service } 24 | 25 | @router.delete("/{service_id}", response_model=MessageResponse[ServiceDTO]) 26 | async def service_delete(service_id: ServiceID, db: DBSession): 27 | 28 | service = (await db.scalars( 29 | sqla.delete(Service) 30 | .where(Service.id == service_id) 31 | .returning(Service) 32 | )).one_or_none() 33 | if not service: 34 | raise HTTPException(404, "Service not found") 35 | await db.commit() 36 | await redis_conn.publish(redis_channels.service, "update") 37 | return { "message": "Service deleted successfully", "response": service } 38 | 39 | @router.put("/{service_id}", response_model=MessageResponse[ServiceDTO]) 40 | async def service_edit(service_id: ServiceID, data: ServiceEditForm, db: DBSession): 41 | service = (await db.scalars( 42 | sqla.update(Service) 43 | .where(Service.id == service_id) 44 | .values(json_like(data)) 45 | .returning(Service) 46 | )).one_or_none() 47 | if not service: 48 | raise HTTPException(404, "Service not found") 49 | await db.commit() 50 | await redis_conn.publish(redis_channels.service, "update") 51 | return { "message": "Service updated successfully", "response": service } -------------------------------------------------------------------------------- /backend/routes/teams.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import APIRouter 3 | import asyncio 4 | from db import Team, DBSession, sqla, TeamID, redis_channels, redis_conn 5 | from exploitfarm.models.teams import TeamDTO, TeamAddForm, TeamEditForm 6 | from utils import json_like 7 | from exploitfarm.models.response import MessageResponse 8 | 9 | router = APIRouter(prefix="/teams", tags=["Teams"]) 10 | 11 | @router.get("", response_model=List[TeamDTO]) 12 | async def team_get(db: DBSession): 13 | stmt = sqla.select(Team) 14 | return json_like((await db.scalars(stmt)).all(), unset=True) 15 | 16 | @router.post("", response_model=MessageResponse[List[TeamEditForm]]) 17 | async def team_new(data: List[TeamAddForm], db: DBSession): 18 | stmt = sqla.insert(Team).values([json_like(ele) for ele in data]).returning(Team) 19 | teams = (await db.scalars(stmt)).all() 20 | await redis_conn.publish(redis_channels.team, "update") 21 | return { "message": "Teams created successfully", "response": json_like(teams, unset=True) } 22 | 23 | @router.post("/delete", response_model=MessageResponse[List[TeamDTO]]) 24 | async def team_delete_list(data: List[TeamID], db: DBSession): 25 | stsm = sqla.delete(Team).where(Team.id.in_(data)).returning(Team) 26 | teams = (await db.scalars(stsm)).all() 27 | await db.commit() 28 | await redis_conn.publish(redis_channels.team, "update") 29 | return { "message": "Teams deleted successfully", "response": json_like(teams, unset=True) } 30 | 31 | @router.delete("/{team_id}", response_model=MessageResponse[TeamDTO]) 32 | async def team_delete(team_id: TeamID, db: DBSession): 33 | stmt = sqla.delete(Team).where(Team.id == team_id).returning(Team) 34 | team = (await db.scalars(stmt)).one() 35 | await db.commit() 36 | await redis_conn.publish(redis_channels.team, "update") 37 | return { "message": "Team deleted successfully", "response": json_like(team, unset=True) } 38 | 39 | @router.put("", response_model=MessageResponse[List[TeamEditForm]]) 40 | async def team_edit_list(data: List[TeamEditForm], db: DBSession): 41 | updates_queries = [ 42 | sqla.update(Team) 43 | .where(Team.id == ele.id) 44 | .values(json_like(ele, exclude=["id"])) 45 | .returning(Team) 46 | for ele in data 47 | ] 48 | teams = [o.one_or_none() for o in await asyncio.gather(*[db.scalars(ele) for ele in updates_queries])] 49 | teams = [team for team in teams if team is not None] 50 | await db.commit() 51 | await redis_conn.publish(redis_channels.team, "update") 52 | return { "message": "Teams updated successfully", "response": json_like(teams, unset=True) } 53 | 54 | -------------------------------------------------------------------------------- /backend/sqla.py: -------------------------------------------------------------------------------- 1 | 2 | #Mix of SQL Alchemy and PostgresSQL dialect feature 3 | 4 | from sqlalchemy import * # noqa: F403 5 | from sqlalchemy.dialects.postgresql import * # noqa: F403 6 | -------------------------------------------------------------------------------- /backend/utils/auth.py: -------------------------------------------------------------------------------- 1 | from models.config import Configuration 2 | from db import APP_SECRET 3 | from env import JWT_ALGORITHM 4 | import jwt 5 | from exploitfarm.models.enums import AuthStatus 6 | 7 | async def login_validation(token: str|None) -> AuthStatus: 8 | 9 | #App status checks 10 | config = await Configuration.get_from_db() 11 | 12 | if not config.login_enabled: 13 | return AuthStatus.ok 14 | 15 | #If the app is running and requires login 16 | if not token: 17 | return AuthStatus.nologin 18 | 19 | try: 20 | payload = jwt.decode(token, await APP_SECRET(), algorithms=[JWT_ALGORITHM]) 21 | authenticated: bool = payload.get("authenticated", False) 22 | if authenticated: 23 | return AuthStatus.ok 24 | else: 25 | return AuthStatus.wrong 26 | except Exception: 27 | return AuthStatus.invalid -------------------------------------------------------------------------------- /backend/utils/binkey.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import os 3 | from typing import Generator 4 | import shutil 5 | 6 | """ 7 | Format: 8 | | 2 bytes key len | 4 byte value len | key | value | 9 | keys are in utf-8 10 | """ 11 | 12 | class KVBinStore: 13 | """ 14 | A class to handle the reading and writing of key-value pairs in a binary file. 15 | """ 16 | 17 | def __init__(self, filename:str): 18 | self.filename = filename 19 | 20 | def __getitem__(self,key:str): 21 | return self.get(key) 22 | 23 | def __setitem__(self, key:str, value:bytes): 24 | self.set(key, value) 25 | 26 | def get(self, key:str, default=None): 27 | """ 28 | Retrieves the value associated with the given key from the binary file. 29 | """ 30 | for k, value in self.iterator(keyscan=True): 31 | if k == key: 32 | return value() 33 | 34 | return default 35 | 36 | def iterator(self, key:bool = False, value:bool = False, keyscan:bool = False): 37 | """ 38 | Returns an iterator that yields key-value pairs from the binary file. 39 | """ 40 | if os.path.isfile(self.filename): 41 | with open(self.filename, 'rb') as f: 42 | while True: 43 | sizes = f.read(6) 44 | if not sizes or len(sizes) != 6: 45 | break 46 | key_len, value_len = struct.unpack(' list[str, bytes]: 74 | return self.iterator(key=True, value=True) 75 | 76 | def keys(self) -> Generator[str, None, None]: 77 | for k, _ in self.iterator(key=True, value=False): 78 | yield k 79 | 80 | def values(self) -> Generator[bytes, None, None]: 81 | for _, v in self.iterator(key=False, value=True): 82 | yield v 83 | 84 | def __dict__(self): 85 | """ 86 | Returns a dictionary representation of the key-value pairs in the binary file. 87 | """ 88 | return dict(self.items()) 89 | 90 | def set(self, key:str, value:bytes): 91 | if not isinstance(value, bytes): 92 | raise TypeError("Value must be of type bytes") 93 | swap_path = self.filename+".swap" 94 | with open(swap_path, "wb") as f: 95 | f.write(struct.pack(' None: 35 | status = await login_validation(token) 36 | if status == AuthStatus.ok: 37 | return None 38 | if status == AuthStatus.nologin: 39 | raise ConnectionRefusedError("Authentication required") 40 | raise ConnectionRefusedError("Unauthorized") 41 | 42 | @sio_server.on("connect") 43 | async def sio_connect(sid, environ, auth): 44 | await check_login(auth.get("token")) 45 | await redis_conn.sadd("sid_list", sid) 46 | 47 | @sio_server.on("disconnect") 48 | async def sio_disconnect(sid): 49 | group, client = await redis_conn.mget(f"sid:{sid}:group", f"sid:{sid}:client") 50 | if isinstance(group, bytes): 51 | group = group.decode() 52 | if isinstance(client, bytes): 53 | client = client.decode() 54 | await redis_conn.srem("sid_list", 0, sid) 55 | if group and client: 56 | await redis_call(redis_conn, "leave-group", sid, group, client) 57 | 58 | @register_event(sio_server, "event-group", GroupResponseEvent, MessageResponse) 59 | async def event_group(sid: str, response_req: GroupResponseEvent): 60 | return await redis_call(redis_conn, "event-group", sid, response_req) 61 | 62 | @register_event(sio_server, "join-group", JoinRequest, MessageResponse[JoinRequestResponse]) 63 | async def join_group(sid, join_req: JoinRequest): 64 | return await redis_call(redis_conn, "join-group", sid, join_req) 65 | 66 | async def disconnect_all(): 67 | while True: 68 | sids = await redis_conn.spop("sid_list", count=100) 69 | if sids is None or len(sids) == 0: 70 | break 71 | for sid in sids: 72 | if isinstance(sid, bytes): 73 | sid = sid.decode() 74 | await sio_server.disconnect(sid) 75 | 76 | async def generate_listener_tasks(): 77 | for chann in REDIS_CHANNEL_PUBLISH_LIST: 78 | async def listener(chann=chann): 79 | await sio_server.emit(chann, "init") 80 | async with redis_conn.pubsub() as pubsub: 81 | await pubsub.subscribe(chann) 82 | while True: 83 | message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=None) 84 | if message: 85 | await sio_server.emit(chann, message) 86 | g.task_list.append(asyncio.create_task(listener())) 87 | 88 | async def password_change_listener(): 89 | async with redis_conn.pubsub() as pubsub: 90 | await pubsub.subscribe(redis_channels.password_change) 91 | while True: 92 | message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=None) 93 | if message: 94 | await disconnect_all() 95 | 96 | 97 | async def tasks_init(): 98 | try: 99 | await connect_db() 100 | await generate_listener_tasks() 101 | pwd_change = asyncio.create_task(password_change_listener()) 102 | logging.info("SocketIO manager started") 103 | await asyncio.gather(*g.task_list, pwd_change) 104 | except KeyboardInterrupt: 105 | pass 106 | finally: 107 | await close_db() 108 | 109 | def inital_setup(): 110 | try: 111 | while True: 112 | try: 113 | g.task_list = [] 114 | with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner: 115 | runner.run(tasks_init()) 116 | except Exception as e: 117 | traceback.print_exc() 118 | logging.exception(f"SocketIO loop failed: {e}, restarting loop") 119 | time.sleep(10) 120 | except (KeyboardInterrupt, StopLoop): 121 | logging.info("SocketIO stopped by KeyboardInterrupt") 122 | 123 | def run_skio_daemon() -> Process: 124 | p = Process(target=inital_setup) 125 | p.start() 126 | return p -------------------------------------------------------------------------------- /client/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt -------------------------------------------------------------------------------- /client/check_py12_str.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rg '(f".*\{.*".*}.*")|f'"'"'.*\{.*\'"'"'.*}.*'"'" 3 | -------------------------------------------------------------------------------- /client/exploitfarm/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = ( 2 | "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" 3 | ) 4 | 5 | from exploitfarm.utils import try_tcp_connection 6 | from exploitfarm.models.enums import AttackMode, SetupStatus, FlagStatus 7 | from exploitfarm.utils import DEV_MODE 8 | from exploitfarm._init_exposed import get_host, Prio 9 | from exploitfarm._init_exposed import nicenessify, get_config, random_str, print, Store 10 | from exploitfarm._init_exposed import os_unix, os_windows, session 11 | 12 | # Exported functions 13 | __all__ = [ 14 | "try_tcp_connection", 15 | "AttackMode", 16 | "SetupStatus", 17 | "get_host", 18 | "get_config", 19 | "random_str", 20 | "session", 21 | "print", 22 | "nicenessify", 23 | "Prio", 24 | "FlagStatus", 25 | "Store", 26 | "DEV_MODE", 27 | "os_unix", 28 | "os_windows", 29 | ] 30 | -------------------------------------------------------------------------------- /client/exploitfarm/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from exploitfarm.xfarm import run 4 | 5 | if __name__ == "__main__": 6 | run() 7 | -------------------------------------------------------------------------------- /client/exploitfarm/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/client/exploitfarm/models/__init__.py -------------------------------------------------------------------------------- /client/exploitfarm/models/client.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from pydantic import AwareDatetime 3 | from exploitfarm.models.dbtypes import UnHashedClientID, ClientID 4 | 5 | ###-- Client Models --### 6 | 7 | class ClientDTO(BaseModel): # Client id will be hashed before returning to make it secret (it's a soft secret, not a real one) 8 | id: ClientID # Using the hash we can only delete the client, but not edit it 9 | name: str|None = None 10 | created_at: AwareDatetime 11 | 12 | class ClientAddForm(BaseModel): 13 | id: UnHashedClientID 14 | name: str|None = None 15 | 16 | class ClientEditForm(BaseModel): 17 | name: str|None = None -------------------------------------------------------------------------------- /client/exploitfarm/models/dbtypes.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from datetime import datetime 3 | from hashlib import sha256 4 | from typing import Any, Union, Annotated 5 | from pydantic import BaseModel, BeforeValidator 6 | 7 | EnvKey = str 8 | ClientID = str 9 | ExploitID = UUID 10 | ServiceID = UUID 11 | TeamID = int 12 | AttackGroupID = UUID 13 | ExploitSourceID = UUID 14 | AttackExecutionID = int 15 | FlagID = int 16 | SubmitterID = int 17 | 18 | DateTime = datetime 19 | 20 | 21 | def extract_id_from_dict(x: Any) -> Any: 22 | if isinstance(x, dict): 23 | return x["id"] 24 | if isinstance(x, BaseModel): 25 | return x.id 26 | return x 27 | 28 | 29 | def client_id_hashing(client_id: Any) -> ClientID: 30 | if isinstance(client_id, Union[dict, BaseModel]): 31 | client_id = extract_id_from_dict(client_id) 32 | if isinstance(client_id, dict): 33 | raise ValueError("Invalid client_id") 34 | try: 35 | if not isinstance(client_id, UUID): 36 | client_id = UUID(client_id) 37 | except Exception: 38 | return str(client_id) 39 | return "sha256-"+sha256(str(client_id).lower().encode()).hexdigest().lower() 40 | 41 | def check_client_id_hashing(client_id: ClientID) -> bool: 42 | return client_id.startswith("sha256-") 43 | 44 | def verify_and_parse_uuid(value: str) -> UUID: 45 | try: 46 | return client_id_hashing(UUID(value)) 47 | except Exception: 48 | raise ValueError("Invalid UUID") 49 | 50 | UnHashedClientID = Annotated[str, BeforeValidator(verify_and_parse_uuid)] -------------------------------------------------------------------------------- /client/exploitfarm/models/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class ResponseStatus(Enum): 4 | OK = "ok" 5 | ERROR = "error" 6 | INVALID = "invalid" 7 | 8 | class ExploitStatus(Enum): 9 | active = 'active' 10 | disabled = 'disabled' 11 | 12 | class AuthStatus(Enum): 13 | ok = "ok" 14 | nologin = "nologin" 15 | wrong = "wrong" 16 | invalid = "invalid" 17 | 18 | class GroupStatus(Enum): 19 | active = 'active' 20 | inactive = 'inactive' 21 | 22 | class FlagStatus(Enum): 23 | ok = 'ok' 24 | wait = 'wait' 25 | timeout = 'timeout' 26 | invalid = 'invalid' 27 | 28 | class AttackExecutionStatus(Enum): 29 | done = 'done' 30 | noflags = 'noflags' 31 | crashed = 'crashed' 32 | 33 | class Language(Enum): 34 | python = "python" 35 | java = "java" 36 | javascript = "javascript" 37 | typescript = "typescript" 38 | csharp = "c#" 39 | cpp = "c++" 40 | php = "php" 41 | r = "r" 42 | kotlin = "kotlin" 43 | go = "go" 44 | ruby = "ruby" 45 | rust = "rust" 46 | lua = "lua" 47 | dart = "dart" 48 | perl = "perl" 49 | haskell = "haskell" 50 | other = "other" 51 | 52 | class MessageStatusLevel(Enum): 53 | info = "info" 54 | warning = "warning" 55 | error = "error" 56 | 57 | class AttackMode(Enum): 58 | WAIT_FOR_TIME_TICK = "wait-for-time-tick" 59 | TICK_DELAY = "tick-delay" 60 | LOOP_DELAY = "loop-delay" 61 | 62 | class SetupStatus(Enum): 63 | SETUP = "setup" 64 | RUNNING = "running" 65 | 66 | class GroupEventRequestType(Enum): 67 | DYNAMIC_TIMEOUT = "dynamic_timeout" 68 | DEADLINE_TIMOEOUT = "deadline_timeout" 69 | ATTACK_REQUEST = "attack_request" 70 | ATTACK_KILL = "attack_kill" 71 | KILLALL_ATTACKS = "killall_attacks" 72 | RUNNING_STATUS = "running_status" 73 | EXPLOIT_PULL = "exploit_pull" 74 | 75 | class GroupEventResponseType(Enum): 76 | ATTACK_ENDED = "attack_ended" 77 | SET_RUNNING_STATUS = "set_running_status" 78 | 79 | #Utility functions 80 | 81 | def get_lang(lang: str|Language) -> Language: 82 | if isinstance(lang, Language): 83 | return lang 84 | res = [ele for ele in list(Language) if ele.value == lang] 85 | if len(res) == 0: 86 | return Language.other 87 | return res[0] 88 | -------------------------------------------------------------------------------- /client/exploitfarm/models/exploit.py: -------------------------------------------------------------------------------- 1 | from exploitfarm.models.dbtypes import ExploitID 2 | from exploitfarm.models.enums import Language 3 | from exploitfarm.models.dbtypes import ServiceID, ClientID, UnHashedClientID, TeamID, ExploitSourceID, AttackGroupID 4 | from pydantic import BaseModel, AwareDatetime, Field, AliasChoices 5 | from exploitfarm.models.enums import AttackExecutionStatus 6 | from exploitfarm.utils import json_like 7 | from exploitfarm.models.enums import ExploitStatus 8 | 9 | ###-- Exploit Models --### 10 | 11 | class ExploitDTO(BaseModel): 12 | id: ExploitID 13 | name: str 14 | language: Language 15 | status: ExploitStatus|None = None 16 | last_update: AwareDatetime|None = None 17 | created_at: AwareDatetime 18 | service: ServiceID = Field(validation_alias=AliasChoices('service_id')) 19 | created_by: ClientID = Field(validation_alias=AliasChoices('created_by_id')) 20 | last_execution_by: ClientID|None = None 21 | last_execution_group_by: AttackGroupID|None = None 22 | last_source: ExploitSourceID|None = None 23 | 24 | class ExploitSourceDTO(BaseModel): 25 | id: ExploitSourceID 26 | hash: str 27 | message: str|None 28 | pushed_at: AwareDatetime 29 | pushed_by: ClientID = Field(validation_alias=AliasChoices('pushed_by_id')) 30 | os_type: str|None 31 | distro: str|None 32 | arch: str|None 33 | exploit: ExploitID = Field(validation_alias=AliasChoices('exploit_id')) 34 | 35 | def db_data(self): 36 | return json_like(self, convert_keys={ 37 | "pushed_by": "pushed_by_id", 38 | "exploit": "exploit_id" 39 | }) 40 | 41 | class ExploitAddForm(BaseModel): 42 | id: ExploitID 43 | name: str 44 | language: Language = Language.other 45 | service: ServiceID 46 | created_by: UnHashedClientID #Only the real client can associate an exploit to it 47 | 48 | def db_data(self): 49 | return json_like(self, convert_keys={ 50 | "created_by": "created_by_id", 51 | "service": "service_id" 52 | }) 53 | 54 | class ExploitNewForm(BaseModel): 55 | name: str|None = None 56 | language: Language|None = None 57 | service: ServiceID|None = None 58 | created_by: UnHashedClientID|None = None #Only the real client can associate an exploit to it 59 | 60 | class ExploitEditForm(BaseModel): 61 | name: str|None = None 62 | language: Language|None = None 63 | service: ServiceID|None = None 64 | 65 | def db_data(self): 66 | return json_like(self, convert_keys={ 67 | "service": "service_id" 68 | }) 69 | 70 | class ExploitSubmitForm(BaseModel): 71 | start_time: AwareDatetime|None = None 72 | end_time: AwareDatetime|None = None 73 | status: AttackExecutionStatus 74 | output: bytes|None = None 75 | executed_by: UnHashedClientID|None = None 76 | source_hash: str|None = None 77 | target: TeamID|None = None 78 | executed_by_group: AttackGroupID|None = None 79 | flags: list[str] 80 | 81 | def db_data(self): 82 | return json_like(self, convert_keys={ 83 | "executed_by": "executed_by_id", 84 | "target": "target_id", 85 | "executed_by_group": "executed_by_group_id" 86 | }, mode="python") 87 | 88 | class ExploitSourcePushForm(BaseModel): 89 | message: str|None 90 | 91 | class ManualSubmitForm(BaseModel): 92 | flags: list[str]|None = None 93 | output: bytes|None = None -------------------------------------------------------------------------------- /client/exploitfarm/models/flags.py: -------------------------------------------------------------------------------- 1 | from exploitfarm.models.enums import FlagStatus, AttackExecutionStatus 2 | from pydantic import AwareDatetime 3 | from exploitfarm.models.dbtypes import FlagID, AttackExecutionID, TeamID, ExploitID, ClientID, ExploitSourceID 4 | from exploitfarm.models.dbtypes import AttackGroupID 5 | from pydantic import BaseModel, Field, AliasChoices 6 | 7 | class FlagDTOSmall(BaseModel): 8 | id: FlagID 9 | flag: str 10 | status: FlagStatus 11 | 12 | class AttackExecutionDTO(BaseModel): 13 | id: AttackExecutionID 14 | start_time: AwareDatetime|None = None 15 | end_time: AwareDatetime|None = None 16 | status: AttackExecutionStatus 17 | output: str|None 18 | received_at: AwareDatetime 19 | target: TeamID|None = Field(None, validation_alias=AliasChoices('target_id')) 20 | exploit: ExploitID|None = Field(None, validation_alias=AliasChoices('exploit_id')) 21 | executed_by: ClientID|None = Field(None, validation_alias=AliasChoices('executed_by_id')) 22 | executed_by_group: AttackGroupID|None = Field(None, validation_alias=AliasChoices('executed_by_group_id')) 23 | flags: list[FlagDTOSmall] 24 | exploit_source: ExploitSourceID|None = Field(None, validation_alias=AliasChoices('exploit_source_id')) 25 | 26 | class FlagDTOAttackDetails(BaseModel): 27 | id: AttackExecutionID 28 | start_time: AwareDatetime|None = None 29 | end_time: AwareDatetime|None = None 30 | status: AttackExecutionStatus 31 | received_at: AwareDatetime 32 | target: TeamID|None = Field(None, validation_alias=AliasChoices('target_id')) 33 | exploit: ExploitID|None = Field(None, validation_alias=AliasChoices('exploit_id')) 34 | executed_by: ClientID|None = Field(None, validation_alias=AliasChoices('executed_by_id')) 35 | executed_by_group: AttackGroupID|None = Field(None, validation_alias=AliasChoices('executed_by_group_id')) 36 | exploit_source: ExploitSourceID|None = Field(None, validation_alias=AliasChoices('exploit_source_id')) 37 | 38 | class FlagDTO(BaseModel): 39 | 40 | id: FlagID 41 | flag: str 42 | status: FlagStatus 43 | last_submission_at: AwareDatetime|None 44 | status_text: str|None = None 45 | submit_attempts: int = 0 46 | attack: FlagDTOAttackDetails 47 | 48 | class TickStats(BaseModel): 49 | tick: int 50 | start_time: AwareDatetime 51 | end_time: AwareDatetime 52 | globals: dict = {} 53 | exploits: dict = {} 54 | services: dict = {} 55 | teams: dict = {} 56 | clients: dict = {} 57 | 58 | class FlagStats(BaseModel): 59 | ticks: list[TickStats] = [] 60 | globals: dict = {} 61 | -------------------------------------------------------------------------------- /client/exploitfarm/models/groups.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, AwareDatetime, Field, AliasChoices 2 | from exploitfarm.models.dbtypes import AttackGroupID, UnHashedClientID, ExploitID, ExploitSourceID, ClientID 3 | from exploitfarm.utils import json_like 4 | from exploitfarm.models.enums import GroupStatus, GroupEventRequestType, GroupEventResponseType 5 | from typing import Literal 6 | from pydantic import PositiveInt 7 | from uuid import UUID 8 | from pydantic import field_validator 9 | 10 | class AddGroupForm(BaseModel): 11 | name: str 12 | exploit: str 13 | created_by: UnHashedClientID 14 | commit: ExploitSourceID|Literal["latest"] = "latest" 15 | def db_data(self): 16 | d = json_like(self, convert_keys={ 17 | "created_by": "created_by_id", 18 | "exploit": "exploit_id", 19 | "commit": "commit_id" 20 | }) 21 | if d["commit_id"] == "latest": 22 | d["commit_id"] = None 23 | return d 24 | 25 | class EditGroupForm(BaseModel): 26 | name: str 27 | 28 | class GroupDTO(BaseModel): 29 | id: AttackGroupID 30 | name: str 31 | members: list[str] = [] 32 | exploit: ExploitID = Field(validation_alias=AliasChoices('exploit_id')) 33 | last_attack_at: AwareDatetime|None = None 34 | status: GroupStatus = GroupStatus.inactive 35 | commit: UUID|Literal["latest"] = Field("latest", validation_alias=AliasChoices('commit_id')) 36 | 37 | class Config: 38 | validate_assignment = True 39 | 40 | @field_validator('commit', mode='before') 41 | def commit_none_default(cls, commit): 42 | return commit or 'latest' 43 | 44 | class __JoinRequest(BaseModel): 45 | group_id: AttackGroupID 46 | queue_size: PositiveInt 47 | 48 | class JoinRequest(__JoinRequest): 49 | client: UnHashedClientID 50 | 51 | class JoinRequestClient(__JoinRequest): 52 | client: UUID 53 | 54 | class JoinRequestResponse(BaseModel): 55 | timeout: int 56 | deadline: AwareDatetime 57 | running: bool 58 | 59 | class GroupRequestEvent(BaseModel): 60 | group_id: AttackGroupID 61 | event: GroupEventRequestType 62 | data: dict|None = None 63 | 64 | class __GroupResponseEvent(BaseModel): 65 | group_id: AttackGroupID 66 | event: GroupEventResponseType 67 | data: dict|None = None 68 | 69 | class GroupResponseEventClient(__GroupResponseEvent): 70 | client: ClientID 71 | 72 | class GroupResponseEvent(__GroupResponseEvent): 73 | client: UnHashedClientID 74 | -------------------------------------------------------------------------------- /client/exploitfarm/models/response.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import TypeVar, Generic, Any 3 | from exploitfarm.models.enums import ResponseStatus, MessageStatusLevel 4 | 5 | ResponseType = TypeVar('ResponseType', bound=Any) 6 | 7 | class MessageResponseInvalidError(Exception): 8 | def __init__(self, message, response=None): 9 | self.message = message 10 | self.response = response 11 | 12 | def __str__(self): 13 | return self.message 14 | 15 | class MessageResponse(BaseModel, Generic[ResponseType]): 16 | status: ResponseStatus = ResponseStatus.OK 17 | message: str|None = None 18 | response: ResponseType|None = None 19 | 20 | class MessageInfo(BaseModel): 21 | level: MessageStatusLevel = MessageStatusLevel.warning 22 | title: str 23 | message: str 24 | -------------------------------------------------------------------------------- /client/exploitfarm/models/service.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from exploitfarm.models.dbtypes import ServiceID 3 | 4 | ###-- Services Models --### 5 | 6 | class ServiceDTO(BaseModel): 7 | id: ServiceID 8 | name: str 9 | 10 | class ServiceAddForm(BaseModel): 11 | name: str 12 | 13 | class ServiceEditForm(BaseModel): 14 | name: str|None = None -------------------------------------------------------------------------------- /client/exploitfarm/models/submitter.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | from pydantic import BaseModel, Field, AwareDatetime 3 | from exploitfarm.models.dbtypes import SubmitterID 4 | 5 | ###-- Submitter Models --### 6 | 7 | SubmitterKargs = Dict[str, Dict[str, Any]] 8 | 9 | class SubmitterDTO(BaseModel): 10 | id: SubmitterID 11 | name: str 12 | code: str 13 | kargs: SubmitterKargs = {} 14 | created_at: AwareDatetime 15 | 16 | class SubmitterAddForm(BaseModel): 17 | name: str = Field("", min_length=1) 18 | code: str 19 | kargs: Dict[str, Any]|None = None 20 | 21 | class SubmitterInfoForm(BaseModel): 22 | code: str 23 | 24 | class SubmitterEditForm(BaseModel): 25 | code: str|None = None 26 | name: str|None = Field(None, min_length=1) 27 | kargs: Dict[str, Any]|None = None -------------------------------------------------------------------------------- /client/exploitfarm/models/teams.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, AwareDatetime 2 | from exploitfarm.models.dbtypes import TeamID 3 | 4 | ###-- Teams Models --### 5 | 6 | class TeamDTO(BaseModel): 7 | id: TeamID 8 | name: str|None 9 | short_name: str|None 10 | host: str 11 | created_at: AwareDatetime 12 | 13 | class TeamAddForm(BaseModel): 14 | name: str|None = None 15 | short_name: str|None = None 16 | host: str 17 | 18 | class TeamEditForm(BaseModel): 19 | id: TeamID 20 | name: str|None = None 21 | short_name: str|None = None 22 | host: str|None = None 23 | -------------------------------------------------------------------------------- /client/exploitfarm/tui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/client/exploitfarm/tui/__init__.py -------------------------------------------------------------------------------- /client/exploitfarm/tui/exploitdownload.py: -------------------------------------------------------------------------------- 1 | from textual.app import App, ComposeResult 2 | from textual.widgets import Button, Header, Label 3 | from textual import on 4 | from textual.widgets import Footer, OptionList 5 | from textual.containers import Horizontal 6 | from exploitfarm.utils.config import ClientConfig 7 | from rich.markup import escape 8 | from dateutil import parser as dtparser 9 | 10 | class ExploitDownload(App[None|tuple[str, str]]): 11 | 12 | def __init__(self, config: ClientConfig): 13 | super().__init__() 14 | self.config = config 15 | self.exploits = [exp for exp in self.config.reqs.exploits()] 16 | self.services = self.config.status["services"] 17 | self.selected_exploit = None 18 | self.selected_exploit_sources = None 19 | 20 | BINDINGS = [ 21 | ("ctrl+c", "cancel()", "Cancel") 22 | ] 23 | 24 | CSS = """ 25 | #selector { 26 | margin: 2 5 0 5; 27 | } 28 | .button-box { 29 | margin-top: 1; 30 | min-height: 4; 31 | align: center middle; 32 | } 33 | #selector { 34 | height: 80%; 35 | align: center middle; 36 | } 37 | .max-width { 38 | width: 90%; 39 | } 40 | .center { 41 | align: center middle; 42 | } 43 | .buttons{ 44 | margin: 0 2; 45 | } 46 | #info_text { 47 | text-align: center; 48 | } 49 | """ 50 | 51 | def compose(self) -> ComposeResult: 52 | yield Header("xFarm - Exploit Download") 53 | yield OptionList( 54 | id="selector", 55 | classes="max-height" 56 | ) 57 | yield Horizontal( 58 | Label("Loading exploits...", id="info_text", classes="max-width center"), 59 | classes="max-width center" 60 | ) 61 | yield Horizontal( 62 | Button("Select", id="select", variant="success", classes="buttons"), 63 | Button("Cancel", id="cancel", variant="error", classes="buttons"), 64 | classes="max-width button-box" 65 | ) 66 | yield Footer() 67 | 68 | def service_name(self, service_id: str) -> str: 69 | for service in self.services: 70 | if service["id"] == service_id: 71 | return service["name"] 72 | return "Unknown" 73 | 74 | def on_mount(self) -> None: 75 | option_list = self.query_one("#selector", OptionList) 76 | info_text = self.query_one("#info_text", Label) 77 | option_list.clear_options() 78 | if len(self.exploits) == 0: 79 | info_text.update("No exploits found.") 80 | else: 81 | info_text.update("Select an exploit to download") 82 | option_list.add_options([ 83 | f"\n [bold][undeline][blue]{escape(exp['name'])}[/] " 84 | f"\\[{escape(self.service_name(exp['service']))}][/][/] ([gray62]{escape(exp['id'])}[/])\n" 85 | f" [bold]language:[/] [gray62]{escape(exp['language'])}[/]" 86 | f" [bold]status:[/] [gray62]{escape(exp['status'])}[/]\n" 87 | for exp in self.exploits 88 | ]) 89 | 90 | def action_cancel(self): 91 | self.cancel() 92 | 93 | 94 | @on(Button.Pressed, "#select") 95 | def select(self): 96 | self.query_one("#selector", OptionList).action_select() 97 | 98 | 99 | @on(OptionList.OptionSelected, "#selector") 100 | def exploit_selected_event(self, message: OptionList.OptionSelected) -> None: 101 | option_list = self.query_one("#selector", OptionList) 102 | info_text = self.query_one("#info_text", Label) 103 | if self.selected_exploit is None: 104 | self.selected_exploit = self.exploits[message.option_index] 105 | option_list.clear_options() 106 | info_text.update( 107 | f"Selected exploit: [bold][undeline]{escape(self.selected_exploit['name'])} " 108 | f"\\[{escape(self.service_name(self.selected_exploit['service']))}]" 109 | f" ([gray62]{self.selected_exploit['id']}[/])" 110 | ) 111 | self.selected_exploit_sources = self.config.reqs.exploit_source_log(self.selected_exploit["id"]) 112 | latest = self.selected_exploit_sources[0]['id'] if len(self.selected_exploit_sources) > 0 else None 113 | option_list.add_option("\n [bold undeline]..[/]\n") 114 | option_list.add_options([ 115 | f"\n [bold u]{escape(source['message'] if source['message'] else 'There is no commit message')}[/] (hash: [gray62]{source['hash']}[/])" 116 | f"\n Pushed at: [blue]{escape(dtparser.parse(source['pushed_at']).astimezone().strftime('%Y-%m-%d %H:%M:%S'))}[/] {'([bold yellow]LATEST[/])' if source['id'] == latest else ''}\n" 117 | for source in self.selected_exploit_sources]) 118 | else: 119 | if message.option_index == 0: 120 | self.selected_exploit = None 121 | self.selected_exploit_sources = None 122 | self.on_mount() 123 | else: 124 | self.exit((self.selected_exploit["id"], self.selected_exploit_sources[message.option_index - 1]["id"])) 125 | 126 | 127 | @on(Button.Pressed, "#cancel") 128 | def cancel(self): 129 | self.exit(None) 130 | 131 | -------------------------------------------------------------------------------- /client/exploitfarm/tui/group_prejoin.py: -------------------------------------------------------------------------------- 1 | from textual.app import App, ComposeResult 2 | from textual.widgets import Button, Header, Label 3 | from textual import on 4 | from textual.widgets import Footer, Input 5 | from textual.containers import Horizontal 6 | from exploitfarm.utils.config import ClientConfig 7 | from rich.markup import escape 8 | import multiprocessing 9 | from textual.validation import Number 10 | 11 | class GroupPreJoin(App[None|int]): 12 | 13 | def __init__(self, config: ClientConfig, group: dict, queue_num: int|None = None): 14 | super().__init__() 15 | self.queue_num = None if queue_num is None else str(queue_num) 16 | self.group = group 17 | self.config = config 18 | self.options = [ 19 | multiprocessing.cpu_count(), 20 | multiprocessing.cpu_count() * 3, 21 | multiprocessing.cpu_count() * 6, 22 | multiprocessing.cpu_count() * 10, 23 | ] 24 | 25 | BINDINGS = [ 26 | ("ctrl+c", "cancel()", "Cancel") 27 | ] 28 | 29 | CSS = """ 30 | .queue-tab-button{ 31 | margin: 0 4; 32 | } 33 | .queue-tab{ 34 | margin: 0 2; 35 | width: auto; 36 | } 37 | .name-label{ 38 | margin: 2; 39 | margin-left: 4; 40 | } 41 | #queue-input{ 42 | margin: 1 4; 43 | } 44 | """ 45 | 46 | def compose(self) -> ComposeResult: 47 | yield Header("xFarm - Pre join attack group") 48 | 49 | yield Label(f"[bold]Group[/bold]: [yellow bold]{escape(self.group['name'])}[/]", classes="name-label") 50 | 51 | 52 | yield Label("[bold]Thread to use in the attack group:[/]", classes="name-label") 53 | yield Input( 54 | placeholder="20", 55 | value=self.queue_num, 56 | validators=[Number(minimum=1)], 57 | type="integer", 58 | id="queue-input", 59 | 60 | ) 61 | yield Horizontal( 62 | *[Button(str(option), id=f"option-{i}", variant="default", classes="queue-tab-button") for i, option in enumerate(self.options)], 63 | classes="queue-tab" 64 | ) 65 | yield Label("", classes="name-label", id="error-label") 66 | yield Horizontal( 67 | Button("Join", id="join", variant="success", classes="queue-tab"), 68 | Button("Cancel", id="cancel", variant="error", classes="queue-tab"), 69 | classes="queue-tab" 70 | ) 71 | yield Footer() 72 | 73 | @on(Button.Pressed, "#option-0") 74 | def option_0(self): 75 | self.queue_num = str(self.options[0]) 76 | self.reset_value_queue() 77 | 78 | @on(Button.Pressed, "#option-1") 79 | def option_1(self): 80 | self.queue_num = str(self.options[1]) 81 | self.reset_value_queue() 82 | 83 | @on(Button.Pressed, "#option-2") 84 | def option_2(self): 85 | self.queue_num = str(self.options[2]) 86 | self.reset_value_queue() 87 | 88 | @on(Button.Pressed, "#option-3") 89 | def option_3(self): 90 | self.queue_num = str(self.options[3]) 91 | self.reset_value_queue() 92 | 93 | @on(Input.Changed, "#queue-input") 94 | def queue_input(self, inchg: Input.Changed): 95 | self.queue_num = inchg.value 96 | 97 | def reset_value_queue(self): 98 | self.query_one("#queue-input", Input).value = self.queue_num 99 | 100 | def action_cancel(self): 101 | self.cancel() 102 | 103 | @on(Button.Pressed, "#join") 104 | def join(self): 105 | try: 106 | res = int(self.queue_num) 107 | if res < 1: 108 | raise ValueError("The number of threads must be greater than 0") 109 | self.exit(res) 110 | except (ValueError, TypeError): 111 | self.query_one("#error-label", Label).update("[bold red]The number of threads must be greater than 0[/]") 112 | 113 | @on(Button.Pressed, "#cancel") 114 | def cancel(self): 115 | self.exit(None) 116 | 117 | -------------------------------------------------------------------------------- /client/exploitfarm/tui/group_select.py: -------------------------------------------------------------------------------- 1 | from textual.app import App, ComposeResult 2 | from textual.widgets import Button, Header, Label 3 | from textual import on 4 | from textual.widgets import Footer, OptionList 5 | from textual.containers import Horizontal 6 | from exploitfarm.utils.config import ClientConfig 7 | from rich.markup import escape 8 | 9 | class GroupSelect(App[None|dict]): 10 | 11 | def __init__(self, config: ClientConfig, groups: list[dict]|None = None): 12 | super().__init__() 13 | self.config = config 14 | self.exploits = [exp for exp in self.config.reqs.exploits()] 15 | if not groups: 16 | self.groups = [group for group in self.config.reqs.groups()] 17 | else: 18 | self.groups = groups 19 | self.services = self.config.status["services"] 20 | 21 | BINDINGS = [ 22 | ("ctrl+c", "cancel()", "Cancel") 23 | ] 24 | 25 | CSS = """ 26 | #selector { 27 | margin: 2 5 0 5; 28 | } 29 | .button-box { 30 | margin-top: 1; 31 | min-height: 4; 32 | align: center middle; 33 | } 34 | #selector { 35 | height: 80%; 36 | align: center middle; 37 | } 38 | .max-width { 39 | width: 90%; 40 | } 41 | .center { 42 | align: center middle; 43 | } 44 | .buttons{ 45 | margin: 0 2; 46 | } 47 | #info_text { 48 | text-align: center; 49 | } 50 | """ 51 | 52 | def compose(self) -> ComposeResult: 53 | yield Header("xFarm - Group Select") 54 | yield OptionList( 55 | id="selector", 56 | classes="max-height" 57 | ) 58 | yield Horizontal( 59 | Label("Loading groups...", id="info_text", classes="max-width center"), 60 | classes="max-width center" 61 | ) 62 | yield Horizontal( 63 | Button("Select", id="select", variant="success", classes="buttons"), 64 | Button("Cancel", id="cancel", variant="error", classes="buttons"), 65 | classes="max-width button-box" 66 | ) 67 | yield Footer() 68 | 69 | def service_name(self, service_id: str) -> str: 70 | for service in self.services: 71 | if service["id"] == service_id: 72 | return service["name"] 73 | return "Unknown" 74 | 75 | def exploit_name(self, exploit_id: str) -> str: 76 | for exploit in self.exploits: 77 | if exploit["id"] == exploit_id: 78 | return exploit["name"] 79 | return "Unknown" 80 | 81 | def explploit_solver(self, exploit_id: str) -> dict: 82 | for exploit in self.exploits: 83 | if exploit["id"] == exploit_id: 84 | return exploit 85 | 86 | def on_mount(self) -> None: 87 | option_list = self.query_one("#selector", OptionList) 88 | info_text = self.query_one("#info_text", Label) 89 | option_list.clear_options() 90 | if len(self.groups) == 0: 91 | info_text.update("No groups found.") 92 | else: 93 | info_text.update("Select a group to join") 94 | option_list.add_options([ 95 | f"\n [bold][undeline][blue]{escape(grp['name'])}[/][/][/] " 96 | f"\\[[bold yellow]{escape(self.exploit_name(grp['exploit']))}[/], service: [bold]{escape(self.service_name(self.explploit_solver(grp['exploit'])['service']))}[/]] (Commit: [gray62]{escape(grp['commit'])}[/])\n" 97 | f" [bold]members:[/] [gray62]{len(grp['members'])} members[/]" 98 | f" [bold]status:[/] [gray62]{escape(grp['status'])}[/]\n" 99 | for grp in self.groups 100 | ]) 101 | 102 | def action_cancel(self): 103 | self.cancel() 104 | 105 | @on(Button.Pressed, "#select") 106 | def select(self): 107 | self.query_one("#selector", OptionList).action_select() 108 | 109 | @on(OptionList.OptionSelected, "#selector") 110 | def group_selected_event(self, message: OptionList.OptionSelected) -> None: 111 | self.exit(self.groups[message.option_index]) 112 | 113 | 114 | @on(Button.Pressed, "#cancel") 115 | def cancel(self): 116 | self.exit(None) 117 | 118 | -------------------------------------------------------------------------------- /client/exploitfarm/tui/shared_attack_group.py: -------------------------------------------------------------------------------- 1 | from textual.app import App, ComposeResult 2 | from textual.widgets import Button, Header 3 | from textual import on 4 | from textual.widgets import Footer 5 | from exploitfarm.utils.config import ClientConfig 6 | from textual.widgets import Input, Label, Checkbox 7 | from textual.containers import Horizontal 8 | from exploitfarm.utils.config import ExploitConfig 9 | from rich.markup import escape 10 | from textual.validation import Length 11 | from exploitfarm.utils.cmd import create_new_attack_group 12 | 13 | class SharedAttackCreateGroup(App[None|dict]): 14 | 15 | def __init__(self, config: ClientConfig, exploit:ExploitConfig, name:str|None = "", use_latest:bool = True): 16 | super().__init__() 17 | self.config = config 18 | self.exploit = exploit 19 | self.sources = self.config.reqs.exploit_source_log(self.exploit.uuid) 20 | if len(self.sources) == 0: 21 | raise ValueError("No sources found") 22 | self.is_latest_commit = self.exploit.hash() == self.sources[0]["hash"] 23 | self.use_latest = self.is_latest_commit and use_latest 24 | self.group_name = name if name else "" 25 | 26 | BINDINGS = [ 27 | ("ctrl+c", "cancel()", "Cancel"), 28 | ("ctrl+s", "create()", "Create attack group"), 29 | ] 30 | 31 | CSS = """ 32 | .button-box { 33 | height: 4; 34 | margin: 2 0; 35 | margin-top: 1; 36 | } 37 | .max-width { 38 | width: 100%; 39 | } 40 | Button { 41 | margin: 1 2; 42 | } 43 | #group_name { 44 | margin: 2; 45 | margin-top: 1; 46 | margin-bottom: 1; 47 | } 48 | .label-group-name { 49 | margin-top: 2; 50 | margin-left: 2; 51 | } 52 | .left-border { 53 | margin-left: 2; 54 | } 55 | """ 56 | 57 | def compose(self) -> ComposeResult: 58 | yield Header("xFarm - Create Attack Group") 59 | yield Label( 60 | "[bold]Attack group name:[/]", 61 | classes="label-group-name" 62 | ) 63 | yield Input( 64 | placeholder="Crypto brute chall 2", 65 | value=self.group_name, 66 | validators=[Length(minimum=1)], 67 | id="group_name", 68 | ) 69 | 70 | yield Label(f"[bold]Exploit name[/]: {escape(self.exploit.name)}", classes="left-border") 71 | yield Label(f"[bold]Exploit commit[/]: {escape(self.exploit.hash())}", classes="left-border") 72 | if not self.is_latest_commit: 73 | yield Label("[bold red]You can't use the latest commit because the current isn't the latest commit! Pull the latest commit first or push this as the latest commit[/]:", classes="left-border") 74 | yield Checkbox("Use Latest Commit", id="use-latest", value=self.use_latest, classes="label-group-name", disabled=not self.is_latest_commit) 75 | yield Horizontal( 76 | Button("Create", id="create", variant="success"), 77 | Button("Cancel", id="cancel", variant="error"), 78 | ) 79 | 80 | yield Footer() 81 | 82 | def action_cancel(self): 83 | self.cancel() 84 | 85 | def action_create(self): 86 | self.create() 87 | 88 | @on(Checkbox.Changed, "#use-latest") 89 | def https_change(self, event: Checkbox.Changed): 90 | self.use_latest = event.value 91 | 92 | @on(Button.Pressed, "#cancel") 93 | def cancel(self): 94 | self.exit(None) 95 | 96 | @on(Button.Pressed, "#create") 97 | def create(self): 98 | group_name = self.query_one("#group_name", Input).value 99 | self.exploit.publish_exploit(self.config) 100 | self.exit(create_new_attack_group(group_name, self.exploit, self.use_latest, self.config)) 101 | 102 | -------------------------------------------------------------------------------- /client/exploitfarm/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | from fasteners import InterProcessLock 4 | import sys 5 | import psutil 6 | import tarfile 7 | import dirhash 8 | import threading 9 | import traceback 10 | from exploitfarm import __version__ 11 | from typing import List 12 | from pydantic import BaseModel 13 | 14 | DEFAULT_SERVER_PORT = 5050 15 | DEV_MODE = __version__ == "0.0.0" 16 | EXCLUDE_FILE_EXPLOIT_TAR = [".flag_queue.json"] 17 | XPLOIT_DEBUG = os.getenv("XPLOIT_DEBUG", "0") in ["1", "true", "True"] 18 | 19 | 20 | class ExploitFarmClientError(Exception): 21 | pass 22 | 23 | 24 | def parse_address(address: str, default_port: int | None = None) -> tuple[str, int]: 25 | parts = address.split(":") 26 | address = parts[0].strip() 27 | port = int(parts[1]) if len(parts) > 1 else default_port 28 | if port is None: 29 | raise ValueError("Port is required") 30 | return address, port 31 | 32 | 33 | def mem_usage() -> float: 34 | mem_stats = psutil.virtual_memory() 35 | return round((mem_stats.used / mem_stats.total) * 100, 1) 36 | 37 | 38 | def try_tcp_connection( 39 | address: str, timeout: float | None = 3 40 | ) -> tuple[bool, str | None]: 41 | address, port = parse_address(address, default_port=DEFAULT_SERVER_PORT) 42 | s = socket.socket() 43 | try: 44 | s.settimeout(timeout) 45 | s.connect((address, port)) 46 | except Exception as e: 47 | return False, f"Connection to {address}:{port} Failed: {e}" 48 | finally: 49 | s.close() 50 | return True, None 51 | 52 | 53 | def create_lock(name: str) -> InterProcessLock: 54 | file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), name) 55 | base_path = os.path.dirname(file_path) 56 | if not os.path.exists(base_path): 57 | os.makedirs(base_path) 58 | return InterProcessLock(file_path) 59 | 60 | 61 | def restart_program(): 62 | """Restarts the current program, with file objects and descriptors 63 | cleanup 64 | """ 65 | 66 | try: 67 | p = psutil.Process(os.getpid()) 68 | for handler in p.get_open_files() + p.connections(): 69 | os.close(handler.fd) 70 | except Exception: 71 | pass 72 | 73 | python = sys.executable 74 | os.execl(python, python, *sys.argv) 75 | 76 | 77 | def make_tarfile(output_filename, source_dir): 78 | with tarfile.open(output_filename, "w:gz") as tar: 79 | tar.add(source_dir, arcname=os.path.basename(source_dir)) 80 | 81 | 82 | def calc_hash(path) -> str: 83 | return dirhash.dirhash(path, "sha256", ignore=EXCLUDE_FILE_EXPLOIT_TAR) 84 | 85 | 86 | def exploit_tar_filter(tarinfo): 87 | if os.path.basename(tarinfo.name) in EXCLUDE_FILE_EXPLOIT_TAR: 88 | return None 89 | else: 90 | return tarinfo 91 | 92 | 93 | def clear_exploit_folder(path: str): 94 | for root, dirs, files in os.walk(path): 95 | for file in files: 96 | if file not in EXCLUDE_FILE_EXPLOIT_TAR: 97 | os.remove(os.path.join(root, file)) 98 | for dir in dirs: 99 | os.rmdir(os.path.join(root, dir)) 100 | 101 | 102 | def dumpstacks(): 103 | if not XPLOIT_DEBUG: 104 | return 105 | id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) 106 | code = [] 107 | for threadId, stack in sys._current_frames().items(): 108 | code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""), threadId)) 109 | for filename, lineno, name, line in traceback.extract_stack(stack): 110 | code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) 111 | if line: 112 | code.append(" %s" % (line.strip())) 113 | with open("stacktrace.log", "w") as f: 114 | f.write("\n".join(code)) 115 | 116 | 117 | def _json_like( 118 | obj: BaseModel, 119 | unset=False, 120 | convert_keys: dict[str, str] | None = None, 121 | exclude: list[str] | None = None, 122 | mode: str = "json", 123 | ): 124 | res = obj.model_dump(mode=mode, exclude_unset=not unset) 125 | if convert_keys: 126 | for from_k, to_k in convert_keys.items(): 127 | if from_k in res: 128 | res[to_k] = res.pop(from_k) 129 | if exclude: 130 | for ele in exclude: 131 | if ele in res: 132 | del res[ele] 133 | return res 134 | 135 | 136 | def json_like( 137 | obj: BaseModel | List[BaseModel], 138 | unset=False, 139 | convert_keys: dict[str, str] = None, 140 | exclude: list[str] = None, 141 | mode: str = "json", 142 | ) -> dict: 143 | if isinstance(obj, list): 144 | return [ 145 | _json_like( 146 | ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode 147 | ) 148 | for ele in obj 149 | ] 150 | return _json_like( 151 | obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode 152 | ) 153 | -------------------------------------------------------------------------------- /client/exploitfarm/utils/cmd.py: -------------------------------------------------------------------------------- 1 | from exploitfarm.utils.config import ClientConfig, ExploitConfig 2 | 3 | 4 | def create_new_attack_group( 5 | group_name: str, exploit: ExploitConfig, use_latest: bool, config: ClientConfig 6 | ) -> dict | None: 7 | if not use_latest: 8 | commit = None 9 | commit_hash = exploit.hash() 10 | commit_history: list = config.reqs.exploit_source_log(exploit.uuid) 11 | for ele in commit_history: 12 | if ele["hash"] == commit_hash: 13 | commit = ele["id"] 14 | break 15 | else: 16 | commit = "latest" 17 | 18 | if not commit: 19 | return None 20 | else: 21 | return config.reqs.new_group( 22 | { 23 | "name": group_name, 24 | "exploit": exploit.uuid, 25 | "created_by": config.client_id, 26 | "commit": commit, 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /client/exploitfarm/utils/exploit.py: -------------------------------------------------------------------------------- 1 | from exploitfarm.models.enums import Language, get_lang 2 | 3 | PYTHON_DEFAULT_CONTENT = """ 4 | from exploitfarm import * 5 | 6 | host = get_host() 7 | 8 | print(f"Hello {host}! This text should contain a lot of flags!") 9 | """ 10 | 11 | 12 | def get_default_file(lang: Language | str) -> str: 13 | lang = get_lang(lang) 14 | shebang = ( 15 | f"#!/usr/bin/env {get_interpreter(lang)}\n\n" if get_interpreter(lang) else "\n" 16 | ) 17 | match lang: 18 | case Language.python: 19 | return shebang + PYTHON_DEFAULT_CONTENT 20 | case _: 21 | return f"{shebang}Your code here\nPlease print the flags to stdout, " 22 | "exploitfarm will filter flags automatically\n" 23 | 24 | 25 | def get_interpreter(lang: Language) -> str | None: 26 | match lang: 27 | case Language.python: 28 | return "python3" 29 | case Language.java: 30 | return "java" 31 | case Language.javascript: 32 | return "node" 33 | case Language.typescript: 34 | return "npm run" 35 | case Language.csharp: 36 | return None 37 | case Language.cpp: 38 | return None 39 | case Language.php: 40 | return "php" 41 | case Language.r: 42 | return "Rscript" 43 | case Language.kotlin: 44 | return "java -jar" 45 | case Language.go: 46 | return None 47 | case Language.ruby: 48 | return "ruby" 49 | case Language.rust: 50 | return None 51 | case Language.lua: 52 | return "lua" 53 | case Language.dart: 54 | return "dart run" 55 | case Language.perl: 56 | return "perl" 57 | case Language.haskell: 58 | return None 59 | case Language.other: 60 | return None 61 | 62 | 63 | def get_filename(lang: Language) -> str: 64 | match lang: 65 | case Language.python: 66 | return "main.py" 67 | case Language.java: 68 | return "main.java" 69 | case Language.javascript: 70 | return "main.js" 71 | case Language.typescript: 72 | return "main.ts" 73 | case Language.csharp: 74 | return "main.cs" 75 | case Language.cpp: 76 | return "main.cpp" 77 | case Language.php: 78 | return "main.php" 79 | case Language.r: 80 | return "main.r" 81 | case Language.kotlin: 82 | return "main.kt" 83 | case Language.go: 84 | return "main.go" 85 | case Language.ruby: 86 | return "main.rb" 87 | case Language.rust: 88 | return "main.rs" 89 | case Language.lua: 90 | return "main.lua" 91 | case Language.dart: 92 | return "main.dart" 93 | case Language.perl: 94 | return "main.pl" 95 | case Language.haskell: 96 | return "main.hs" 97 | case Language.other: 98 | return "main" 99 | -------------------------------------------------------------------------------- /client/exploitfarm/utils/windows_close_fix.py: -------------------------------------------------------------------------------- 1 | import os 2 | from contextlib import contextmanager 3 | 4 | os_windows = os.name == "nt" 5 | os_unix = os.name == "posix" 6 | 7 | 8 | class CtrlCWinManager: 9 | def __init__(self, trigger: callable): 10 | if os_windows: 11 | # By default, Ctrl+C does not work on Windows if we spawn subprocesses. 12 | # Here we fix that using WinApi. See https://stackoverflow.com/a/43095532 13 | 14 | import signal 15 | import ctypes 16 | from ctypes import wintypes 17 | 18 | self.kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) 19 | 20 | # BOOL WINAPI HandlerRoutine( 21 | # _In_ DWORD dwCtrlType 22 | # ); 23 | PHANDLER_ROUTINE = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD) 24 | 25 | self.win_ignore_ctrl_c = PHANDLER_ROUTINE() # = NULL 26 | 27 | def _errcheck_bool(result, _, args): 28 | if not result: 29 | raise ctypes.WinError(ctypes.get_last_error()) 30 | return args 31 | 32 | # BOOL WINAPI SetConsoleCtrlHandler( 33 | # _In_opt_ PHANDLER_ROUTINE HandlerRoutine, 34 | # _In_ BOOL Add 35 | # ); 36 | self.kernel32.SetConsoleCtrlHandler.errcheck = _errcheck_bool 37 | self.kernel32.SetConsoleCtrlHandler.argtypes = ( 38 | PHANDLER_ROUTINE, 39 | wintypes.BOOL, 40 | ) 41 | 42 | @PHANDLER_ROUTINE 43 | def win_ctrl_handler(dwCtrlType): 44 | if dwCtrlType == signal.CTRL_C_EVENT: 45 | self.kernel32.SetConsoleCtrlHandler(self.win_ignore_ctrl_c, True) 46 | trigger() 47 | return False 48 | 49 | self.kernel32.SetConsoleCtrlHandler(win_ctrl_handler, True) 50 | 51 | @contextmanager 52 | def skip_ctrl_c_handling(self): 53 | try: 54 | if os_windows: 55 | self.kernel32.SetConsoleCtrlHandler(self.win_ignore_ctrl_c, True) 56 | yield 57 | finally: 58 | if os_windows: 59 | self.kernel32.SetConsoleCtrlHandler(self.win_ignore_ctrl_c, False) 60 | -------------------------------------------------------------------------------- /client/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.32.3 2 | urllib3 >= 1.25.3, < 2.1.0 3 | python-dateutil==2.9.0.post0 4 | pydantic>=2 5 | typing-extensions>=4.7.1 6 | fasteners==0.19 7 | toml==0.10.2 8 | psutil==7.0.0 9 | dirhash==0.5.0 10 | requests-toolbelt==1.0.0 11 | python-socketio[client]==5.12.1 12 | textual>=3.0.0 13 | xfarm 14 | orjson 15 | typer 16 | -------------------------------------------------------------------------------- /client/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | with open("requirements.txt", "r", encoding="utf-8") as f: 7 | required = [ 8 | ele.strip() 9 | for ele in f.read().splitlines() 10 | if not ele.strip().startswith("#") and ele.strip() != "" 11 | ] 12 | 13 | VERSION = "{{VERSION_PLACEHOLDER}}" 14 | 15 | setuptools.setup( 16 | name="exploitfarm", 17 | version=VERSION 18 | if "{" not in VERSION 19 | else "0.0.0", # uv pip install -U . --no-cache-dir for testing 20 | author="Pwnzer0tt1", 21 | author_email="pwnzer0tt1@poliba.it", 22 | scripts=["xfarm"], 23 | py_modules=["exploitfarm"], 24 | install_requires=required, 25 | include_package_data=True, 26 | description="Exploit Farm client", 27 | long_description=long_description, 28 | long_description_content_type="text/markdown", 29 | url="https://github.com/pwnzer0tt1/exploitfarm", 30 | packages=setuptools.find_packages(), 31 | classifiers=[ 32 | "Programming Language :: Python :: 3", 33 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 34 | "Operating System :: OS Independent", 35 | ], 36 | python_requires=">=3.10", 37 | ) 38 | -------------------------------------------------------------------------------- /client/xfarm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from exploitfarm.xfarm import run 4 | 5 | if __name__ == "__main__": 6 | run() 7 | -------------------------------------------------------------------------------- /client/xfarm-pip/README.md: -------------------------------------------------------------------------------- 1 | # Exploit Farm python library 2 | 3 | Alias of 'exploitfarm' libaray -------------------------------------------------------------------------------- /client/xfarm-pip/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="xfarm", 8 | version="{{VERSION_PLACEHOLDER}}", 9 | author="Pwnzer0tt1", 10 | author_email="pwnzer0tt1@poliba.it", 11 | py_modules=["xfarm"], 12 | install_requires=["exploitfarm=={{VERSION_PLACEHOLDER}}"], 13 | include_package_data=True, 14 | description="Exploit Farm client", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/pwnzer0tt1/exploitfarm", 18 | packages=setuptools.find_packages(), 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 22 | "Operating System :: OS Independent", 23 | ], 24 | python_requires='>=3.10', 25 | ) 26 | -------------------------------------------------------------------------------- /client/xfarm-pip/xfarm/__init__.py: -------------------------------------------------------------------------------- 1 | from exploitfarm import * 2 | -------------------------------------------------------------------------------- /client/xfarm-pip/xfarm/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from exploitfarm.xfarm import run 4 | 5 | if __name__ == "__main__": 6 | run() 7 | -------------------------------------------------------------------------------- /db.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | database: #DB for testing 3 | image: postgres:17 4 | restart: unless-stopped 5 | environment: 6 | POSTGRES_USER: exploitfarm 7 | POSTGRES_PASSWORD: exploitfarm 8 | POSTGRES_DB: exploitfarm 9 | ports: 10 | - 5432:5432 11 | volumes: 12 | - ./db-data:/var/lib/postgresql/data 13 | redis: #Redis for testing 14 | image: redis:7 15 | restart: unless-stopped 16 | ports: 17 | - 6379:6379 18 | 19 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | documentation.* 2 | !documentation.tex 3 | !documentation.pdf 4 | .texpadtmp -------------------------------------------------------------------------------- /docs/Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/Architecture.png -------------------------------------------------------------------------------- /docs/Business Model Canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/Business Model Canvas.png -------------------------------------------------------------------------------- /docs/ExploitFarmDB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/ExploitFarmDB.png -------------------------------------------------------------------------------- /docs/RoadToExploitFarm1.0.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/RoadToExploitFarm1.0.0.pdf -------------------------------------------------------------------------------- /docs/SharedAttacksDesign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/SharedAttacksDesign.png -------------------------------------------------------------------------------- /docs/architecture.puml: -------------------------------------------------------------------------------- 1 | @startuml architecture 2 | 3 | 4 | node Server as server 5 | 6 | 7 | database Databases as databases { 8 | database "PostgreSQL" as db 9 | database "Redis" as redis 10 | } 11 | 12 | storage "Clients (0..N)" { 13 | node "XFarm Client" as xfarm 14 | node "Frontend" as frontend 15 | } 16 | 17 | 18 | server <-> db 19 | server <-> redis 20 | 21 | server --> xfarm 22 | server --> frontend 23 | 24 | @enduml -------------------------------------------------------------------------------- /docs/attack_assign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/attack_assign.png -------------------------------------------------------------------------------- /docs/attack_assign.puml: -------------------------------------------------------------------------------- 1 | @startuml attack_assign 2 | start 3 | 4 | repeat 5 | 6 | repeat 7 | :assign attack from queue\n(using priorities logics); 8 | 9 | repeat while (Wait for attacks to finish) is (attack finished) not (no attacks left) 10 | 11 | :wait for next tick; 12 | 13 | @enduml 14 | 15 | -------------------------------------------------------------------------------- /docs/attack_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/attack_sequence.png -------------------------------------------------------------------------------- /docs/attack_sequence.puml: -------------------------------------------------------------------------------- 1 | @startuml attack_sequence 2 | 3 | !theme sketchy-outline 4 | !pragma teoz true 5 | 6 | actor Client 7 | participant Server 8 | database DB 9 | Client -> Server: Get the configuration 10 | Server <-- DB: Sync configuration 11 | Server --> Client: Send the configuration 12 | Client --> Server: Create service (if not exists) 13 | Server --> DB: Save the service 14 | Client -> Server: Create the exploit 15 | Server -> DB: Save the exploit and update timestamp 16 | group Attack Loop 17 | Client <--> Server: (socket.io) get configuration 18 | & Server <--> DB: Sync configuration 19 | Client -> Server: (loop) Send attack data 20 | Server -> DB: Save attack data / update timestamp 21 | end 22 | 23 | @enduml -------------------------------------------------------------------------------- /docs/backlog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/backlog.png -------------------------------------------------------------------------------- /docs/db.puml: -------------------------------------------------------------------------------- 1 | @startuml er-schema 2 | 3 | hide circle 4 | 5 | 6 | entity "**Env**" as env { 7 | key: string 8 | -- 9 | value: string 10 | } 11 | 12 | entity "**Exploit**" as exploit{ 13 | id : UUID 14 | -- 15 | name: string 16 | language: string [enum] 17 | hash: string 18 | cli: boolean 19 | created_at: date 20 | } 21 | 22 | entity "**Flag**" as flag{ 23 | id: number 24 | -- 25 | flag: string [unique] 26 | status: string [enum] [indexed] 27 | last_submission_at: date [nullable] [indexed] 28 | status_text: string [nullable] 29 | submit_attempts: int 30 | } 31 | 32 | entity "**Attack-Execution**" as attack{ 33 | id: number 34 | -- 35 | start_time: date 36 | end_time: date 37 | status: string [enum] 38 | error: string [nullable] 39 | output: string [compressed, nullable] 40 | received_at: date 41 | } 42 | 43 | entity "**Client**" as client{ 44 | id: string 45 | -- 46 | name: string 47 | address: string 48 | } 49 | 50 | entity "**Service**" as service{ 51 | id : UUID 52 | -- 53 | name: string 54 | } 55 | 56 | entity "**Team**" as team{ 57 | id : number 58 | -- 59 | name: string [optional] 60 | short_name: string [optional] 61 | address: string 62 | created_at: string 63 | } 64 | 65 | entity "**Submitter**" as submitter{ 66 | id: number 67 | -- 68 | name: string 69 | code: string 70 | kargs: array 71 | } 72 | 73 | entity "**Exploit-Source**" as exploit_source{ 74 | hash: UUID 75 | -- 76 | pushed_at: date 77 | os: string 78 | distro: string 79 | arch: string 80 | } 81 | 82 | entity "**Attack-Group**" as attack_group{ 83 | id: UUID 84 | -- 85 | name: string 86 | created_at: date 87 | } 88 | 89 | 90 | service ||--o{ exploit 91 | team ||--o{ attack 92 | client ||--o{ exploit 93 | client ||--o{ attack 94 | exploit ||--o{ attack 95 | attack ||--o{ flag 96 | exploit ||--o{ exploit_source 97 | attack_group ||--o{ attack 98 | exploit ||--o{ attack_group 99 | client }|--o{ attack_group : done using redis 100 | attack }o--o| exploit_source 101 | exploit_source }o--o| client 102 | attack_group }o--o| exploit_source : null means 'latest' 103 | 104 | @enduml -------------------------------------------------------------------------------- /docs/demo-video.md: -------------------------------------------------------------------------------- 1 | https://github.com/user-attachments/assets/5506dcec-e3e3-467e-9148-36290632762b 2 | -------------------------------------------------------------------------------- /docs/documentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/documentation.pdf -------------------------------------------------------------------------------- /docs/exploitfarm-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/exploitfarm-web.png -------------------------------------------------------------------------------- /docs/general_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/general_layout.png -------------------------------------------------------------------------------- /docs/general_use_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/general_use_case.png -------------------------------------------------------------------------------- /docs/general_use_case.puml: -------------------------------------------------------------------------------- 1 | @startuml use-case 2 | 3 | 'left to right direction 4 | skinparam linetype ortho 5 | 6 | package Clients as client { 7 | actor xfarm 8 | actor frontend as front 9 | } 10 | 11 | package Backend { 12 | 13 | package APIs { 14 | (Getting statistics) as get_stats 15 | (Edit configurations) as edit_configs 16 | (Sync configurations) as sync_configs 17 | (Submit flags) as submit_flags 18 | (Manual Flag Submission) as manual_flag 19 | 20 | submit_flags <|-- manual_flag 21 | 22 | 'align in a vertical line 23 | edit_configs -[hidden]down- sync_configs 24 | sync_configs -[hidden]down- get_stats 25 | get_stats -[hidden]down- manual_flag 26 | manual_flag -[hidden]down- submit_flags 27 | } 28 | 29 | package "Attack Features" as AttackFeatures{ 30 | (Start an attack) as start_attack 31 | (Create a new attack group) as create_group 32 | (Join an attack group) as join_group 33 | (Leave an attack group) as leave_group 34 | (Push an exploit) as push_exploit 35 | (Pull an exploit) as pull_exploit 36 | (Create exploit) as create_exploit 37 | (Attack Victims) as attack_victims 38 | (Submit Attack execution) as submit_attack 39 | } 40 | 41 | 42 | 43 | manual_flag ..> submit_attack : <> 44 | 45 | start_attack <|-- create_group 46 | start_attack <|-- join_group 47 | 48 | 'align in a vertical line 49 | start_attack -[hidden]down- create_group 50 | start_attack -[hidden]down- join_group 51 | 'align on the same line 52 | create_group -[hidden]left- join_group 53 | 54 | start_attack ..> sync_configs : <> 55 | start_attack ..> submit_attack : <> 56 | start_attack ..> create_exploit : <> 57 | start_attack ..> attack_victims : <> 58 | 59 | submit_attack ..> submit_flags : <> 60 | create_group ..> push_exploit : <> 61 | join_group ..> pull_exploit : <> 62 | 63 | 'align in a vertical line 64 | create_group -[hidden]down- submit_attack 65 | create_group -[hidden]down- create_exploit 66 | create_group -[hidden]down- attack_victims 67 | create_group -[hidden]down- push_exploit 68 | create_group -[hidden]down- pull_exploit 69 | 70 | create_group -[hidden]down- manual_flag 71 | 72 | } 73 | 74 | package "Attack Defence Network\n(external to exploitfarm)" { 75 | actor Victim as victim 76 | actor "Game Server" as game 77 | } 78 | 79 | ' Client to use cases 80 | 81 | front -> get_stats 82 | front -> edit_configs 83 | xfarm -> AttackFeatures 84 | client -> sync_configs 85 | client -> manual_flag 86 | 87 | ' Server to use cases 88 | attack_victims -> victim 89 | submit_flags -> game 90 | 91 | 92 | @enduml 93 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/logo.png -------------------------------------------------------------------------------- /docs/scheduling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/scheduling.png -------------------------------------------------------------------------------- /docs/scheduling.puml: -------------------------------------------------------------------------------- 1 | @startgantt scheduling 2 | !theme superhero 3 | 14 | 15 | Project starts the 3rd of october 2024 and ends the 20th of december 2024 16 | 17 | [ Requisiti e Progettazione] as [TASK1] starts 2024-10-03 and ends 2024-11-31 18 | note bottom 19 | - Business Canvas 20 | - Sequence Diagrams 21 | - Documentazione (Latex) 22 | - Use Case Diagrams 23 | end note 24 | [TASK1] is colored in DeepSkyBlue 25 | 26 | [ Exploit Source Managment] as [TASK2] starts 2024-10-20 and ends 2024-11-01 27 | [TASK2] is colored in LimeGreen 28 | [ Socket.IO + sqlalchemy integration] as [TASK3] starts 2024-10-31 and ends 2024-11-05 29 | [TASK3] is colored in Gold 30 | [TASK2]->[TASK3] 31 | 32 | [ Exploit Execution Groups Managment + Tests] as [TASK4] starts 2024-11-05 and ends 2024-12-15 33 | [TASK4] is colored in Crimson 34 | [TASK2]->[TASK4] 35 | 36 | [Exploit management completato] as [M1] happens at [TASK2]'s end 37 | 38 | [TASK3] displays on same row as [TASK2] 39 | 40 | @endgantt -------------------------------------------------------------------------------- /docs/shared_attack_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/shared_attack_sequence.png -------------------------------------------------------------------------------- /docs/shared_attack_sequence.puml: -------------------------------------------------------------------------------- 1 | @startuml shared_attack_sequence 2 | 3 | !theme sketchy-outline 4 | !pragma teoz true 5 | 6 | actor "Client i" as Client 7 | participant Server 8 | Client -> Server: Regsitration/Creation of a attack group 9 | Client -> Server: *Other joins* 10 | Client -> Server: Start the attack 11 | group Attack Loop 12 | Client <--> Server: Attack Timeout Sync 13 | Server -> Client: *Assign attack tasks...* 14 | Client -> Server: *Send attack finished...* 15 | Server <--> Client: Join/Leave attack group 16 | Server -> Client: End of attack for this tick 17 | end 18 | 19 | @enduml -------------------------------------------------------------------------------- /docs/xfarm-start-cmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/docs/xfarm-start-cmd.png -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | parserOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | project: ['./tsconfig.json', './tsconfig.node.json'], 21 | tsconfigRootDir: __dirname, 22 | }, 23 | ``` 24 | 25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 28 | -------------------------------------------------------------------------------- /frontend/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/bun.lockb -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Exploit Farm 👾 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "tsapi": "bunx openapi-typescript http://127.0.0.1:5050/openapi.json -o ./src/utils/backend_types.ts" 12 | }, 13 | "dependencies": { 14 | "@mantine/carousel": "^7.12.2", 15 | "@mantine/charts": "^7.12.2", 16 | "@mantine/code-highlight": "^7.12.2", 17 | "@mantine/core": "^7.12.2", 18 | "@mantine/dates": "^7.12.2", 19 | "@mantine/dropzone": "^7.12.2", 20 | "@mantine/form": "^7.12.2", 21 | "@mantine/hooks": "^7.12.2", 22 | "@mantine/modals": "^7.12.2", 23 | "@mantine/notifications": "^7.12.2", 24 | "@mantine/nprogress": "^7.12.2", 25 | "@mantine/spotlight": "^7.12.2", 26 | "@mantine/tiptap": "^7.12.2", 27 | "@monaco-editor/react": "^4.6.0", 28 | "@tabler/icons-react": "^3.17.0", 29 | "@tanstack/react-query": "^5.56.2", 30 | "@tiptap/extension-link": "^2.7.1", 31 | "@tiptap/react": "^2.7.1", 32 | "@tiptap/starter-kit": "^2.7.1", 33 | "@types/react-syntax-highlighter": "^15.5.13", 34 | "dayjs": "^1.11.13", 35 | "embla-carousel-react": "^8.3.0", 36 | "framer-motion": "^11.5.4", 37 | "immer": "^10.1.1", 38 | "react": "^18.3.1", 39 | "react-dom": "^18.3.1", 40 | "react-icons": "^5.3.0", 41 | "react-router": "^6.26.2", 42 | "react-router-dom": "^6.26.2", 43 | "react-syntax-highlighter": "^15.5.0", 44 | "recharts": "^2.12.7", 45 | "sass": "^1.78.0", 46 | "socket.io-client": "^4.8.1", 47 | "url": "^0.11.4", 48 | "use-immer": "^0.9.0", 49 | "zustand": "^4.5.5" 50 | }, 51 | "devDependencies": { 52 | "@types/react": "^18.3.7", 53 | "@types/react-dom": "^18.3.0", 54 | "@typescript-eslint/eslint-plugin": "^6.21.0", 55 | "@typescript-eslint/parser": "^6.21.0", 56 | "@vitejs/plugin-react-swc": "^3.7.0", 57 | "eslint": "^8.57.1", 58 | "eslint-plugin-react-hooks": "^4.6.2", 59 | "eslint-plugin-react-refresh": "^0.4.12", 60 | "postcss": "^8.4.47", 61 | "postcss-preset-mantine": "^1.17.0", 62 | "postcss-simple-vars": "^7.0.1", 63 | "typescript": "^5.6.2", 64 | "vite": "^4.5.5" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-preset-mantine': {}, 4 | 'postcss-simple-vars': { 5 | variables: { 6 | 'mantine-breakpoint-xs': '36em', 7 | 'mantine-breakpoint-sm': '48em', 8 | 'mantine-breakpoint-md': '62em', 9 | 'mantine-breakpoint-lg': '75em', 10 | 'mantine-breakpoint-xl': '88em', 11 | }, 12 | }, 13 | }, 14 | }; -------------------------------------------------------------------------------- /frontend/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/.gitkeep -------------------------------------------------------------------------------- /frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/logo.png -------------------------------------------------------------------------------- /frontend/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwnzer0tt1/exploitfarm/b501957578a5be8fd30b619da77c92802977079f/frontend/public/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import '@mantine/core/styles.css'; 2 | import '@mantine/notifications/styles.css' 3 | import '@mantine/charts/styles.css' 4 | import '@mantine/dates/styles.css'; 5 | import '@mantine/dropzone/styles.css'; 6 | 7 | import { notifications, Notifications } from '@mantine/notifications'; 8 | import { LoadingOverlay, MantineProvider, Title } from '@mantine/core'; 9 | import { LoginProvider } from '@/components/LoginProvider'; 10 | import { Routes, Route, BrowserRouter } from "react-router-dom"; 11 | import { useGlobalStore, useTokenStore } from './utils/stores'; 12 | import { statusQuery } from './utils/queries'; 13 | import { HomePage } from './components/screens/HomePage'; 14 | import { MainLayout } from './components/MainLayout'; 15 | import { useEffect } from 'react'; 16 | import { DEBOUNCED_SOCKET_IO_CHANNELS, socket_io, SOCKET_IO_CHANNELS, sockIoChannelToQueryKeys } from './utils/net'; 17 | import { useQueryClient } from '@tanstack/react-query'; 18 | import { useDebouncedCallback } from '@mantine/hooks'; 19 | 20 | export default function App() { 21 | 22 | const queryClient = useQueryClient() 23 | const { setErrorMessage, loading:loadingStatus } = useGlobalStore() 24 | const { loginToken } = useTokenStore() 25 | 26 | const debouncedCalls = DEBOUNCED_SOCKET_IO_CHANNELS.map((channel) => ( 27 | useDebouncedCallback(() => { 28 | sockIoChannelToQueryKeys(channel).forEach((data) => 29 | queryClient.invalidateQueries({ queryKey: data }) 30 | ) 31 | }, 3000) 32 | )) 33 | 34 | useEffect(() => { 35 | SOCKET_IO_CHANNELS.forEach((channel) => { 36 | socket_io.on(channel, (_data) => { 37 | if (DEBOUNCED_SOCKET_IO_CHANNELS.includes(channel)) { 38 | debouncedCalls[DEBOUNCED_SOCKET_IO_CHANNELS.indexOf(channel)]() 39 | } else { 40 | sockIoChannelToQueryKeys(channel).forEach((data) => 41 | queryClient.invalidateQueries({ queryKey: data }) 42 | ) 43 | } 44 | }) 45 | }) 46 | socket_io.on("connect_error", (err) => { 47 | setErrorMessage({ 48 | title: "BACKEND SEEMS DOWN!", 49 | message: "Can't connect to backend APIs: " + err.message, 50 | color: "red" 51 | }) 52 | }); 53 | 54 | let first_time = true 55 | socket_io.on("connect", () => { 56 | if (socket_io.connected) { 57 | setErrorMessage(null) 58 | if (!first_time) { 59 | queryClient.resetQueries({ queryKey: [] }) 60 | notifications.show({ 61 | id: "connected-backend", 62 | title: "Connected to the backend!", 63 | message: "Successfully connected to the backend!", 64 | color: "blue", 65 | icon: "🚀", 66 | }) 67 | } 68 | } 69 | first_time = false 70 | }); 71 | return () => { 72 | SOCKET_IO_CHANNELS.forEach((channel) => { 73 | socket_io.off(channel) 74 | }) 75 | socket_io.off("connect_error") 76 | } 77 | }, []) 78 | 79 | 80 | useEffect(() => { 81 | socket_io.auth = { token: loginToken } 82 | socket_io.disconnect() 83 | socket_io.connect() 84 | }, [loginToken]) 85 | 86 | const status = statusQuery() 87 | 88 | return ( 89 | 90 | 91 | 92 | 93 | 94 | 95 | } /> 96 | } /> 97 | 404 Not Found} /> 98 | 99 | 100 | 101 | 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /frontend/src/components/LoginProvider.tsx: -------------------------------------------------------------------------------- 1 | import { postFormRequest } from "@/utils/net"; 2 | import { statusQuery } from "@/utils/queries"; 3 | import { useGlobalStore, useTokenStore } from "@/utils/stores"; 4 | import { Box, Button, Container, Group, PasswordInput, Space } from "@mantine/core" 5 | import { useForm } from '@mantine/form'; 6 | import { notifications } from '@mantine/notifications' 7 | import { useEffect } from "react"; 8 | import { SetupScreen } from "@/components/screens/SetupScreen"; 9 | import { WelcomeTitle } from "@/components/elements/WelcomeTitle"; 10 | import { useQueryClient } from "@tanstack/react-query"; 11 | 12 | export const LoginProvider = ({ children }: { children:any }) => { 13 | 14 | const [token, setToken] = useTokenStore((store) => [store.loginToken, store.setToken]) 15 | const [loadingStatus, setLoading] = useGlobalStore((store) => [store.loading, store.setLoader]) 16 | const status = statusQuery() 17 | const queryClient = useQueryClient() 18 | 19 | const form = useForm({ 20 | initialValues: { 21 | password: '', 22 | }, 23 | validate: { 24 | password: (val) => val == ""? "Password is required" : null, 25 | }, 26 | }); 27 | 28 | useEffect(() => { 29 | setLoading(false) 30 | form.reset() 31 | },[token]) 32 | 33 | useEffect(() => { 34 | if (status.isError){ 35 | queryClient.resetQueries({ queryKey: [] }) 36 | } 37 | },[status.isError]) 38 | 39 | if (status.isError){ 40 | return 44 | 45 | 46 | Check if the backend is running and reachable. 47 | A connection will be tried automatically every 5 seconds. 48 | 49 | 50 | } 51 | 52 | if (status.data?.status == "setup"){ 53 | return 54 | } 55 | 56 | if (status.data?.loggined){ 57 | return <>{children} 58 | } 59 | 60 | return 64 | 65 | 66 |
{ 71 | setLoading(true) 72 | postFormRequest("login", {body: {username: "web-user", ...values}}) 73 | .then( (res) => { 74 | if(res.access_token){ 75 | setToken(res.access_token) 76 | status.refetch() 77 | }else{ 78 | notifications.show({ 79 | title: "Unexpected Error", 80 | message: res.detail??res??"Unknown error", 81 | color: "red", 82 | autoClose: 5000 83 | }) 84 | } 85 | }) 86 | .catch( (err) => { 87 | notifications.show({ 88 | title: "Something went wrong!", 89 | message: err.message??"Unknown error", 90 | color: "red", 91 | autoClose: 5000 92 | }) 93 | }) 94 | .finally(()=>{ 95 | setLoading(false) 96 | }) 97 | })}> 98 | 105 | 106 | 107 | 108 | 109 | 110 |
111 |
112 | } -------------------------------------------------------------------------------- /frontend/src/components/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, AppShell, Box, Container, Divider, Image, Modal, Space, Title } from "@mantine/core" 2 | import { EngineButton, LogoutButton, OptionButton } from "./inputs/Buttons" 3 | import { useGlobalStore, useTokenStore } from "@/utils/stores" 4 | import { useState } from "react" 5 | import { statusQuery } from "@/utils/queries" 6 | import { SetupScreen } from "@/components/screens/SetupScreen" 7 | import { OptionScreen } from "@/components/screens/OptionScreen" 8 | 9 | export const MainLayout = ({ children }: { children: any }) => { 10 | const setToken = useTokenStore((store) => store.setToken) 11 | const { header, errorMessage } = useGlobalStore() 12 | const status = statusQuery() 13 | const [openSetup, setOpenSetup] = useState(false) 14 | const [openOptions, setOpenOptions] = useState(false) 15 | 16 | return 24 | 25 | 30 | 31 | ExploitFarm Logo 32 | 33 | 34 | Exploit Farm 35 | 36 | 37 | {header} 38 | setOpenSetup(true)} /> 39 | 40 | setOpenOptions(true)} /> 41 | 42 | {status.data?.config?.AUTHENTICATION_REQUIRED?<> 43 | { 44 | setToken(null) 45 | status.refetch() 46 | }} /> 47 | 48 | :null} 49 | 50 | 51 | 52 | 53 | {errorMessage? 59 | {errorMessage.message} 60 | :null} 61 | 62 | {children} 63 | 64 | 65 | 66 | Made with ❤️ and 🚩 by Pwnzer0tt1 67 | 68 | 69 | 70 | 71 | 72 | setOpenSetup(false)} 75 | title="Setup Editor ⚙️" 76 | centered 77 | fullScreen 78 | closeOnClickOutside={false} 79 | closeOnEscape={false} 80 | > 81 | setOpenSetup(false)}/> 82 | 83 | setOpenOptions(false)} 86 | title="Options ⚙️" 87 | centered 88 | fullScreen 89 | closeOnClickOutside={false} 90 | closeOnEscape={false} 91 | > 92 | 93 | 94 | 95 | 96 | } -------------------------------------------------------------------------------- /frontend/src/components/charts/LineChartTeamsView.tsx: -------------------------------------------------------------------------------- 1 | import { hashedColor } from "@/utils" 2 | import { statsQuery, statusQuery, useTeamSolver } from "@/utils/queries" 3 | import { getDateSmallFormatted } from "@/utils/time" 4 | import { ChartData, ChartTooltipProps, LineChart } from "@mantine/charts" 5 | import { Box, Divider, Paper, Space } from "@mantine/core" 6 | import { useMemo } from "react" 7 | import { FlagTypeControl } from "@/components/inputs/Controllers" 8 | import { useLocalStorage } from "@mantine/hooks" 9 | import { FlagStatusType } from "@/components/charts/LineChartFlagView" 10 | 11 | 12 | function ChartTooltip({ label, payload }: ChartTooltipProps) { 13 | if (!payload) return null; 14 | const teamSolver = useTeamSolver() 15 | const topTeams = 10 16 | return ( 17 | 18 | 19 | {label} 20 | 21 | Top {topTeams} Teams: 22 | {payload.sort((a, b) => b.value - a.value).slice(0, topTeams).map((item) => ( 23 | 24 | {teamSolver(item.name)}: {item.value} flags 25 | 26 | ))} 27 | 28 | 29 | ); 30 | } 31 | 32 | //This will be expanded in the future 33 | export const LineChartTeamsView = ({ flagType, withControls, teamsList }:{ flagType?:FlagStatusType, withControls?:boolean, teamsList?: number[] }) => { 34 | const status = statusQuery() 35 | const teamSolver = useTeamSolver() 36 | const base_secret = status.data?.server_id??"_" 37 | const [flagStatusFilterChart, setFlagStatusFilterChart] = useLocalStorage({ key: "flagStatusTeamsFilter", defaultValue:flagType??"ok"}) 38 | const finalFlagStatus = withControls?flagStatusFilterChart:(flagType??"ok") 39 | 40 | const series = useMemo(() => { 41 | return [ 42 | { name: "null", label: "Manual", color: "gray"}, 43 | ...status.data?.teams?.filter((team) => teamsList === undefined || teamsList.includes(team.id)).map((team) => ({ name: team.id.toString(), label: teamSolver(team.id), color: hashedColor(base_secret+team.id) }))??[] 44 | ] 45 | }, [status.isFetching, teamsList]) 46 | const stats = statsQuery() 47 | const useTick = status.data?.config?.START_TIME != null 48 | const data = useMemo(() => { 49 | const res = stats.data?.ticks.map((tick) => { 50 | let result:{ date: string, [s:string]: string|number} = { date: useTick?"Tick #"+tick.tick.toString():getDateSmallFormatted(tick.start_time) } 51 | 52 | Object.keys(tick.teams).forEach((id) => { 53 | result[id] = tick.teams[id]?.flags[finalFlagStatus]??0 54 | }) 55 | 56 | return result 57 | }) 58 | return res?.filter((item) => Object.keys(item).length > 1)??[] 59 | }, [stats.isFetching, finalFlagStatus]) 60 | 61 | return 62 | Teams chart 63 | 64 | 65 | 66 | 67 | , 75 | }} 76 | /> 77 | {withControls?<> 78 | 79 | 80 | 81 | 82 | 83 | 84 | :null} 85 | 86 | 87 | } -------------------------------------------------------------------------------- /frontend/src/components/elements/CustomMonacoEditor.tsx: -------------------------------------------------------------------------------- 1 | import OneDarkPro from "@/monaco-theme/OneDarkProDarker.json" 2 | import { Editor, type Monaco } from "@monaco-editor/react"; 3 | 4 | export const CustomMonacoEditor = (props:any) => { 5 | const handleEditorDidMount = (monaco: Monaco) => { 6 | monaco.editor.defineTheme("OneDarkPro",{ 7 | base: "vs-dark", 8 | inherit: true, 9 | rules: [], 10 | ...OneDarkPro, 11 | }); 12 | }; 13 | return ( 14 | 25 | ); 26 | }; -------------------------------------------------------------------------------- /frontend/src/components/elements/ExploitSourceCard.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Card, Title } from "@mantine/core" 2 | import { DeleteButton, DownloadButton, EditButton } from "../inputs/Buttons" 3 | import { getDateFormatted } from "@/utils/time" 4 | import { deleteExploitSource, triggerDownloadExploitSource, useClientSolver } from "@/utils/queries" 5 | import { useState } from "react" 6 | import { ExploitSource } from "@/utils/types" 7 | import { YesOrNoModal } from "../modals/YesOrNoModal" 8 | import { notifications } from "@mantine/notifications" 9 | import { useQueryClient } from "@tanstack/react-query" 10 | import { EditExploitSource } from "../modals/EditExploitSource" 11 | 12 | export const ExploitSourceCard = ({ src, latest, viewOnly }: { src: ExploitSource, latest?:boolean, viewOnly?:boolean }) => { 13 | 14 | const clientSolver = useClientSolver() 15 | const [deleteSourceId, setDeleteSourceId] = useState() 16 | const [editSourceId, setEditSourceId] = useState() 17 | const queryClient = useQueryClient() 18 | 19 | return 20 | 21 | 📦 {src.id} {latest?"(LATEST)":""} 22 | 23 | {!viewOnly?{ 24 | setEditSourceId(src.id) 25 | }} />:null} 26 | { 27 | triggerDownloadExploitSource(src.hash) 28 | }} /> 29 | { 30 | setDeleteSourceId(src.id) 31 | }} /> 32 | 33 | 34 | 35 | 💬 Message: {src.message == ""?"No commit message":src.message} 36 | #️⃣ hash: {src.hash} 37 | ⏱️ Pushed at: {getDateFormatted(src.pushed_at)} 38 | 👤 By: {clientSolver(src.pushed_by)} ({src.arch} {src.distro} {src.os_type}) 39 | setDeleteSourceId(undefined)} 43 | onConfirm={()=>{ 44 | deleteExploitSource(deleteSourceId??"").then(()=>{ 45 | notifications.show({ title: "Exploit commit deleted", message: "The exploit commit has been deleted successfully", color: "green" }) 46 | queryClient.refetchQueries({ queryKey: ["exploits", "sources", src.exploit], }) 47 | }).catch((err)=>{ 48 | notifications.show({ title: "Error deleting the exploit commit", message: `An error occurred while deleting the exploit commit: ${err.message}`, color: "red" }) 49 | }).finally(()=>{ setDeleteSourceId(undefined) }) 50 | }} 51 | title={"Deleting exploit commit"} 52 | /> 53 | setEditSourceId(undefined)} exploit_id={src.exploit} /> 54 | 55 | } -------------------------------------------------------------------------------- /frontend/src/components/elements/ExploitsBar.tsx: -------------------------------------------------------------------------------- 1 | import { exploitsQuery, useClientSolver, useExtendedExploitSolver, useGroupSolver } from "@/utils/queries" 2 | import { Box, Divider, Flex, Paper, ScrollArea, Space, Tooltip } from "@mantine/core" 3 | import { useState } from "react" 4 | import { GoDotFill } from "react-icons/go" 5 | import { ExploitDetailModal } from "@/components/modals/ExploitDetailModal" 6 | 7 | 8 | export const ExploitBar = () => { 9 | const exploits = exploitsQuery() 10 | const extendedExploitSolver = useExtendedExploitSolver() 11 | const clientSolver = useClientSolver() 12 | const groupSolver = useGroupSolver() 13 | const [modalExploitId, setModalExploitId] = useState(null) 14 | 15 | return 16 | Launched Exploits 17 | 18 | 19 | 20 | 21 | 22 | {(exploits.data??[]).sort( (a, b) => { 23 | if (a.status == "disabled" && b.status == "active") 24 | return -1 25 | else if (a.status == "active" && b.status == "disabled") 26 | return 1 27 | else 28 | return b.name.localeCompare(a.name) 29 | } ).map((expl) => setModalExploitId(expl.id)} className="transparency-on-hover" key={expl.id}> 30 | 31 | 32 | 33 | 34 | 35 | 36 | {extendedExploitSolver(expl.id)} 37 | 38 | 39 | {expl.status=="active"?"running":"ran"} by: {expl.last_execution_group_by?`${groupSolver(expl.last_execution_group_by)}`:clientSolver(expl.last_execution_by)} 40 | 41 | 42 | 43 | 44 | 45 | )} 46 | 47 | 48 | setModalExploitId(null)} exploitId={modalExploitId??""} /> 49 | 50 | 51 | 52 | } 53 | 54 | export const StatusPoint = (props:{ status?:"active"|"inactive"|"disabled"|null }) => { 55 | return 56 | 57 | 58 | } -------------------------------------------------------------------------------- /frontend/src/components/inputs/Buttons.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mantine/core" 2 | import { forwardRef } from "react"; 3 | import { CgOptions } from "react-icons/cg"; 4 | import { FaCheck, FaEdit, FaTrash } from "react-icons/fa"; 5 | import { IoMdArrowRoundBack } from "react-icons/io" 6 | import { IoLogOut } from "react-icons/io5" 7 | import { MdAdd } from "react-icons/md"; 8 | import { FaWrench } from "react-icons/fa6"; 9 | import { FaDownload } from "react-icons/fa"; 10 | 11 | export const BackButton = forwardRefvoid, disabled?: boolean}>(({ onClick, disabled }, ref) => { 12 | return 15 | }) 16 | 17 | export const LogoutButton = forwardRefvoid, disabled?: boolean}>(({ onClick, disabled }, ref) => { 18 | return 21 | }) 22 | 23 | export const AddButton = forwardRefvoid, disabled?: boolean}>(({ onClick, disabled }, ref) => { 24 | return 27 | }) 28 | 29 | export const OptionButton = forwardRefvoid, disabled?: boolean}>(({ onClick, disabled }, ref) => { 30 | return 33 | }) 34 | 35 | export const EngineButton = forwardRefvoid, disabled?: boolean}>(({ onClick, disabled }, ref) => { 36 | return 39 | }) 40 | 41 | export const DeleteButton = forwardRefvoid, disabled?: boolean}>(({ onClick, disabled }, ref) => { 42 | return 45 | }) 46 | 47 | export const DownloadButton = forwardRefvoid, disabled?: boolean}>(({ onClick, disabled }, ref) => { 48 | return 51 | }) 52 | 53 | export const EditButton = forwardRefvoid, disabled?: boolean}>((props, ref) => { 54 | return 57 | }) 58 | 59 | export const DoneButton = forwardRefvoid, disabled?: boolean}>((props, ref) => { 60 | return 63 | }) -------------------------------------------------------------------------------- /frontend/src/components/inputs/KArgsInput.tsx: -------------------------------------------------------------------------------- 1 | import { KargsSubmitter } from "@/utils/types" 2 | import { ComboboxItem, Select } from "@mantine/core" 3 | 4 | export const typeDetailInfo = { 5 | str: "String", 6 | int: "Integer", 7 | float: "Float", 8 | bool: "Boolean", 9 | } 10 | 11 | export const KArgsTypeSelector = ({ value, onChange }:{ value: KargsSubmitter, onChange?: (value:KargsSubmitter|null, option:ComboboxItem)=>void }) => { 12 | const isStaticType = onChange == null 13 | let options = Object.keys(typeDetailInfo).map((type) => ({value:type, label:(typeDetailInfo as any)[type]})) 14 | if (!isStaticType){ 15 | options = [ 16 | ...options.filter((opt) => !["int", "float"].includes(opt.value)), {value:"float", label:"Number"}, 17 | ] 18 | } 19 | 20 | return 84 | 85 |