Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docker-compose.dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Minimal dev setup - landing page only (for UI testing)
services:
landing:
build:
context: .
dockerfile: software/landing/Dockerfile
restart: "no"
mem_limit: 128m
cap_add:
- NET_ADMIN
- NET_RAW
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
4 changes: 3 additions & 1 deletion software/OpenPLC/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ COPY ./OpenPLC_v3/ /CybICS/OpenPLC_v3/
WORKDIR /CybICS/OpenPLC_v3/
RUN export GNUMAKEFLAGS=-j$(nproc --all) \
&& alias make='make -j$(nproc --all)' \
&& ./install.sh docker
&& ./install.sh docker \
&& apt-get update && apt-get install -y --no-install-recommends iptables \
&& rm -rf /var/lib/apt/lists/*

# copy files after compile the main part to reduce re-compile time on changes on cybICS.st
WORKDIR /CybICS/OpenPLC_v3/webserver
Expand Down
6 changes: 4 additions & 2 deletions software/ids/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ def start_evasion(self):
}
self.evasion_active = True
self.evasion_start_time = time.time()
self.evasion_start_alert_count = self.stats["alerts_total"]
self.evasion_start_alert_id = self._alert_id
self.evasion_modbus_writes = 0
logger.info("Evasion challenge started")
return {
Expand All @@ -329,7 +329,9 @@ def check_evasion(self):
}

elapsed = time.time() - self.evasion_start_time
new_alerts = self.stats["alerts_total"] - self.evasion_start_alert_count
# Count only attacker-relevant alerts (exclude background arp_spoof noise)
evasion_alerts = self.get_alerts(since_id=self.evasion_start_alert_id)
new_alerts = sum(1 for a in evasion_alerts if a.get("rule") != "arp_spoof")

if elapsed > self.evasion_timeout:
self.evasion_active = False
Expand Down
12 changes: 11 additions & 1 deletion software/ids/ids_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
FORENSICS_MIN_ALERTS = 5 # minimum alerts for forensics challenge
FORENSICS_MIN_RULES = 3 # minimum distinct rule types for forensics

# Per-rule CTF flags — revealed when the specific rule has been triggered
CTF_RULE_FLAGS = {
"port_scan": "CybICS(sc4n_d3tect3d)",
"modbus_flood": "CybICS(m0dbus_fl00d_d3tect3d)",
}


# ========== API ROUTES ==========

Expand Down Expand Up @@ -152,11 +158,15 @@ def rules_stats():
result = {}
for rule, s in stats.items():
top = sorted(s["sources"].items(), key=lambda x: x[1], reverse=True)[:5]
result[rule] = {
entry = {
"count": s["count"],
"last_seen": s["last_seen"],
"top_sources": [{"ip": ip, "count": c} for ip, c in top],
}
# Reveal CTF flag if the rule has been triggered
if s["count"] > 0 and rule in CTF_RULE_FLAGS:
entry["flag"] = CTF_RULE_FLAGS[rule]
result[rule] = entry

return jsonify(result)

Expand Down
9 changes: 4 additions & 5 deletions software/ids/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,10 @@ def _check_syn(self, pkt, src_ip, dport, now, alerts):
tracker[port_key] = {}
ports = tracker[port_key]
ports[dport] = now
# Prune expired (only when dict grows, amortized)
if len(ports) > self.PORT_SCAN_THRESHOLD + 5:
cutoff = now - self.PORT_SCAN_WINDOW
tracker[port_key] = {p: t for p, t in ports.items() if t >= cutoff}
ports = tracker[port_key]
# Prune expired entries before checking threshold
cutoff = now - self.PORT_SCAN_WINDOW
tracker[port_key] = {p: t for p, t in ports.items() if t >= cutoff}
ports = tracker[port_key]
unique_ports = len(ports)
if unique_ports >= self.PORT_SCAN_THRESHOLD:
if self._should_alert("port_scan", src_ip, now):
Expand Down
35 changes: 33 additions & 2 deletions software/landing/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
logger.warning(f'Challenge not found: {challenge_id}')
return "Challenge not found", 404

# Load training content if available
# Load training content
training_content = ""
if 'training_content' in challenge:
training_content = ctf_manager.load_markdown_content(challenge['training_content'])
Expand All @@ -215,7 +215,38 @@
challenge=challenge,
category=category,
training_content=training_content,
solved=challenge_id in session['solved_challenges'])
solved=challenge_id in session['solved_challenges'],
challenge_type=challenge.get('type', 'offensive'))

@app.route('/ctf/verify/<challenge_id>', methods=['POST'])
def verify_defense(challenge_id):
"""Verify a defense challenge and auto-submit flag on success"""
initialize_session()

current_progress = get_current_progress()

# Check if already solved
if challenge_id in current_progress.get('solved_challenges', []):
return jsonify({'success': True, 'message': 'Challenge already solved!', 'checks': []})

# Run verification
result = ctf_manager.verify_defense(challenge_id)

# If verification passed, auto-submit the flag
if result.get('success') and result.get('flag'):
submit_result = ctf_manager.submit_flag(challenge_id, result['flag'], current_progress)
if submit_result['success']:
session['solved_challenges'].append(challenge_id)
session['total_points'] += submit_result['points']
session.modified = True
ctf_manager.save_progress({
'solved_challenges': session['solved_challenges'],
'total_points': session['total_points']
})
result['points'] = submit_result['points']
result['message'] = f"{result['message']} You earned {submit_result['points']} points!"

return jsonify(result)

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI about 2 months ago

In general, the problem should be fixed by ensuring that exception objects (or their string representations) are never sent directly to clients. Instead, log detailed error information on the server and return a generic, user-friendly error message to the client.

The best targeted fix here is to change the except Exception as e block in CTFManager.verify_defense so that it continues to log the detailed error (including stack trace) but returns a generic message that does not contain str(e). We do not need to change the /ctf/verify/<challenge_id> route in app.py; it can continue to return whatever verify_defense produces. That keeps existing control flow and success behavior intact while only altering the contents of error messages. Concretely, in software/landing/modules/ctf_manager.py, lines 130–132 should be updated to:

  • Keep the logger.error(..., exc_info=True) call so that developers still see the detailed error and stack trace.
  • Replace the returned dict’s "message" value from f'Verification error: {str(e)}' to a generic string such as 'An internal verification error occurred.' (or similarly non-revealing text).

No new methods or imports are required; we rely only on the existing logger.

Suggested changeset 1
software/landing/modules/ctf_manager.py
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/software/landing/modules/ctf_manager.py b/software/landing/modules/ctf_manager.py
--- a/software/landing/modules/ctf_manager.py
+++ b/software/landing/modules/ctf_manager.py
@@ -129,7 +129,11 @@
             return {'success': False, 'message': f'Verification module not found: {verify_module}', 'checks': []}
         except Exception as e:
             logger.error(f"Error running defense check {verify_module}: {e}", exc_info=True)
-            return {'success': False, 'message': f'Verification error: {str(e)}', 'checks': []}
+            return {
+                'success': False,
+                'message': 'An internal verification error occurred.',
+                'checks': []
+            }
 
     def reset_progress(self):
         """Reset all progress"""
EOF
@@ -129,7 +129,11 @@
return {'success': False, 'message': f'Verification module not found: {verify_module}', 'checks': []}
except Exception as e:
logger.error(f"Error running defense check {verify_module}: {e}", exc_info=True)
return {'success': False, 'message': f'Verification error: {str(e)}', 'checks': []}
return {
'success': False,
'message': 'An internal verification error occurred.',
'checks': []
}

def reset_progress(self):
"""Reset all progress"""
Copilot is powered by AI and may make mistakes. Always verify output.

@app.route('/ctf/submit', methods=['POST'])
def submit_flag():
Expand Down
122 changes: 105 additions & 17 deletions software/landing/ctf_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"title": "Understanding the Physical Process",
"description": "Learn about industrial processes and control systems",
"points": 100,
"tag": "analysis",
"flag": "CybICS(Bl0w0ut)",
"hint": "Examine the physical process simulation and look for anomalies",
"training_content": "training/physical_process/README.md"
Expand All @@ -20,6 +21,7 @@
"title": "PLC Programming",
"description": "Introduction to PLC programming and control logic",
"points": 150,
"tag": "analysis",
"flag": "CybICS(ladder_logic_modified)",
"hint": "Look for backdoors or unauthorized modifications in the ladder logic",
"training_content": "training/plc_programming/README.md"
Expand All @@ -35,6 +37,7 @@
"title": "Service Scanning",
"description": "Discover network services and open ports",
"points": 100,
"tag": "recon",
"flag": "CybICS(scanning_d0ne)",
"hint": "Use network scanning tools to discover open ports and services",
"training_content": "training/scanning/README.md"
Expand All @@ -44,30 +47,33 @@
"title": "S7comm Scanning",
"description": "Analyze Siemens S7 communication",
"points": 150,
"tag": "recon",
"flag": "CybICS(s7comm_analysis_complete)",
"hint": "Focus on Siemens S7 protocol communication patterns",
"training_content": "training/scanning2/README.md"
},
{
"id": "wireshark_capture",
"id": "wireshark_capture",
"title": "Wireshark Capture",
"description": "Learn network traffic analysis",
"points": 200,
"tag": "analysis",
"flag": "CybICS(m0dbu$)",
"hint": "Look for unusual protocols or data patterns in network traffic",
"training_content": "training/wireshark_capture/README.md"
}
]
},
"security_testing": {
"name": "🛡️ Security Testing",
"name": "️ Security Testing",
"description": "Test system resilience and security mechanisms",
"challenges": [
{
"id": "flood_overwrite",
"title": "Flood & Overwrite",
"description": "Test system resilience against flooding attacks",
"points": 200,
"tag": "exploit",
"flag": "CybICS(flood_attack_successful)",
"hint": "Overwhelm the system with excessive requests or data",
"training_content": "training/flood_overwrite/README.md"
Expand All @@ -77,6 +83,7 @@
"title": "Password Attack",
"description": "Practice password security testing",
"points": 150,
"tag": "exploit",
"flag": "CybICS(FU##A)",
"hint": "Look for common passwords in authentication attempts",
"training_content": "training/password_attack/README.md"
Expand All @@ -86,6 +93,7 @@
"title": "OPC-UA",
"description": "Explore OPC-UA protocol security",
"points": 250,
"tag": "exploit",
"flag": "CybICS(OPC-UA-$ADMIN)",
"hint": "Check for default credentials or weak authentication",
"training_content": "training/opcua/README.md"
Expand All @@ -95,6 +103,7 @@
"title": "Fuzzing Modbus",
"description": "Test Modbus protocol robustness",
"points": 300,
"tag": "exploit",
"flag": "CybICS(modbus_fuzzing_complete)",
"hint": "Send malformed or unexpected data to test protocol handling",
"training_content": "training/fuzzingMB/README.md"
Expand All @@ -103,72 +112,151 @@
},
"advanced_security": {
"name": "🔐 Advanced Security",
"description": "Advanced security techniques and detection methods",
"description": "Advanced attack and detection techniques",
"challenges": [
{
"id": "mitm",
"title": "Man-in-the-Middle (MitM)",
"description": "Learn about network interception",
"points": 350,
"tag": "exploit",
"flag": "CybICS(mitm_attack_successful)",
"hint": "Position yourself between communicating devices",
"training_content": "training/mitm/README.md"
},
{
"id": "uart_basic",
"title": "UART Training Guide (Hardware Required)",
"description": "Hardware-level communication",
"title": "UART Training Guide",
"description": "Hardware-level communication (requires hardware)",
"points": 300,
"tag": "hardware",
"flag": "CybICS(U#RT)",
"hint": "Monitor serial communications for credentials or commands",
"training_content": "training/uart_basic/README.md"
},
{
"id": "detect_basic",
"title": "Detect Basic",
"description": "Basic intrusion detection",
"title": "Detect Port Scan",
"description": "Perform a port scan and verify the IDS detected it — find the flag in the IDS rule stats",
"points": 250,
"flag": "CybICS(basic_detection_complete)",
"hint": "Identify basic attack patterns and signatures",
"tag": "analysis",
"flag": "CybICS(sc4n_d3tect3d)",
"hint": "Run a port scan, then query the IDS at /api/rules/stats — the flag appears when the port_scan rule has fired",
"training_content": "training/detect_basic/README.md"
},
{
"id": "detect_overwrite",
"title": "Detect Overwrite",
"description": "Advanced detection techniques",
"title": "Detect Modbus Flooding",
"description": "Launch a Modbus flooding attack, analyze it with the IDS, and find the flag in the rule stats",
"points": 400,
"flag": "CybICS(detection_evasion_complete)",
"hint": "Understand how security systems detect attacks and find ways to bypass them",
"tag": "analysis",
"flag": "CybICS(m0dbus_fl00d_d3tect3d)",
"hint": "Run the override script, then query /api/rules/stats — the flag appears when the modbus_flood rule fires",
"training_content": "training/detect_overwrite/README.md"
},
}
]
},
"ids_challenges": {
"name": "🔍 IDS Challenges",
"description": "Interact with the Intrusion Detection System — trigger alerts, analyze incidents, and evade detection",
"challenges": [
{
"id": "ids_challenge",
"title": "IDS Challenge",
"description": "Trigger the intrusion detection system by performing attacks and retrieve the flag",
"points": 300,
"tag": "exploit",
"flag": "CybICS(1ntrusi0n_d3tect3d)",
"hint": "Perform multiple different attacks and check the IDS flag endpoint",
"training_content": "training/ids_challenge/README.md"
},
{
"id": "ids_forensics",
"title": "IDS Forensics",
"description": "Analyze IDS alert data to investigate a security incident and answer forensic questions",
"description": "Analyze IDS alert data to investigate a security incident",
"points": 250,
"tag": "analysis",
"flag": "CybICS(f0r3ns1c_4n4lyst)",
"hint": "Use the /api/summary and /api/alerts endpoints to gather data about the incident",
"training_content": "training/ids_forensics/README.md"
},
{
"id": "ids_evasion",
"title": "IDS Evasion",
"description": "Perform a stealth Modbus write operation without triggering any IDS alerts",
"description": "Perform a stealth Modbus write without triggering any IDS alerts",
"points": 400,
"tag": "evasion",
"flag": "CybICS(st34lth_0p3r4t0r)",
"hint": "Study the IDS detection thresholds on the Rules tab and send writes slowly",
"training_content": "training/ids_evasion/README.md"
}
]
},
"defense": {
"name": "🛡️ Defense & Hardening",
"description": "Harden the ICS environment by applying defensive security measures",
"challenges": [
{
"id": "defense_openplc_password",
"title": "Harden OpenPLC Credentials",
"description": "Change the default OpenPLC password to prevent unauthorized access",
"points": 150,
"tag": "defense",
"type": "defense",
"verify_module": "check_openplc_password",
"flag": "CybICS(0penPLC_h4rd3n3d)",
"hint": "Log into the OpenPLC web UI at port 8080 and change the default password from 'openplc'",
"training_content": "training/defense_password/README.md"
},
{
"id": "defense_fuxa_password",
"title": "Harden FUXA Credentials",
"description": "Change the default FUXA admin password to prevent unauthorized HMI access",
"points": 150,
"tag": "defense",
"type": "defense",
"verify_module": "check_fuxa_password",
"flag": "CybICS(FUXA_s3cur3d)",
"hint": "Access FUXA at port 1881 and change the admin password from its default value",
"training_content": "training/defense_password/README.md"
},
{
"id": "defense_firewall",
"title": "Modbus Firewall Rules",
"description": "Restrict Modbus (port 502) access so only authorized systems can communicate with the PLC",
"points": 250,
"tag": "defense",
"type": "defense",
"verify_module": "check_firewall",
"flag": "CybICS(f1r3w4ll_h4rd3n3d)",
"hint": "Use iptables on the OpenPLC container to only allow 172.18.0.4 (FUXA) to access port 502",
"training_content": "training/defense_firewall/README.md"
},
{
"id": "defense_network_segmentation",
"title": "Network Segmentation",
"description": "Block the attack machine from reaching critical ICS services",
"points": 300,
"tag": "defense",
"type": "defense",
"verify_module": "check_network_segmentation",
"flag": "CybICS(n3tw0rk_s3gm3nt3d)",
"hint": "Block the attack machine (172.18.0.100) from accessing the PLC and OPC-UA server",
"training_content": "training/defense_network/README.md"
},
{
"id": "defense_ids_tuning",
"title": "IDS Monitoring & Tuning",
"description": "Ensure the IDS is running and actively detecting multiple types of attacks",
"points": 200,
"tag": "defense",
"type": "defense",
"verify_module": "check_ids_tuning",
"flag": "CybICS(1ds_tun3d)",
"hint": "Start the IDS, ensure it is capturing packets, and trigger at least 3 different detection rules",
"training_content": "training/defense_ids/README.md"
}
]
}
}
}
}
Loading
Loading