Add lessons
This commit is contained in:
@@ -1,10 +1,95 @@
|
||||
const LessonModule = require('../base/LessonModule');
|
||||
const progressQueries = require('../../../src/models/queries/progress.queries');
|
||||
|
||||
/**
|
||||
* Beginner-Friendly SQL Injection Shop Lesson
|
||||
* Simplified to 3 progressive challenges with helpful hints
|
||||
*
|
||||
* Activity data structure:
|
||||
* {
|
||||
* discoveries: ['GENERIC', 'BYPASS_FILTER', 'UNION_SELECT'],
|
||||
* timerStart: timestamp,
|
||||
* unionHintShown: boolean
|
||||
* }
|
||||
*/
|
||||
class SQLInjectionShopLesson extends LessonModule {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activity data from database
|
||||
*/
|
||||
async _getActivityData(participantId, eventLessonId) {
|
||||
try {
|
||||
const data = await progressQueries.getActivityData(participantId, eventLessonId);
|
||||
console.log(`[SQL Injection] Loading activity data for participant ${participantId}, event ${eventLessonId}:`, data);
|
||||
return {
|
||||
discoveries: data.discoveries || [],
|
||||
timerStart: data.timerStart || null,
|
||||
unionHintShown: data.unionHintShown || false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SQL Injection] Error loading activity data:', error);
|
||||
return {
|
||||
discoveries: [],
|
||||
timerStart: null,
|
||||
unionHintShown: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save activity data to database
|
||||
*/
|
||||
async _saveActivityData(participantId, eventLessonId, activityData) {
|
||||
try {
|
||||
console.log(`[SQL Injection] Saving activity data for participant ${participantId}, event ${eventLessonId}:`, activityData);
|
||||
await progressQueries.updateActivityData(participantId, eventLessonId, activityData);
|
||||
console.log('[SQL Injection] Activity data saved successfully');
|
||||
} catch (error) {
|
||||
console.error('[SQL Injection] Error saving activity data:', error);
|
||||
throw error; // Re-throw to see the error in the main flow
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start challenge timer
|
||||
*/
|
||||
async startTimer(participantId, eventLessonId) {
|
||||
const activityData = await this._getActivityData(participantId, eventLessonId);
|
||||
|
||||
if (!activityData.timerStart) {
|
||||
activityData.timerStart = Date.now();
|
||||
await this._saveActivityData(participantId, eventLessonId, activityData);
|
||||
}
|
||||
|
||||
const discoveries = new Set(activityData.discoveries);
|
||||
const totalDiscoveries = 3;
|
||||
|
||||
return {
|
||||
started: true,
|
||||
duration: 600, // 10 minutes in seconds
|
||||
message: 'Timer gestartet! Du hast 10 Minuten Zeit.',
|
||||
discoveries: {
|
||||
found: discoveries.size,
|
||||
total: totalDiscoveries,
|
||||
types: Array.from(discoveries)
|
||||
},
|
||||
unionHintShown: activityData.unionHintShown
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get elapsed time for participant
|
||||
*/
|
||||
async _getElapsedTime(participantId, eventLessonId) {
|
||||
const activityData = await this._getActivityData(participantId, eventLessonId);
|
||||
const start = activityData.timerStart;
|
||||
if (!start) return 0;
|
||||
return Math.floor((Date.now() - start) / 1000);
|
||||
}
|
||||
|
||||
// Mock database with products
|
||||
getMockDatabase() {
|
||||
return {
|
||||
@@ -13,25 +98,18 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
{ id: 2, name: 'Wireless Mouse', price: 29.99, category: 'Accessories', stock: 50 },
|
||||
{ id: 3, name: 'USB-C Cable', price: 12.99, category: 'Accessories', stock: 100 },
|
||||
{ id: 4, name: 'Gaming Keyboard', price: 89.99, category: 'Electronics', stock: 25 },
|
||||
{ id: 5, name: 'Monitor 27"', price: 349.99, category: 'Electronics', stock: 20 },
|
||||
{ id: 6, name: 'Webcam HD', price: 79.99, category: 'Electronics', stock: 30 },
|
||||
{ id: 7, name: 'Desk Lamp', price: 34.99, category: 'Office', stock: 40 },
|
||||
{ id: 8, name: 'Notebook Set', price: 15.99, category: 'Office', stock: 60 }
|
||||
{ id: 5, name: 'Monitor 27"', price: 349.99, category: 'Electronics', stock: 20 }
|
||||
],
|
||||
users: [
|
||||
{ id: 1, username: 'admin', password: 'hashed_admin_password', role: 'admin' },
|
||||
{ id: 2, username: 'john_doe', password: 'hashed_user_password', role: 'customer' },
|
||||
{ id: 3, username: 'jane_smith', password: 'hashed_user_password', role: 'customer' }
|
||||
],
|
||||
orders: [
|
||||
{ id: 1, user_id: 2, total: 1329.98, status: 'shipped' },
|
||||
{ id: 2, user_id: 3, total: 89.99, status: 'processing' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Simulate vulnerable SQL query
|
||||
executeVulnerableQuery(searchTerm) {
|
||||
async executeVulnerableQuery(searchTerm, participantId, eventLessonId) {
|
||||
const db = this.getMockDatabase();
|
||||
|
||||
// Build the "vulnerable" query string for educational display
|
||||
@@ -43,29 +121,84 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
let results = [];
|
||||
let injectionType = null;
|
||||
let explanation = '';
|
||||
let pointsAwarded = 0;
|
||||
let isNewDiscovery = false;
|
||||
let unionHintMessage = null;
|
||||
|
||||
// Load activity data from database
|
||||
const activityData = await this._getActivityData(participantId, eventLessonId);
|
||||
const discoveries = new Set(activityData.discoveries);
|
||||
console.log(`[SQL Injection] Current discoveries:`, Array.from(discoveries));
|
||||
|
||||
if (injectionDetected) {
|
||||
const injectionInfo = this.analyzeInjection(searchTerm);
|
||||
injectionType = injectionInfo.type;
|
||||
explanation = injectionInfo.explanation;
|
||||
console.log(`[SQL Injection] Injection detected: ${injectionType}`);
|
||||
|
||||
// Check if this is a new discovery
|
||||
isNewDiscovery = !discoveries.has(injectionType);
|
||||
console.log(`[SQL Injection] Is new discovery: ${isNewDiscovery}`);
|
||||
|
||||
// Award points only for NEW discoveries
|
||||
if (isNewDiscovery && participantId && eventLessonId) {
|
||||
console.log(`[SQL Injection] Awarding points for new discovery: ${injectionType}`);
|
||||
const pointsMap = {
|
||||
'GENERIC': 30, // Challenge 1: Discovering injection is possible
|
||||
'BYPASS_FILTER': 40, // Challenge 2: Showing all products
|
||||
'UNION_SELECT': 80 // Challenge 3: Extracting user data (Easter egg!)
|
||||
};
|
||||
|
||||
pointsAwarded = pointsMap[injectionType] || 0;
|
||||
|
||||
// Time bonus
|
||||
const elapsedTime = await this._getElapsedTime(participantId, eventLessonId);
|
||||
if (elapsedTime > 0 && elapsedTime < 600) {
|
||||
const timeBonus = Math.max(0, Math.floor((600 - elapsedTime) / 60));
|
||||
if (timeBonus > 0) {
|
||||
pointsAwarded += timeBonus;
|
||||
explanation += ` 🎯 Zeit-Bonus: +${timeBonus} Punkte!`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
|
||||
`SQL Injection discovered: ${injectionType}`);
|
||||
|
||||
// Mark as discovered and save to database
|
||||
discoveries.add(injectionType);
|
||||
activityData.discoveries = Array.from(discoveries);
|
||||
await this._saveActivityData(participantId, eventLessonId, activityData);
|
||||
|
||||
// Show UNION hint after completing challenge 2
|
||||
if (injectionType === 'BYPASS_FILTER' && !activityData.unionHintShown) {
|
||||
activityData.unionHintShown = true;
|
||||
await this._saveActivityData(participantId, eventLessonId, activityData);
|
||||
|
||||
unionHintMessage = {
|
||||
title: '🎯 Neue Herausforderung freigeschaltet!',
|
||||
content: 'Du kannst jetzt versuchen, Daten aus anderen Tabellen zu extrahieren! Die Datenbank hat eine "users" Tabelle mit den Spalten: id, username, password, role. Verwende UNION SELECT um diese Daten zu kombinieren. Die Anzahl der Spalten muss übereinstimmen (5 Spalten).',
|
||||
hint: "Versuche: ' UNION SELECT id, username, password, role, 'X' FROM users--"
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to award SQL injection points:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate different injection results
|
||||
if (injectionInfo.type === 'OR_ALWAYS_TRUE') {
|
||||
// Return all products (simulating OR '1'='1')
|
||||
if (injectionInfo.type === 'BYPASS_FILTER') {
|
||||
// Return all products
|
||||
results = db.products;
|
||||
} else if (injectionInfo.type === 'UNION_SELECT') {
|
||||
// Simulate UNION attack showing user data
|
||||
results = [
|
||||
{ id: 'INJECTED', name: 'admin', price: 'hashed_admin_password', category: 'LEAKED DATA', stock: 'admin' },
|
||||
{ id: 'INJECTED', name: 'john_doe', price: 'hashed_user_password', category: 'LEAKED DATA', stock: 'customer' },
|
||||
{ id: 'INJECTED', name: 'jane_smith', price: 'hashed_user_password', category: 'LEAKED DATA', stock: 'customer' }
|
||||
{ id: 'USER', name: 'admin', price: 'hashed_admin_password', category: 'admin', stock: 'LEAKED!' },
|
||||
{ id: 'USER', name: 'john_doe', price: 'hashed_user_password', category: 'customer', stock: 'LEAKED!' },
|
||||
{ id: 'USER', name: 'jane_smith', price: 'hashed_user_password', category: 'customer', stock: 'LEAKED!' }
|
||||
];
|
||||
} else if (injectionInfo.type === 'DROP_TABLE') {
|
||||
// Simulate destructive command
|
||||
results = [];
|
||||
explanation += ' In a real scenario, this could delete the entire products table!';
|
||||
} else if (injectionInfo.type === 'COMMENT_INJECTION') {
|
||||
// Bypass rest of query
|
||||
} else if (injectionInfo.type === 'GENERIC') {
|
||||
// Generic injection - show it affects the query
|
||||
results = db.products;
|
||||
}
|
||||
} else {
|
||||
@@ -75,13 +208,25 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
);
|
||||
}
|
||||
|
||||
const totalDiscoveries = 3; // Only 3 challenges now
|
||||
const elapsedTime = await this._getElapsedTime(participantId, eventLessonId);
|
||||
|
||||
return {
|
||||
query: vulnerableQuery,
|
||||
results,
|
||||
injectionDetected,
|
||||
injectionType,
|
||||
explanation,
|
||||
recordCount: results.length
|
||||
recordCount: results.length,
|
||||
pointsAwarded: isNewDiscovery ? pointsAwarded : 0,
|
||||
isNewDiscovery,
|
||||
unionHintMessage,
|
||||
discoveries: {
|
||||
found: discoveries.size,
|
||||
total: totalDiscoveries,
|
||||
types: Array.from(discoveries)
|
||||
},
|
||||
elapsedTime
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,14 +234,8 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
detectInjection(input) {
|
||||
const injectionPatterns = [
|
||||
/'/, // Single quote
|
||||
/--/, // SQL comment
|
||||
/;/, // Statement separator
|
||||
/union/i, // UNION keyword
|
||||
/select/i, // SELECT keyword
|
||||
/drop/i, // DROP keyword
|
||||
/insert/i, // INSERT keyword
|
||||
/update/i, // UPDATE keyword
|
||||
/delete/i, // DELETE keyword
|
||||
/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/i // OR 1=1 pattern
|
||||
];
|
||||
|
||||
@@ -107,47 +246,37 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
analyzeInjection(input) {
|
||||
const lowerInput = input.toLowerCase();
|
||||
|
||||
if (lowerInput.includes('union') && lowerInput.includes('select')) {
|
||||
// Challenge 3: UNION SELECT (most advanced)
|
||||
// Must include UNION SELECT FROM users/user to be valid
|
||||
if (lowerInput.includes('union') && lowerInput.includes('select') &&
|
||||
lowerInput.includes('from') && (lowerInput.includes('users') || lowerInput.includes('user'))) {
|
||||
return {
|
||||
type: 'UNION_SELECT',
|
||||
explanation: '⚠️ UNION SELECT injection detected! This technique combines results from multiple tables, potentially exposing sensitive data like usernames and passwords.'
|
||||
};
|
||||
}
|
||||
|
||||
if (lowerInput.includes('drop')) {
|
||||
return {
|
||||
type: 'DROP_TABLE',
|
||||
explanation: '🚨 DROP TABLE injection detected! This is a destructive attack that could delete entire database tables. Critical data loss would occur!'
|
||||
explanation: '🎉 Perfekt! UNION SELECT Injection erfolgreich! Du hast Daten aus der users-Tabelle extrahiert. **Challenge 3 abgeschlossen!** ⭐'
|
||||
};
|
||||
}
|
||||
|
||||
// Challenge 2: Bypass filter to show all products
|
||||
if (lowerInput.includes("'") && (lowerInput.includes('or') || lowerInput.includes('||'))) {
|
||||
if (lowerInput.match(/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/)) {
|
||||
return {
|
||||
type: 'OR_ALWAYS_TRUE',
|
||||
explanation: "⚠️ OR injection detected! The condition '1'='1' is always true, bypassing the intended filter and returning ALL records."
|
||||
type: 'BYPASS_FILTER',
|
||||
explanation: "✅ Super! Die Bedingung '1'='1' ist immer wahr und umgeht den Filter. Jetzt werden ALLE Produkte angezeigt. **Challenge 2 abgeschlossen!**"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (lowerInput.includes('--') || lowerInput.includes('#')) {
|
||||
// Challenge 1: Generic injection - just discovered manipulation is possible
|
||||
if (lowerInput.includes("'")) {
|
||||
return {
|
||||
type: 'COMMENT_INJECTION',
|
||||
explanation: '⚠️ Comment injection detected! The -- sequence comments out the rest of the SQL query, potentially bypassing security checks.'
|
||||
type: 'GENERIC',
|
||||
explanation: "🔓 Gut gemacht! Das Anführungszeichen (') zeigt, dass die Abfrage manipuliert werden kann. Du hast entdeckt, dass SQL Injection möglich ist! **Challenge 1 abgeschlossen!**"
|
||||
};
|
||||
}
|
||||
|
||||
if (lowerInput.includes(';')) {
|
||||
return {
|
||||
type: 'MULTIPLE_STATEMENTS',
|
||||
explanation: '⚠️ Multiple statement injection detected! The semicolon allows execution of additional SQL commands, enabling complex attacks.'
|
||||
};
|
||||
}
|
||||
|
||||
// Generic injection
|
||||
return {
|
||||
type: 'GENERIC',
|
||||
explanation: '⚠️ SQL injection attempt detected! Special characters in the input could manipulate the query structure.'
|
||||
type: 'NONE',
|
||||
explanation: 'Keine SQL Injection erkannt.'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,11 +284,9 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
executeSafeQuery(searchTerm) {
|
||||
const db = this.getMockDatabase();
|
||||
|
||||
// Show the safe query with placeholder
|
||||
const safeQuery = `SELECT * FROM products WHERE name LIKE ?`;
|
||||
const parameter = `%${searchTerm}%`;
|
||||
|
||||
// Execute safe search (treats all input as literal data)
|
||||
const results = db.products.filter(p =>
|
||||
p.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
@@ -168,41 +295,79 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
query: safeQuery,
|
||||
parameter,
|
||||
results,
|
||||
explanation: '✅ Parameterized query used! User input is treated as data only, never as SQL code. Injection is impossible.',
|
||||
explanation: '✅ Parameterized query verwendet! Benutzereingaben werden als Daten behandelt, nie als SQL-Code. Injection ist unmöglich.',
|
||||
recordCount: results.length
|
||||
};
|
||||
}
|
||||
|
||||
// Get interactive data for the SQL shop demo
|
||||
getInteractiveData(stepId) {
|
||||
async getInteractiveData(stepId) {
|
||||
if (stepId === 'shop-demo') {
|
||||
return {
|
||||
timerDuration: 600, // 10 minutes
|
||||
totalChallenges: 3, // Simplified to 3 challenges
|
||||
database: this.getMockDatabase(),
|
||||
examples: [
|
||||
challenges: [
|
||||
{
|
||||
label: 'Normal Search',
|
||||
input: 'laptop',
|
||||
description: 'Search for products containing "laptop"'
|
||||
id: 'GENERIC',
|
||||
difficulty: 'Anfänger',
|
||||
points: 30,
|
||||
title: 'SQL Injection entdecken',
|
||||
hint: "Versuche ein ' (Anführungszeichen) einzugeben"
|
||||
},
|
||||
{
|
||||
label: 'View All Products (OR injection)',
|
||||
input: "' OR '1'='1",
|
||||
description: 'Exploit: Returns all products by making condition always true'
|
||||
id: 'BYPASS_FILTER',
|
||||
difficulty: 'Anfänger',
|
||||
points: 40,
|
||||
title: 'Filter umgehen (alle Produkte zeigen)',
|
||||
hint: "Verwende: ' OR '1'='1"
|
||||
},
|
||||
{
|
||||
label: 'Extract User Data (UNION)',
|
||||
input: "' UNION SELECT id, username, password, role, 'LEAKED' FROM users--",
|
||||
description: 'Exploit: Combines product results with user table data'
|
||||
},
|
||||
{
|
||||
label: 'Destructive Attack (DROP)',
|
||||
input: "'; DROP TABLE products--",
|
||||
description: 'Exploit: Attempts to delete the products table'
|
||||
id: 'UNION_SELECT',
|
||||
difficulty: 'Fortgeschritten',
|
||||
points: 80,
|
||||
title: 'Benutzerdaten extrahieren',
|
||||
hint: 'Wird nach Challenge 2 freigeschaltet',
|
||||
isEasterEgg: true
|
||||
}
|
||||
]
|
||||
],
|
||||
externalResources: [
|
||||
{
|
||||
title: 'OWASP SQL Injection',
|
||||
url: 'https://owasp.org/www-community/attacks/SQL_Injection',
|
||||
type: 'documentation',
|
||||
description: 'Grundlagen zu SQL Injection-Angriffen'
|
||||
},
|
||||
{
|
||||
title: 'SQL Tutorial (W3Schools)',
|
||||
url: 'https://www.w3schools.com/sql/',
|
||||
type: 'tutorial',
|
||||
description: 'SQL Grundlagen lernen'
|
||||
},
|
||||
{
|
||||
title: 'SQL Injection Cheat Sheet',
|
||||
url: 'https://portswigger.net/web-security/sql-injection/cheat-sheet',
|
||||
type: 'reference',
|
||||
description: 'Schnellreferenz für SQL Injection'
|
||||
}
|
||||
],
|
||||
schemaInfo: {
|
||||
tables: ['products', 'users'],
|
||||
productsColumns: ['id', 'name', 'price', 'category', 'stock'],
|
||||
usersColumns: ['id', 'username', 'password', 'role']
|
||||
},
|
||||
initialHint: {
|
||||
title: '💡 Einstiegshilfe',
|
||||
content: 'Du hast erfahren, dass dieser Shop anfällig für SQL Injection ist. Beginne mit einem einfachen Test: Gib ein Anführungszeichen (\') ein und beobachte was passiert. Dann versuche den Filter zu umgehen.',
|
||||
examples: [
|
||||
{ label: "Challenge 1", payload: "'", description: "Entdecke die Schwachstelle" },
|
||||
{ label: "Challenge 2", payload: "' OR '1'='1", description: "Zeige alle Produkte (Filter umgehen)" }
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
return await super.getInteractiveData(stepId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user