-
Notifications
You must be signed in to change notification settings - Fork 10
Add Instanced challenges #450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: bl4ze/dev
Are you sure you want to change the base?
Changes from 27 commits
8d18c80
4ddd5cb
6075e1e
5b43dce
0714508
3022211
db539d0
57f8aad
047b97e
4cb8eea
5312624
78a9107
00a5cb1
d2e3a99
857b447
ea1ed3a
ec8b944
eb7a6f0
9555cca
3dcf7c0
1b4b7c0
d3e1c0a
89b96b0
28e2a11
83481d0
2d03aa1
5f3effb
8f5ee9a
70b679b
4ffd6e8
584e4eb
5ed55dd
d54ff4f
4be8f6a
b28730c
0f26f37
2842db8
4cde860
9b2a826
4f266f8
f8a43db
32433a3
6d12008
9ce212a
117cb9d
362ca7c
97b5b46
33e662e
0fe5ee9
7c8d14a
70f3e58
e8d98a4
c5069db
f87ce83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,9 @@ default_cpu_shares = 1024 | |
| default_memory_limit = 1024 | ||
| default_pids_limit = 100 | ||
|
|
||
| # Port range for localhost deployments (format: START:END) | ||
| local_host_port_range = "30000:40000" | ||
|
|
||
|
v1bh475u marked this conversation as resolved.
Outdated
|
||
| # List of ip addresses of all the servers where challenge could be deployed for | ||
| # balanced load accross servers. | ||
| [available_servers] | ||
|
|
@@ -44,6 +47,9 @@ username = "user1" | |
| # Path to private SSH key for interacting with the server. | ||
| ssh_key_path = "/path/to/your/private/key1" | ||
|
|
||
| # Port range for this server (format: START:END) | ||
| port_range = "30000:40000" | ||
|
|
||
| # Status of remote server to be used | ||
| # If it is set to false then that remote server will not be used | ||
| active = false | ||
|
|
@@ -53,10 +59,13 @@ active = false | |
| host = "localhost" | ||
|
|
||
| # Username to be used for ssh connection (Leave empty for localhost) | ||
| username = "user1" | ||
| username = "" | ||
|
|
||
| # Path to private SSH key for interacting with the server. (Leave empty for localhost) | ||
| ssh_key_path = "/path/to/your/private/key1" | ||
| ssh_key_path = "" | ||
|
|
||
| # Port range for this server (format: START:END) - uses local_host_port_range if empty | ||
| port_range = "" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand the need for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't localhost treated separately? it's not necessarily added to the server list and is used as a default if no servers have been added. I had brought up treating localhost as a server with dagger (along with I think a few minor fixes), but that's work for another pr.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My issue with this is that because we are treating localhost and other servers separately in case of deployment, we are always having a nasty entangled if-else case spread everywhere making the code kinda messy. |
||
|
|
||
| # Status of remote server to be used | ||
| active = true | ||
|
|
@@ -113,6 +122,19 @@ host = "localhost" | |
| port = "5432" | ||
| sslmode = "prefer" | ||
|
|
||
| [redis_config] | ||
| host = "localhost" | ||
| port = "6379" | ||
| password = "" | ||
| user = "" | ||
|
|
||
| [instance_config] | ||
| # Port Range for localhost. per-server this is configured via `port-range` | ||
| local_host_port_range = '10000:11000' | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| default_expiration = 300 | ||
| max_extension = 600 | ||
| max_instances_per_user = 3 | ||
|
|
||
|
Comment on lines
+128
to
+132
|
||
| # The following fields are required only while hosting a competition on beast | ||
| # This section contains information about the competition to be hosted | ||
| # Structure of the sections with the acceptable fields are: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| FROM php:7.4-apache | ||
|
|
||
| # Install MySQL extension | ||
| RUN docker-php-ext-install mysqli pdo pdo_mysql | ||
|
|
||
| # Copy challenge files | ||
| COPY challenge/ /var/www/html/ | ||
|
|
||
| # Set permissions | ||
| RUN chown -R www-data:www-data /var/www/html | ||
|
|
||
| EXPOSE 80 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| # Instanced Docker Compose Challenge Example | ||
|
|
||
| This is an example of an **instanced challenge using Docker Compose** - a multi-container challenge where each user gets their own isolated environment with a web server and database. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| ┌──────────────────────────────────────────┐ | ||
| │ User's Instanced Environment │ | ||
| │ ┌─────────────┐ ┌─────────────┐ │ | ||
| │ │ PHP/Apache │ ───▶ │ MySQL │ │ | ||
| │ │ (web) │ │ (db) │ │ | ||
| │ └─────────────┘ └─────────────┘ │ | ||
| │ │ │ | ||
| │ ▼ │ | ||
| │ Port: 31234 (dynamically assigned) │ | ||
| └──────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| ## Key Configuration | ||
|
|
||
| In `beast.toml`: | ||
|
|
||
| ```toml | ||
| [challenge.metadata] | ||
| instanced = true | ||
| instance_expiration = 600 # 10 minutes | ||
|
|
||
| [challenge.env] | ||
| docker_compose = "docker-compose.yml" | ||
| default_port = 8080 | ||
| ``` | ||
|
|
||
| In `docker-compose.yml`, use the `INSTANCE_PORT` environment variable: | ||
|
|
||
| ```yaml | ||
| services: | ||
| web: | ||
| ports: | ||
| - "${INSTANCE_PORT:-8080}:80" | ||
| ``` | ||
|
|
||
| ## Challenge Details | ||
|
|
||
| This is a SQL injection challenge: | ||
|
|
||
| 1. The login form is vulnerable to SQL injection | ||
| 2. Bypass authentication to login as admin | ||
| 3. The flag is stored in the `secrets` table | ||
|
|
||
| ### Solution | ||
|
|
||
| ``` | ||
| Username: admin' OR '1'='1' -- | ||
| Password: anything | ||
| ``` | ||
|
|
||
| Or use UNION-based injection to extract data directly. | ||
|
|
||
| ## Testing Locally | ||
|
|
||
| ```bash | ||
| # Build and run locally (for testing) | ||
| cd _examples/instanced-compose | ||
| docker-compose up -d | ||
|
|
||
| # Access at http://localhost:8080 | ||
| ``` | ||
|
|
||
| ## Usage via Beast API | ||
|
|
||
| ```bash | ||
| # Spawn your instance | ||
| curl -X POST -H "Authorization: Bearer $TOKEN" \ | ||
| http://localhost:8080/api/instances/instanced-compose/spawn | ||
|
|
||
| # Response: | ||
| # { | ||
| # "instance_id": "abc123def456", | ||
| # "challenge_name": "instanced-compose", | ||
| # "hosted_address": "localhost", | ||
| # "port": 31234, | ||
| # "expires_at": "2024-01-15T10:40:00Z", | ||
| # "ttl_seconds": 600 | ||
| # } | ||
|
|
||
| # Access your instance | ||
| open http://localhost:31234 | ||
| ``` |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||
| [author] | ||||||
| name = "beast-admin" | ||||||
| email = "admin@beast.local" | ||||||
| ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ" | ||||||
|
|
||||||
| [challenge.metadata] | ||||||
| name = "instanced-compose" | ||||||
| flag = "FLAG{c0mp0s3_1nst4nc3s_r0ck!}" | ||||||
| type = "web" | ||||||
| description = "A web challenge with database backend. Each user gets their own isolated environment!" | ||||||
| points = 200 | ||||||
| difficulty = "medium" | ||||||
| tags = ["web", "sql", "instanced"] | ||||||
| maxAttemptLimit = 100 | ||||||
| instanced = true | ||||||
| instance_expiration = 12 | ||||||
|
||||||
| instance_expiration = 12 | |
| instance_expiration = 600 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <title>Secret Vault - Login</title> | ||
| <style> | ||
| body { | ||
| font-family: 'Segoe UI', Arial, sans-serif; | ||
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); | ||
| min-height: 100vh; | ||
| display: flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| margin: 0; | ||
| color: #fff; | ||
| } | ||
| .container { | ||
| background: rgba(255, 255, 255, 0.1); | ||
| padding: 40px; | ||
| border-radius: 15px; | ||
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | ||
| backdrop-filter: blur(10px); | ||
| width: 350px; | ||
| } | ||
| h1 { | ||
| text-align: center; | ||
| margin-bottom: 30px; | ||
| color: #00d9ff; | ||
| } | ||
| .form-group { | ||
| margin-bottom: 20px; | ||
| } | ||
| label { | ||
| display: block; | ||
| margin-bottom: 8px; | ||
| color: #aaa; | ||
| } | ||
| input[type="text"], input[type="password"] { | ||
| width: 100%; | ||
| padding: 12px; | ||
| border: none; | ||
| border-radius: 8px; | ||
| background: rgba(255, 255, 255, 0.1); | ||
| color: #fff; | ||
| font-size: 16px; | ||
| box-sizing: border-box; | ||
| } | ||
| input[type="submit"] { | ||
| width: 100%; | ||
| padding: 14px; | ||
| border: none; | ||
| border-radius: 8px; | ||
| background: #00d9ff; | ||
| color: #1a1a2e; | ||
| font-size: 16px; | ||
| font-weight: bold; | ||
| cursor: pointer; | ||
| transition: background 0.3s; | ||
| } | ||
| input[type="submit"]:hover { | ||
| background: #00b8d4; | ||
| } | ||
| .error { | ||
| background: rgba(255, 0, 0, 0.2); | ||
| padding: 10px; | ||
| border-radius: 8px; | ||
| margin-bottom: 20px; | ||
| text-align: center; | ||
| } | ||
| .success { | ||
| background: rgba(0, 255, 0, 0.2); | ||
| padding: 10px; | ||
| border-radius: 8px; | ||
| margin-bottom: 20px; | ||
| text-align: center; | ||
| } | ||
| .hint { | ||
| text-align: center; | ||
| margin-top: 20px; | ||
| color: #666; | ||
| font-size: 12px; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="container"> | ||
| <h1>Secret Vault</h1> | ||
|
|
||
| <?php | ||
| $error = ''; | ||
| $success = ''; | ||
|
|
||
| if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||
| $host = getenv('DB_HOST') ?: 'db'; | ||
| $user = getenv('DB_USER') ?: 'challenge'; | ||
| $pass = getenv('DB_PASS') ?: 'challengepass'; | ||
| $dbname = getenv('DB_NAME') ?: 'ctf'; | ||
|
|
||
| $conn = new mysqli($host, $user, $pass, $dbname); | ||
|
|
||
| if ($conn->connect_error) { | ||
| $error = "Connection failed. Please try again."; | ||
| } else { | ||
| $username = $_POST['username']; | ||
| $password = $_POST['password']; | ||
|
|
||
| // VULNERABLE: SQL Injection! | ||
| $query = "SELECT * FROM users WHERE username='$username' AND password='$password'"; | ||
| $result = $conn->query($query); | ||
|
|
||
| if ($result && $result->num_rows > 0) { | ||
| $row = $result->fetch_assoc(); | ||
|
|
||
| if ($row['role'] === 'admin') { | ||
| // Admin login - show secrets | ||
| $secrets_query = "SELECT * FROM secrets"; | ||
| $secrets_result = $conn->query($secrets_query); | ||
|
|
||
| $success = "Welcome Admin! Here are your secrets:<br><br>"; | ||
| while ($secret = $secrets_result->fetch_assoc()) { | ||
| $success .= "<b>" . htmlspecialchars($secret['secret_name']) . ":</b> " . | ||
| htmlspecialchars($secret['secret_value']) . "<br>"; | ||
| } | ||
| } else { | ||
| $success = "Welcome, " . htmlspecialchars($row['username']) . "! You're logged in as a regular user."; | ||
| } | ||
| } else { | ||
| $error = "Invalid username or password!"; | ||
| } | ||
|
|
||
| $conn->close(); | ||
| } | ||
| } | ||
| ?> | ||
|
|
||
| <?php if ($error): ?> | ||
| <div class="error"><?php echo $error; ?></div> | ||
| <?php endif; ?> | ||
|
|
||
| <?php if ($success): ?> | ||
| <div class="success"><?php echo $success; ?></div> | ||
| <?php else: ?> | ||
| <form method="POST"> | ||
| <div class="form-group"> | ||
| <label>Username</label> | ||
| <input type="text" name="username" required placeholder="Enter username"> | ||
| </div> | ||
| <div class="form-group"> | ||
| <label>Password</label> | ||
| <input type="password" name="password" required placeholder="Enter password"> | ||
| </div> | ||
| <input type="submit" value="Login"> | ||
| </form> | ||
| <?php endif; ?> | ||
|
|
||
| <p class="hint">Hint: Try logging in as admin to see the secrets!</p> | ||
| </div> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| version: '3.8' | ||
|
|
||
| services: | ||
| web: | ||
| build: | ||
| context: . | ||
| dockerfile: Dockerfile | ||
| ports: | ||
| # Use INSTANCE_PORT env var if available, otherwise default to 8080 | ||
| - "${INSTANCE_PORT:-8080}:80" | ||
| environment: | ||
| - DB_HOST=db | ||
| - DB_USER=challenge | ||
| - DB_PASS=challengepass | ||
| - DB_NAME=ctf | ||
| depends_on: | ||
| - db | ||
| restart: unless-stopped | ||
|
|
||
| db: | ||
| image: mysql:5.7 | ||
| environment: | ||
| - MYSQL_ROOT_PASSWORD=rootpass | ||
| - MYSQL_DATABASE=ctf | ||
| - MYSQL_USER=challenge | ||
| - MYSQL_PASSWORD=challengepass | ||
| volumes: | ||
| - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro | ||
| restart: unless-stopped |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have this field at all?