Skip to content

Commit

Permalink
fix: general fixes + image on README
Browse files Browse the repository at this point in the history
  • Loading branch information
domysh committed Jun 27, 2024
1 parent d2ceddb commit cf731d5
Show file tree
Hide file tree
Showing 20 changed files with 63 additions and 45 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ allow players to analyze and improve their exploits and have a clear view of the

The design is inspired by [DestructiveFarm](https://github.com/DestructiveVoice/DestructiveFarm), but it is being developed from scratch, only some very small parts are being reused.

![ExploitFarm Web Interface](docs/exploitfarm-web.png)

## How it works

There is a main central server of exploitfarm that will be responsible for get configurations, send exploits to clients, receive flags and send to the platform, and store all data from attacks. The server doen't attack, it mainly coordinates the attacks and submit flags.
Expand Down Expand Up @@ -148,6 +150,8 @@ Now you can write your exploit, and test it with the following command:
xfarm start --test <host>
```

![ExploitFarm Web Interface](docs/xfarm-start-cmd.png)

The `<host>` will be passed to the XFARM_HOST environment variable, and will be used to test the exploit. The attack will be executed once and if some flags are found they will be printed on the screen and also submitted to the server as a manual submission.

If your exploit is working as expected, you can start the attack with the following command:
Expand Down
10 changes: 3 additions & 7 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ async def is_loggined(token: str = Depends(oauth2_scheme)) -> None|bool:
#If the app is running and requires login
if not token:
return None
#Authenticating with the AUTH_KEY
if token.strip() == await AUTH_KEY():
return True

try:
payload = jwt.decode(token, await APP_SECRET(), algorithms=[JWT_ALGORITHM])
authenticated: bool = payload.get("authenticated", False)
Expand Down Expand Up @@ -169,8 +167,8 @@ async def get_status(loggined: None|bool = Depends(is_loggined)):
status=config.SETUP_STATUS,
loggined=bool(loggined),
config=config if loggined else None,
server_id=await SERVER_ID(),
server_time=datetime_now(),
auth_key=None if config.SETUP_STATUS == SetupStatus.SETUP else await AUTH_KEY(),
teams = json_like(await Team.objects.all()) if loggined else None,
submitter=None if not loggined or config.SUBMITTER is None else json_like(await Submitter.objects.get(id=config.SUBMITTER)),
messages= messages if loggined else None,
Expand All @@ -189,8 +187,6 @@ async def set_status(data: Dict[str, str|int|None]):
raise HTTPException(400, f"Invalid key {key}")
if key == "SETUP_STATUS" and config.SETUP_STATUS != SetupStatus.SETUP:
raise HTTPException(400, "Setup status cannot be changed back to setup")
if key == "SERVER_ID":
raise HTTPException(400, "Server ID cannot be changed")
if key == "SUBMITTER":
if not await Submitter.objects.get_or_none(id=data[key]):
raise HTTPException(400, "Submitter not found")
Expand Down Expand Up @@ -230,7 +226,7 @@ async def catch_all(full_path:str):
os.environ["TIMEOUT"] = "30"
os.environ["TZ"] = "Etc/UTC"
time.tzset()
init_db()
init_db()
submitter = run_submitter_daemon()
stats = run_stats_daemon()
try:
Expand Down
31 changes: 23 additions & 8 deletions backend/db.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ormar, sqlalchemy, databases, env, secrets
import sqlalchemy.exc
from pydantic import AwareDatetime
from pydantic import PlainSerializer, BaseModel
from typing import Dict, Any, Annotated
Expand Down Expand Up @@ -158,7 +159,7 @@ async def FUNC() -> str:
return FUNC

APP_SECRET = get_dbenv_func("APP_SECRET", lambda: secrets.token_hex(32), value_cached=True)
AUTH_KEY = get_dbenv_func("AUTH_KEY", lambda: str(uuid4()))
SERVER_ID = get_dbenv_func("SERVER_ID", lambda: str(uuid4()), value_cached=True)
SUBMITTER_ERROR_OUTPUT = get_dbenv_func("SUBMITTER_ERROR_OUTPUT", lambda: "")

async def __async_init_db():
Expand All @@ -169,18 +170,32 @@ async def __async_init_db():
await close_db()

def init_db():
if RESET_DB_DANGEROUS:
dbconf.metadata.drop_all(dbconf.engine)
dbconf.metadata.create_all(dbconf.engine)
asyncio.run(__async_init_db())
while True:
try:
if RESET_DB_DANGEROUS:
print("!!! Resetting database !!!")
dbconf.metadata.drop_all(dbconf.engine)
dbconf.metadata.create_all(dbconf.engine)
asyncio.run(__async_init_db())
print("Database initialized.")

break
except sqlalchemy.exc.OperationalError:
print("Database not ready, retrying...")
time.sleep(1)
continue

async def connect_db():
connection = await dbconf.database.connect()
return connection
if dbconf.database.is_connected:
return dbconf.database
else:
connection = await dbconf.database.connect()
return connection


async def close_db():
return await dbconf.database.disconnect()
if dbconf.database.is_connected:
await dbconf.database.disconnect()

transactional = dbconf.database.transaction()

Expand Down
1 change: 0 additions & 1 deletion backend/models/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from pydantic import BaseModel
from pydantic import AwareDatetime
from pydantic import IPvAnyAddress
from db import UnHashedClientID, ClientID

###-- Client Models --###
Expand Down
4 changes: 1 addition & 3 deletions backend/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ class Configuration(BaseModel):

SETUP_STATUS: SetupStatus = SetupStatus.SETUP

SERVER_ID: UUID = uuid4()

__start_time = None
__end_time = None

Expand Down Expand Up @@ -126,13 +124,13 @@ class StatusAPI(BaseModel):
loggined: bool
config: Configuration|None = None
server_time: AwareDatetime
auth_key: str|None = None
submitter: None|SubmitterDTO = None
teams: List[TeamDTO]|None = None
messages: List[MessageInfo]|None
services: List[ServiceDTO]|None
start_time: AwareDatetime|None = None
end_time: AwareDatetime|None = None
version: str = env.VERSION
server_id: str = None
whoami: str = "exploitfarm"

3 changes: 0 additions & 3 deletions client/exploitfarm/utils/reqs.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,6 @@ def login(self,password:str) -> str:
def authenicate(self, password:str, save:bool = True) -> str|None:
auth_key = self.login(password)
self.refresh_session(auth_key)
status = self.status()
auth_key = status.get("auth_key", None)
self.refresh_session(auth_key)
self.config.server.auth_key = auth_key
if save: self.config.write()
return auth_key
Expand Down
4 changes: 2 additions & 2 deletions client/exploitfarm/xploit.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,10 @@ def update_server_config():
else:
g.memory["config_update"] = True
if not g.server_id:
g.server_id = g.config.status["config"]["SERVER_ID"]
g.server_id = g.config.status["server_id"]
repush_flags()
else:
if g.server_id != g.config.status["config"]["SERVER_ID"]:
if g.server_id != g.config.status["server_id"]:
qprint('Server ID changed, restart the exploit')
shutdown(restart=True)
return
Expand Down
Binary file added docs/exploitfarm-web.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/xfarm-start-cmd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function App() {
alignItems: "center"
}}>
<Space w="md" />
<Image src="/logo.png" alt="ExploitFarm Logo" width={50} height={50} style={{marginLeft:5}}/>
<Image src="/logo.png" alt="ExploitFarm Logo" width={50} height={50} mih={50} miw={50} style={{marginLeft:5}}/>
<Space w="xs" />
<Title order={2}>
Exploit Farm
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/ExploitsBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const ExploitBar = () => {
const exploits = exploitsQuery()
const extendedExploitSolver = useExtendedExploitSolver()
const status = statusQuery()
const base_secret = status.data?.config?.SERVER_ID??"_"
const base_secret = status.data?.server_id??"_"

return <Box style={{width:"100%", fontSize:"90%"}} >
<b>Launched Exploits</b>
Expand All @@ -17,7 +17,14 @@ export const ExploitBar = () => {
<Space h="lg" />
<ScrollArea style={{width:"100%"}}>
<Flex>
{(exploits.data??[]).sort( (expl) => expl.status=="disabled"?-1:1 ).map((expl) => <Box>
{(exploits.data??[]).sort( (a, b) => {
if (a.status == "disabled" && b.status == "active")
return -1
else if (a.status == "active" && b.status == "disabled")
return 1
else
return b.name.localeCompare(a.name)
} ).map((expl) => <Box>
<Box style={{fontSize:"90%"}}>
<Paper shadow="md" radius="xl" withBorder px={20} py={5} bg="gray" mx="xs">
<Box className="center-flex">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/LineChartAttackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const LineChartAttackView = ({ seriesType, attackType, chartType, withCon
const status = statusQuery()
const exploits = exploitsQuery()
const clients = clientsQuery()
const base_secret = status.data?.config?.SERVER_ID??"_"
const base_secret = status.data?.server_id??"_"

const [seriesTypeChart, setSeriesTypeChart] = useLocalStorage<SeriesType>({ key: "attackSeriesType", defaultValue:seriesType??"globals"})
const [attackStatusFilterChart, setAttackStatusFilterChart] = useLocalStorage<AttackStatusType>({ key: "attackStatusFilter", defaultValue:attackType??"tot"})
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/LineChartFlagView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const LineChartFlagView = ({ seriesType, flagType, chartType, withControl
const status = statusQuery()
const exploits = exploitsQuery()
const clients = clientsQuery()
const base_secret = status.data?.config?.SERVER_ID??"_"
const base_secret = status.data?.server_id??"_"
const [seriesTypeChart, setSeriesTypeChart] = useLocalStorage<SeriesType>({ key: "flagSeriesType", defaultValue:seriesType??"services"})
const [flagStatusFilterChart, setFlagStatusFilterChart] = useLocalStorage<FlagStatusType>({ key: "flagStatusFilter", defaultValue:flagType??"tot"})
const [flagTypeChart, setFlagTypeChart] = useLocalStorage<LineChartType>({ key: "flagTypeChart", defaultValue:chartType??"area"})
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/LineChartTeamsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function ChartTooltip({ label, payload }: ChartTooltipProps) {
export const LineChartTeamsView = ({ flagType, withControls }:{ flagType?:FlagStatusType, withControls?:boolean }) => {
const status = statusQuery()
const teamSolver = useTeamSolver()
const base_secret = status.data?.config?.SERVER_ID??"_"
const base_secret = status.data?.server_id??"_"
const [flagStatusFilterChart, setFlagStatusFilterChart] = useLocalStorage<FlagStatusType>({ key: "flagStatusTeamsFilter", defaultValue:flagType??"tot"})
const finalFlagStatus = withControls?flagStatusFilterChart:(flagType??"tot")

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/WelcomeTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const WelcomeTitle = (
}}>
{title??"Welcome to ExploitFarm"}
</Title>
<Image src="/logo.png" alt="ExploitFarm Logo" width={70} height={70} style={{marginLeft:5}}/>
<Image src="/logo.png" alt="ExploitFarm Logo" width={70} height={70} mih={70} miw={70} style={{marginLeft:5}}/>
</Box>
<Space h="lg" />
<Title order={3} style={{
Expand Down
10 changes: 2 additions & 8 deletions frontend/src/utils/backend_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,6 @@ export interface components {
PASSWORD_HASH?: string | null;
/** @default setup */
SETUP_STATUS?: components["schemas"]["SetupStatus"];
/**
* Server Id
* Format: uuid
* @default 951d5330-d3f4-48a8-b62e-7ed0b2971c91
*/
SERVER_ID?: string;
};
/** CustomPage[AttackExecutionDTO] */
CustomPage_AttackExecutionDTO_: {
Expand Down Expand Up @@ -535,14 +529,14 @@ export interface components {
status: components["schemas"]["SetupStatus"];
/** Loggined */
loggined: boolean;
/** Server Id */
server_id: string;
config?: components["schemas"]["Configuration"] | null;
/**
* Server Time
* Format: date-time
*/
server_time: string;
/** Auth Key */
auth_key?: string | null;
submitter?: components["schemas"]["SubmitterDTO"] | null;
/** Teams */
teams?: components["schemas"]["TeamDTO"][] | null;
Expand Down
4 changes: 4 additions & 0 deletions start.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,13 @@ def write_compose():
- "host.docker.internal:host-gateway"
ports:
- {args.port}:5050
depends_on:
- database
database:
image: postgres
restart: unless-stopped
container_name: {container_name}-database
command: ["postgres", "-c", "max_connections=1000"]
environment:
- POSTGRES_USER={container_name}
- POSTGRES_PASSWORD={container_name}
Expand Down
5 changes: 3 additions & 2 deletions tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
parser.add_argument("--password", "-p", type=str, default=None, help="Setup the environment")
parser.add_argument("--teams", "-T", type=int, default=100, help="Number of teams")
parser.add_argument("--tick", "-t", type=int, default=60, help="Tick duration")
parser.add_argument("--docker", action="store_true", help="Run in docker mode (with host.docker.internal)")
args = parser.parse_args()

app = FastAPI()
Expand Down Expand Up @@ -48,10 +49,10 @@ def run_webserver():
workers=1
)

SUBMITTER_CODE = """
SUBMITTER_CODE = f"""
import requests
def submit(flags, url:str = "http://127.0.0.1:4456/"):
def submit(flags, url:str = "http://{'host.docker.internal' if args.docker else '127.0.0.1'}:4456/"):
return requests.post(url, json=flags).json()
"""
Expand Down
2 changes: 1 addition & 1 deletion tests/xploit_test/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name = "xploit_test"
interpreter = "python3"
run = "main.py"
language = "python"
service = "c5f5f858-f218-4e71-8725-dff481fe7e2c"
service = "7e6b60c9-aa37-4a95-9985-a7786ea505ae"
9 changes: 6 additions & 3 deletions tests/xploit_test/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
host = get_host()

simulate_random_crash = False
simulate_work = True
simulate_connection = True
simulate_work = False
simulate_connection = False
random_flags = True
flags_max = 10
flags_min = 5

if simulate_connection:
remote("google.com", 80).close()

print(f"Hello {host}! This text should contain a lot of flags!")

flags =[random_str(32)+"=" for _ in range(10)]
flags =[random_str(32)+"=" for _ in range(flags_max if not random_flags else random.randint(flags_min, flags_max))]

print(f"Submitted {len(flags)} flags")

Expand Down

0 comments on commit cf731d5

Please sign in to comment.