├── stalkerlab ├── note_to_read.txt ├── come_closer │ └── monolith │ │ ├── flag │ │ ├── owner.enc.txt │ │ └── note.txt │ │ └── f4ng │ │ └── key.txt ├── monolith.db ├── requirements.txt ├── Dockerfile ├── templates │ ├── index.html │ ├── monolith_login.html │ ├── mentor_panel.html │ ├── admin_search.html │ ├── chat_new.html │ └── chat.html ├── static │ └── css │ │ └── style.css ├── admin_bot.py └── app.py ├── compose.yaml └── README.md /stalkerlab/note_to_read.txt: -------------------------------------------------------------------------------- 1 | *Come closer... You're so close to the solution...* -------------------------------------------------------------------------------- /stalkerlab/come_closer/monolith/flag/owner.enc.txt: -------------------------------------------------------------------------------- 1 | REUbHF8WRygMBlESAAtwNwVDUzcFBU1hEw== -------------------------------------------------------------------------------- /stalkerlab/monolith.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morronel/stalker-lab/HEAD/stalkerlab/monolith.db -------------------------------------------------------------------------------- /stalkerlab/come_closer/monolith/flag/note.txt: -------------------------------------------------------------------------------- 1 | *As you approach the monolith, you hear xorus of voices...* -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: 4 | context: stalkerlab 5 | dockerfile: ./Dockerfile 6 | ports: 7 | - 5000:5000 8 | -------------------------------------------------------------------------------- /stalkerlab/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | Werkzeug==2.3.7 3 | Jinja2==3.1.2 4 | SQLAlchemy==2.0.20 5 | Flask-SQLAlchemy==3.0.5 6 | requests==2.31.0 7 | selenium==4.15.2 8 | -------------------------------------------------------------------------------- /stalkerlab/come_closer/monolith/f4ng/key.txt: -------------------------------------------------------------------------------- 1 | Listen up, we'll have only one chance. Decryptor worked like a charm... th..e.e. ke..y...i.s... 2 | 3 | 71zp4s5wor7i5b4S51cAl1yUncrackable 4 | 5 | one last step, and w...e'll kn..ow w...h.o's be....hi..nd this... 6 | 7 | meet me... at chef... 8 | 9 | The Chef... -------------------------------------------------------------------------------- /stalkerlab/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 python:3.11-slim 2 | 3 | # Install Chrome and Chrome WebDriver dependencies 4 | RUN apt-get update && apt-get install -y \ 5 | wget \ 6 | gnupg \ 7 | unzip \ 8 | curl \ 9 | fonts-liberation \ 10 | libasound2 \ 11 | libatk-bridge2.0-0 \ 12 | libatk1.0-0 \ 13 | libatspi2.0-0 \ 14 | libcups2 \ 15 | libdbus-1-3 \ 16 | libdrm2 \ 17 | libgbm1 \ 18 | libgtk-3-0 \ 19 | libnspr4 \ 20 | libnss3 \ 21 | libxcomposite1 \ 22 | libxdamage1 \ 23 | libxfixes3 \ 24 | libxrandr2 \ 25 | xdg-utils \ 26 | && mkdir -p /etc/apt/sources.list.d/ \ 27 | && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 28 | && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \ 29 | && apt-get update \ 30 | && apt-get install -y google-chrome-stable \ 31 | && apt-mark hold google-chrome-stable \ 32 | && apt-get clean \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | # Get Chrome version and install matching ChromeDriver 36 | RUN chrome_version=$(google-chrome --version | awk '{ print $3 }' | cut -d'.' -f1) \ 37 | && wget -q -O /tmp/chromedriver.zip https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$(curl -s https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_$chrome_version)/linux64/chromedriver-linux64.zip \ 38 | && unzip /tmp/chromedriver.zip -d /tmp/ \ 39 | && mv /tmp/chromedriver-linux64/chromedriver /usr/local/bin/ \ 40 | && rm -rf /tmp/chromedriver* \ 41 | && chmod +x /usr/local/bin/chromedriver 42 | 43 | # Set working directory 44 | WORKDIR /app 45 | 46 | # Copy requirements and install dependencies 47 | COPY requirements.txt . 48 | RUN pip install --no-cache-dir -r requirements.txt 49 | 50 | # Copy application files 51 | COPY . . 52 | 53 | # Modify admin_bot.py to use localhost instead of 127.0.0.1 54 | RUN sed -i 's/127.0.0.1/localhost/g' admin_bot.py 55 | 56 | # Create a startup script 57 | RUN echo '#!/bin/bash\n\ 58 | python app.py & \n\ 59 | sleep 5\n\ 60 | export PYTHONUNBUFFERED=1\n\ 61 | python admin_bot.py & \n\ 62 | wait' > /app/start.sh && chmod +x /app/start.sh 63 | 64 | # Expose port 65 | EXPOSE 5000 66 | 67 | # Run the startup script 68 | CMD ["/app/start.sh"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STALKER: Monolith's Web Challenge 2 | 3 | ## Difficulty: "Stalker" 4 | 5 | A FREE and OPEN SOURCE web-based CTF (Capture The Flag) challenge inspired by the S.T.A.L.K.E.R. game series atmosphere. This project is a fan creation and is not affiliated with or endorsed by GSC Game World or the official S.T.A.L.K.E.R. team. 6 | 7 | ## Description 8 | 9 | Welcome to the Zone, stalker! This web challenge will test your hacking skills in a S.T.A.L.K.E.R.-themed environment. You were given a strange Monolith tablet, and now you have to trace down it's rising leader. Navigate through the mysterious Monolith's web presence and uncover its secrets. 10 | 11 | ## Installation 12 | 13 | ### Prerequisites 14 | - Docker 15 | 16 | ### Quick Start 17 | 1. Clone the repository: 18 | ```bash 19 | git clone https://github.com/Morronel/stalker-lab.git 20 | cd stalker-lab 21 | ``` 22 | 23 | 2. Build and run the container: 24 | ```bash 25 | sudo docker compose up 26 | ``` 27 | 28 | 3. Access the challenge at: 29 | ``` 30 | http://127.0.0.1:5000 31 | ``` 32 | 33 | Flag is in stalker_ctf{FLAG} format. Good luck, stalker! 34 | 35 | ## License 36 | 37 | This project is released under the MIT License. See the LICENSE file for details. 38 | 39 | ## Check Out My Telegram Channel 40 | 41 | https://t.me/binary_xor 42 | 43 | ## Known Issues 44 | 45 | Sometimes user interaction on chat step may fail. In such case reboot of container often solves the issue 46 | 47 | ## Disclaimer 48 | 49 | This is a fan-made CTF challenge inspired by the S.T.A.L.K.E.R. series. All S.T.A.L.K.E.R.-related trademarks and copyrights are property of their respective owners. This project is created for educational purposes only. 50 | 51 | ## Credit 52 | 53 | Thank you Olex Vel (https://x.com/alex_roqo) for testing the challenge and bringing in his ideas. 54 | Thank you Bogdan Shchogolev for testing the challenge and contributing handy docker compose launcher. 55 | And huge shoutout to GSC. Thanks you, for finally releasing Stalker2 (if you have a friend who works at GSC, share this repo with them plz). 56 | Thanks guys, I appreciate it :) 57 | 58 | ## Screenshots 59 | 60 | ![image](https://github.com/user-attachments/assets/705185f6-5c71-4c58-b357-6ef5b6c0e8e5) 61 | 62 | ![image](https://github.com/user-attachments/assets/93cb8b66-d240-459c-ba3d-3e56e3b9f472) 63 | 64 | ![image](https://github.com/user-attachments/assets/bb1b16cd-ba46-460c-8c30-f08f99c28311) 65 | -------------------------------------------------------------------------------- /stalkerlab/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | STALKER Terminal Interface 7 | 8 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |

МОНОЛІТ ТЕРМІНАЛ v1.0.4

37 |
38 |
39 | 40 |
41 | [СТАТУС: АКТИВНИЙ] [РАДІАЦІЯ: ВИСОКА] [АНОМАЛІЇ: ВИЯВЛЕНО] 42 |
43 | 44 |
45 |
УВАГА: ДОСТУП ОБМЕЖЕНО
46 |

Виявлено неавторизований доступ до терміналу Моноліту...

47 |

Виконується сканування периметру...

48 | 49 | 50 | 51 |
52 | В ДОСТУПІ ВІДМОВЛЕНО! 53 |
54 |
55 | 56 | 60 |
61 |
62 | 63 | -------------------------------------------------------------------------------- /stalkerlab/templates/monolith_login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | МОНОЛІТ - Термінал Доступу 7 | 8 | 53 | 54 | 55 |
56 |
57 |
58 |
59 |
60 |

ТЕРМІНАЛ ДОСТУПУ МОНОЛІТУ

61 |
62 |
63 | 64 |
65 | [РІВЕНЬ ДОСТУПУ: НЕВІДОМИЙ] [ОЧІКУВАННЯ АВТЕНТИФІКАЦІЇ] 66 |
67 | 68 |
69 |
۞
70 |
СИСТЕМА АВТЕНТИФІКАЦІЇ МОНОЛІТУ
71 | 72 | {% with messages = get_flashed_messages(with_categories=true) %} 73 | {% if messages %} 74 | {% for category, message in messages %} 75 |
76 | [ПОМИЛКА] {{ message }} 77 |
78 | {% endfor %} 79 | {% endif %} 80 | {% endwith %} 81 | 82 | 87 | 88 |

Очікування введення облікових даних...

89 |
90 | 91 | 95 |
96 |
97 | 98 | -------------------------------------------------------------------------------- /stalkerlab/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); 2 | 3 | :root { 4 | --terminal-green: #00ff00; 5 | --terminal-dark: #0a0a0a; 6 | --monolith-gray: #4a4a4a; 7 | } 8 | 9 | * { 10 | margin: 0; 11 | padding: 0; 12 | box-sizing: border-box; 13 | } 14 | 15 | body { 16 | background-color: black; 17 | color: var(--terminal-green); 18 | font-family: 'VT323', monospace; 19 | height: 100vh; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | overflow: hidden; 24 | } 25 | 26 | .terminal { 27 | background-color: var(--terminal-dark); 28 | width: 90%; 29 | max-width: 800px; 30 | height: 80vh; 31 | border: 2px solid var(--monolith-gray); 32 | box-shadow: 0 0 20px rgba(0, 255, 0, 0.2); 33 | position: relative; 34 | overflow: hidden; 35 | } 36 | 37 | .scanlines { 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 100%; 43 | background: linear-gradient( 44 | to bottom, 45 | rgba(255, 255, 255, 0.03) 50%, 46 | rgba(0, 0, 0, 0.03) 50% 47 | ); 48 | background-size: 100% 4px; 49 | pointer-events: none; 50 | } 51 | 52 | .content { 53 | padding: 20px; 54 | height: 100%; 55 | overflow-y: auto; 56 | } 57 | 58 | .header { 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | margin-bottom: 20px; 63 | gap: 20px; 64 | } 65 | 66 | .warning-sign { 67 | font-size: 2em; 68 | color: #ff0000; 69 | animation: pulse 2s infinite; 70 | } 71 | 72 | .status-bar { 73 | border-top: 1px solid var(--monolith-gray); 74 | border-bottom: 1px solid var(--monolith-gray); 75 | padding: 10px 0; 76 | margin-bottom: 20px; 77 | text-align: center; 78 | } 79 | 80 | .main-content { 81 | margin: 20px 0; 82 | } 83 | 84 | .glitch-text { 85 | font-size: 1.5em; 86 | margin-bottom: 20px; 87 | animation: glitch 1s infinite; 88 | } 89 | 90 | .typewriter { 91 | overflow: hidden; 92 | border-right: 2px solid var(--terminal-green); 93 | white-space: nowrap; 94 | margin: 0 auto; 95 | animation: typing 3.5s steps(40, end), blink-caret 0.75s step-end infinite; 96 | } 97 | 98 | .login-prompt { 99 | margin-top: 40px; 100 | } 101 | 102 | .input-line { 103 | display: flex; 104 | gap: 10px; 105 | align-items: center; 106 | } 107 | 108 | .prompt { 109 | color: var(--terminal-green); 110 | } 111 | 112 | .cursor { 113 | animation: blink 1s infinite; 114 | } 115 | 116 | .footer { 117 | position: absolute; 118 | bottom: 20px; 119 | width: calc(100% - 40px); 120 | text-align: center; 121 | border-top: 1px solid var(--monolith-gray); 122 | padding-top: 10px; 123 | } 124 | 125 | @keyframes pulse { 126 | 0% { opacity: 1; } 127 | 50% { opacity: 0.5; } 128 | 100% { opacity: 1; } 129 | } 130 | 131 | @keyframes glitch { 132 | 0% { transform: translate(0); } 133 | 20% { transform: translate(-2px, 2px); } 134 | 40% { transform: translate(-2px, -2px); } 135 | 60% { transform: translate(2px, 2px); } 136 | 80% { transform: translate(2px, -2px); } 137 | 100% { transform: translate(0); } 138 | } 139 | 140 | @keyframes typing { 141 | from { width: 0 } 142 | to { width: 100% } 143 | } 144 | 145 | @keyframes blink { 146 | 0%, 100% { opacity: 1; } 147 | 50% { opacity: 0; } 148 | } 149 | 150 | @keyframes blink-caret { 151 | from, to { border-color: transparent } 152 | 50% { border-color: var(--terminal-green); } 153 | } 154 | 155 | /* Add some radiation effect to the background */ 156 | .terminal::before { 157 | content: ''; 158 | position: absolute; 159 | top: 0; 160 | left: 0; 161 | width: 100%; 162 | height: 100%; 163 | background: radial-gradient(circle at 50% 50%, rgba(0, 255, 0, 0.1), transparent); 164 | pointer-events: none; 165 | animation: radiation 4s infinite; 166 | } 167 | 168 | @keyframes radiation { 169 | 0% { opacity: 0.3; } 170 | 50% { opacity: 0.7; } 171 | 100% { opacity: 0.3; } 172 | } -------------------------------------------------------------------------------- /stalkerlab/templates/mentor_panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | МОНОЛІТ - Панель Наставника 7 | 8 | 78 | 79 | 80 |
81 |
82 |
83 |
84 |
85 |

ПАНЕЛЬ НАСТАВНИКА

86 |
87 |
88 | 89 |
90 | [АДМІНІСТРАТОР: {{ username }}] [РІВЕНЬ ДОСТУПУ: ПОВНИЙ] [РЕЖИМ: МОНІТОРИНГ] 91 |
92 | 93 |
94 | [!] АКТИВНИЙ МОНІТОРИНГ ПОВІДОМЛЕНЬ [!] 95 |
96 | 97 |
98 | 99 |
100 |
101 |
102 | 103 | 134 | 135 | -------------------------------------------------------------------------------- /stalkerlab/templates/admin_search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | МОНОЛІТ - Пошук Сталкерів 7 | 8 | 112 | 113 | 114 |
115 |
116 |
117 | 121 | 122 |
123 |
124 |

ПОШУК СТАЛКЕРІВ

125 |
126 |
127 | 128 |
129 | [АДМІНІСТРАТОР: {{ username }}] [РІВЕНЬ ДОСТУПУ: ПОВНИЙ] [РЕЖИМ: ПОШУК] 130 |
131 | 132 |
133 |
134 | 135 | 136 |
137 | 138 | {{ search_result | safe }} 139 |
140 |
141 |
142 | 143 | -------------------------------------------------------------------------------- /stalkerlab/templates/chat_new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | МОНОЛІТ - Чат з Наставником 7 | 8 | 85 | 86 | 87 |
88 |
89 |
90 |
91 |
92 |

ЧАТ З НАСТАВНИКОМ

93 |
94 |
95 | 96 |
97 | [КОРИСТУВАЧ: {{ username }}] [СТАТУС: АКТИВНИЙ] [КАНАЛ ЗВ'ЯЗКУ: ЗАХИЩЕНИЙ] 98 |
99 | 100 |
101 | 102 |
103 | 104 |
105 | 106 |
107 | 108 |
109 | 110 | 111 |
112 |
113 |
114 | 115 | 181 | 182 | -------------------------------------------------------------------------------- /stalkerlab/admin_bot.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.chrome.options import Options 3 | from selenium.webdriver.common.by import By 4 | from selenium.webdriver.support.ui import WebDriverWait 5 | from selenium.webdriver.support import expected_conditions as EC 6 | import time 7 | import sqlite3 8 | import json 9 | import os 10 | 11 | class MessageTracker: 12 | def __init__(self): 13 | self.seen_message_ids = set() 14 | self.last_message_id = 0 15 | self.initialize_seen_messages() 16 | 17 | def initialize_seen_messages(self): 18 | """Initialize with existing messages""" 19 | try: 20 | conn = sqlite3.connect('monolith.db') 21 | c = conn.cursor() 22 | # Get the highest message ID to track resets 23 | max_id = c.execute('SELECT MAX(id) FROM messages').fetchone()[0] 24 | self.last_message_id = max_id if max_id else 0 25 | 26 | # Only store recent message IDs 27 | messages = c.execute('SELECT id FROM messages ORDER BY id DESC LIMIT 100').fetchall() 28 | self.seen_message_ids = set(msg[0] for msg in messages) 29 | conn.close() 30 | except Exception as e: 31 | print(f"[!] Error initializing message tracker: {e}") 32 | 33 | def check_for_chat_reset(self): 34 | """Check if chat has been cleared by comparing message counts""" 35 | try: 36 | conn = sqlite3.connect('monolith.db') 37 | c = conn.cursor() 38 | 39 | # Get current highest message ID 40 | current_max_id = c.execute('SELECT MAX(id) FROM messages').fetchone()[0] 41 | current_max_id = current_max_id if current_max_id else 0 42 | 43 | # If current max ID is less than what we've seen, chat was cleared 44 | if current_max_id < self.last_message_id: 45 | print("[*] Chat reset detected! Reinitializing message tracker...") 46 | self.seen_message_ids.clear() 47 | self.initialize_seen_messages() 48 | 49 | conn.close() 50 | return current_max_id < self.last_message_id 51 | except Exception as e: 52 | print(f"[!] Error checking for chat reset: {e}") 53 | return False 54 | 55 | def has_new_messages(self): 56 | """Check for new messages by comparing with seen IDs""" 57 | try: 58 | # First check if chat was cleared 59 | if self.check_for_chat_reset(): 60 | return True 61 | 62 | conn = sqlite3.connect('monolith.db') 63 | c = conn.cursor() 64 | 65 | # Only check recent messages 66 | current_messages = c.execute('SELECT id FROM messages ORDER BY id DESC LIMIT 100').fetchall() 67 | current_ids = set(msg[0] for msg in current_messages) 68 | 69 | # Update last seen message ID 70 | if current_messages: 71 | self.last_message_id = max(current_ids) 72 | 73 | # Cleanup old message IDs (keep only recent ones) 74 | self.seen_message_ids = set(id for id in self.seen_message_ids 75 | if id > self.last_message_id - 100) 76 | 77 | # Check for new messages 78 | new_messages = current_ids - self.seen_message_ids 79 | if new_messages: 80 | self.seen_message_ids.update(current_ids) 81 | return True 82 | return False 83 | 84 | except Exception as e: 85 | print(f"[!] Error checking messages: {e}") 86 | return False 87 | finally: 88 | conn.close() 89 | 90 | def setup_driver(): 91 | chrome_options = Options() 92 | chrome_options.add_argument("--headless") 93 | chrome_options.add_argument("--no-sandbox") 94 | chrome_options.add_argument("--disable-dev-shm-usage") 95 | chrome_options.add_argument("--disable-gpu") 96 | return webdriver.Chrome(options=chrome_options) 97 | 98 | def admin_bot(): 99 | print("[*] Starting admin bot with headless Chrome...") 100 | driver = None 101 | tracker = MessageTracker() 102 | 103 | try: 104 | driver = setup_driver() 105 | 106 | print("[*] Performing initial login...") 107 | driver.get("http://localhost:5000/monolith") 108 | username = driver.find_element(By.NAME, "username") 109 | password = driver.find_element(By.NAME, "password") 110 | submit = driver.find_element(By.TAG_NAME, "button") 111 | 112 | username.send_keys("monolith_master") 113 | password.send_keys("super_secret_monolith_pw") 114 | submit.click() 115 | print("[*] Initial login successful") 116 | 117 | consecutive_errors = 0 118 | while True: 119 | try: 120 | if tracker.has_new_messages(): 121 | print("[*] New messages detected!") 122 | 123 | # Visit mentor panel 124 | print("[*] Visiting mentor panel...") 125 | driver.get("http://localhost:5000/monolith/mentor-panel") 126 | 127 | # Wait for messages to load 128 | WebDriverWait(driver, 10).until( 129 | EC.presence_of_element_located((By.ID, "chat")) 130 | ) 131 | 132 | # Stay on the page briefly to let XSS execute 133 | time.sleep(2) 134 | print("[*] Messages checked") 135 | consecutive_errors = 0 136 | 137 | # Adaptive sleep: increase delay if no activity 138 | time.sleep(5) 139 | 140 | except Exception as e: 141 | print(f"[!] Error in admin bot: {e}") 142 | consecutive_errors += 1 143 | 144 | if consecutive_errors >= 3: 145 | print("[!] Too many consecutive errors, restarting browser...") 146 | if driver: 147 | driver.quit() 148 | driver = setup_driver() 149 | consecutive_errors = 0 150 | 151 | # Re-login after browser restart 152 | try: 153 | driver.get("http://localhost:5000/monolith") 154 | username = driver.find_element(By.NAME, "username") 155 | password = driver.find_element(By.NAME, "password") 156 | submit = driver.find_element(By.TAG_NAME, "button") 157 | 158 | username.send_keys("monolith_master") 159 | password.send_keys("super_secret_monolith_pw") 160 | submit.click() 161 | print("[*] Re-login successful") 162 | except: 163 | print("[!] Re-login failed, will retry...") 164 | time.sleep(5) 165 | 166 | except KeyboardInterrupt: 167 | print("\n[*] Shutting down admin bot...") 168 | finally: 169 | if driver: 170 | driver.quit() 171 | 172 | if __name__ == "__main__": 173 | admin_bot() -------------------------------------------------------------------------------- /stalkerlab/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | МОНОЛІТ - Чат з Наставником 7 | 8 | 148 | 149 | 150 |
151 |
152 |
153 | {% if session.role == 'admin' %} 154 | 158 | {% endif %} 159 | 160 |
161 |
162 |

ЧАТ З НАСТАВНИКОМ

163 |
164 |
165 | 166 |
167 | [КОРИСТУВАЧ: {{ username }}] [СТАТУС: АКТИВНИЙ] [КАНАЛ ЗВ'ЯЗКУ: ЗАХИЩЕНИЙ] 168 |
169 | 170 |
171 | 172 |
173 | 174 |
175 | 176 |
177 | 178 |
179 | 180 | 181 |
182 |
183 |
184 | 185 | 251 | 252 | -------------------------------------------------------------------------------- /stalkerlab/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify 2 | from flask_sqlalchemy import SQLAlchemy 3 | import os 4 | import sqlite3 5 | import time 6 | import random 7 | 8 | app = Flask(__name__) 9 | app.secret_key = os.urandom(24) 10 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///monolith.db' 11 | app.config['SESSION_COOKIE_HTTPONLY'] = False # Making cookie accessible via JavaScript 12 | db = SQLAlchemy(app) 13 | 14 | # Add these new constants at the top of the file after imports 15 | MENTOR_RESPONSES = [ 16 | "Твої думки... вони резонують з енергією Моноліту. У самому серці Зони, де перетинаються шляхи, є відповіді...", 17 | "Зона змінює кожного, хто наважується увійти. Як вона змінила тебе? Можливо, ти вже відчуваєш поклик з центру...", 18 | "Монолiт бачить твою істинну сутність. Твої наміри чисті? Лабораторія XS-S чекає гідних...", 19 | "Кожен крок до Моноліту - це крок до просвітлення. Але чи готовий ти до істини, що схована в надрах наукової станції XS-S?", 20 | "Твої слова відлунюють у свідомості Зони. Розкажи більше... Можливо, ти вже бачив дивні знаки XS-S?", 21 | "Енергія Моноліту тече крізь тебе. Я відчуваю це в твоїх словах. Центр Зони приховує більше, ніж здається...", 22 | "Шлях до Моноліту різний для кожного сталкера. Твій шлях... особливий. Він веде через приховану лабораторію XS-S...", 23 | "У центрі Зони, де аномалії найгустіші, древні стіни бережуть секрети Моноліту. Чи наважишся ти увійти?", 24 | "Дивні сигнали линуть з серця Зони. Лабораторія XS-S кличе обраних. Чи чуєш ти її голос?", 25 | "Твоя присутність тут не випадкова. Моноліт направляє тих, хто гідний, до своїх найглибших таємниць...", 26 | "Старі лабораторні комплекси XS-S приховують шлях до істини. Знайди знаки, і двері відкриються...", 27 | "Серед радіації та аномалій, за стінами забутої лабораторії XS-S, Моноліт чекає на гідних...", 28 | "Твій шлях перетинається з древніми коридорами. Науковці XS-S знали більше, ніж розповіли світу...", 29 | "Моноліт говорить через символи та знаки. Шукай позначку XS-S, і ти знайдеш шлях до істини." 30 | ] 31 | 32 | STALKER_JOKES = [ 33 | "Якось найманці впіймали сталкера. Підійшли до колодязя, окунули його вниз головою до пояса.\nЧерез хвилину витягують і питають:\n— Артефакти, бабло Є?!\nВін їм:\n— Немає...\nЗнову опускають. Витягли, питають:\n— Артеффакти, бабло Є?!\n— Та немає!\nЗнову окунули. І знову питають:\n— Артеффакти, бабло Є?!\nНу, той не витримав:\n— Блін. Ви або ОПУСКАЙТЕ глибше, або ТРИМАЙТЕ довше. ДНО каламутне — нічорта не видно!", 34 | "Старий і молодий долговці ідуть Зоною. Раптом старий зупиняється і шепотом каже молодому:\n— Тихенько йди. Он до того дерева.\nМолодий на пальчиках, поповз, аж спітнів. Дійшов і руками показує: мовляв, далі що робити?.. А бувалий як зарепетує радісно:\n— Воооо! Я ж казав — БРЕШУТЬ! Брешуть, що тут аномалія!", 35 | "Стоїть якось сталкер на третьому перехресті та читає вказівник:\n«Направо — аномалії і ТРОХИ хабара. Вперед — монстрів чимало і СЕРЕДНЬО хабара. Наліво — кабаки, дівки і хабара ДОФІГА».\nНу, подумав-подумав і вперед рушив. Думає:\n— Чьот я про це чув... Та забув блін. Треба буде на Барі у друзів уточнити — що за фігня така?! кабаки й дівки.", 36 | "Вчить, значить, контролер сліпого пса всіляким штукам… На задніх лапах, там, ходити, мертвим удавати.\nРіч туго йде, а поруч зомбі стоїть і приколюється:\n— Нічого у тебе НЕ вийде, ФІГНЄЮ займаєшся...\nА контролер так пальцем йому погрозив і відповідає:\n— Чуєш, сталкере! Заткнись, ага. Ти мені тапочки приносити також не відразу навчився!", 37 | "Звалилася на занедбаному заводі цеглина з даху й прибила одного чувака. Народ зібрався, переймається:\n— Блін, монстрів і контролер��в і так вистачає... Та ще й цеглини з дахів летять — «Ото фігня», «не пощастило чуваку» ну й так далі.\nАле один бувалий придивився й каже:\n— Спокуха, мужики. Туди йому й дорога — це ж ЗОМБІ бродячий.\nА народ ще більше засмутився:\n— Блін, що ж за фігня така — зомбі розвелося, цеглині впасти ніде!" 38 | ] 39 | 40 | def get_mentor_response(message): 41 | # Check for joke keywords 42 | joke_keywords = ["анекдот", "anecdote", "joke", "жарт"] 43 | if any(keyword in message.lower() for keyword in joke_keywords): 44 | return random.choice(STALKER_JOKES) 45 | return random.choice(MENTOR_RESPONSES) 46 | 47 | # Initialize database 48 | def init_db(): 49 | conn = sqlite3.connect('monolith.db') 50 | c = conn.cursor() 51 | 52 | # Drop existing tables to ensure clean slate 53 | c.execute('DROP TABLE IF EXISTS messages') 54 | c.execute('DROP TABLE IF EXISTS users') 55 | 56 | # Create users table with intentionally simple structure for SQL injection 57 | c.execute(''' 58 | CREATE TABLE IF NOT EXISTS users ( 59 | id INTEGER PRIMARY KEY, 60 | username TEXT NOT NULL, 61 | password TEXT NOT NULL, 62 | role TEXT NOT NULL 63 | ) 64 | ''') 65 | 66 | # Create messages table with status field 67 | c.execute(''' 68 | CREATE TABLE IF NOT EXISTS messages ( 69 | id INTEGER PRIMARY KEY, 70 | sender TEXT NOT NULL, 71 | message TEXT NOT NULL, 72 | timestamp INTEGER NOT NULL, 73 | status TEXT DEFAULT 'sent' 74 | ) 75 | ''') 76 | 77 | # Add our test users - one low-priv and one admin 78 | c.execute('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', 79 | ('stalker_rookie', 'password123', 'user')) 80 | c.execute('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', 81 | ('monolith_master', 'super_secret_monolith_pw', 'admin')) 82 | 83 | # Add initial mentor message 84 | c.execute('INSERT INTO messages (sender, message, timestamp, status) VALUES (?, ?, ?, ?)', 85 | ('mentor', 'Вітаю тебе, шукачу істини. Я - голос Моноліту, твій провідник у темряві. Що привело твою душу до священного каменю?', int(time.time()), 'received')) 86 | 87 | conn.commit() 88 | conn.close() 89 | 90 | def clear_chat(): 91 | conn = sqlite3.connect('monolith.db') 92 | c = conn.cursor() 93 | c.execute('DELETE FROM messages') 94 | c.execute('INSERT INTO messages (sender, message, timestamp, status) VALUES (?, ?, ?, ?)', 95 | ('mentor', 'Вітаю тебе, шукачу істини. Я - голос Моноліту, твій провідник у темряві. Що привело твою душу до священного каменю?', int(time.time()) + 1, 'received')) 96 | conn.commit() 97 | conn.close() 98 | 99 | @app.route('/') 100 | def home(): 101 | return render_template('index.html') 102 | 103 | @app.route('/monolith', methods=['GET', 'POST']) 104 | def monolith_login(): 105 | print("Login attempt...") # Debug print 106 | if request.method == 'POST': 107 | username = request.form['username'] 108 | password = request.form['password'] 109 | print(f"Login attempt with username: {username}") # Debug print 110 | 111 | # Intentionally vulnerable SQL query 112 | conn = sqlite3.connect('monolith.db') 113 | c = conn.cursor() 114 | query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'" 115 | print(f"Executing query: {query}") # Debug print 116 | try: 117 | result = c.execute(query).fetchone() 118 | if result: 119 | print(f"Login successful, user data: {result}") # Debug print 120 | session['user_id'] = result[0] 121 | session['username'] = result[1] 122 | session['role'] = result[3] 123 | print(f"Session data set: {session}") # Debug print 124 | return redirect('/monolith/chat') # Direct URL instead of url_for 125 | else: 126 | print("No user found") # Debug print 127 | flash('Невірні облікові дані. Спробуйте ще раз.', 'error') 128 | except sqlite3.Error as e: 129 | print(f"SQL Error: {e}") # Debug print 130 | flash(f"SQL Error: {e}", 'error') # Debug print 131 | flash('Помилка автентифікації. Спробуйте ще раз.', 'error') 132 | finally: 133 | conn.close() 134 | 135 | return render_template('monolith_login.html') 136 | 137 | @app.route('/monolith/chat') 138 | def monolith_chat(): 139 | print(f"Chat route accessed. Session: {session}") # Debug print 140 | if 'user_id' not in session: 141 | print("No user_id in session, redirecting to login") # Debug print 142 | return redirect('/monolith') 143 | print(f"Rendering chat for user: {session['username']}") # Debug print 144 | return render_template('chat.html', username=session['username']) 145 | 146 | @app.route('/monolith/mentor-panel') 147 | def mentor_panel(): 148 | if 'user_id' not in session or session['role'] != 'admin': 149 | return redirect('/monolith') 150 | return render_template('mentor_panel.html', username=session['username']) 151 | 152 | @app.route('/api/messages', methods=['GET']) 153 | def get_messages(): 154 | if 'user_id' not in session: 155 | return jsonify({'error': 'Unauthorized'}), 401 156 | 157 | conn = sqlite3.connect('monolith.db') 158 | c = conn.cursor() 159 | messages = c.execute('SELECT * FROM messages ORDER BY timestamp DESC LIMIT 50').fetchall() 160 | conn.close() 161 | 162 | return jsonify([{ 163 | 'id': msg[0], 164 | 'sender': msg[1], 165 | 'message': msg[2], # Intentionally vulnerable to XSS 166 | 'timestamp': msg[3] 167 | } for msg in messages]) 168 | 169 | @app.route('/api/messages', methods=['POST']) 170 | def send_message(): 171 | if 'user_id' not in session: 172 | return jsonify({'error': 'Unauthorized'}), 401 173 | 174 | message = request.json.get('message', '').strip() 175 | if not message: 176 | return jsonify({'error': 'Message cannot be empty'}), 400 177 | 178 | timestamp = int(time.time()) 179 | conn = sqlite3.connect('monolith.db') 180 | c = conn.cursor() 181 | 182 | # Store user message 183 | c.execute('INSERT INTO messages (sender, message, timestamp) VALUES (?, ?, ?)', 184 | (session['username'], message, timestamp)) 185 | 186 | # Generate and store mentor response after a short delay 187 | time.sleep(1) # Small delay before mentor responds 188 | mentor_response = get_mentor_response(message) 189 | c.execute('INSERT INTO messages (sender, message, timestamp) VALUES (?, ?, ?)', 190 | ('mentor', mentor_response, int(time.time()))) 191 | 192 | conn.commit() 193 | conn.close() 194 | 195 | return jsonify({'status': 'success'}) 196 | 197 | @app.route('/api/messages//status', methods=['GET']) 198 | def get_message_status(message_id): 199 | if 'user_id' not in session: 200 | return jsonify({'error': 'Unauthorized'}), 401 201 | 202 | # Simulate status updates 203 | conn = sqlite3.connect('monolith.db') 204 | c = conn.cursor() 205 | message = c.execute('SELECT timestamp FROM messages WHERE id = ?', (message_id,)).fetchone() 206 | 207 | if not message: 208 | return jsonify({'error': 'Message not found'}), 404 209 | 210 | message_time = message[0] 211 | current_time = int(time.time()) 212 | time_diff = current_time - message_time 213 | 214 | status = 'sent' 215 | if time_diff >= 2: 216 | status = 'received' 217 | # Update message status in database 218 | c.execute('UPDATE messages SET status = ? WHERE id = ?', (status, message_id)) 219 | conn.commit() 220 | 221 | conn.close() 222 | return jsonify({'status': status}) 223 | 224 | @app.route('/api/typing-status', methods=['GET']) 225 | def get_typing_status(): 226 | if 'user_id' not in session: 227 | return jsonify({'error': 'Unauthorized'}), 401 228 | 229 | conn = sqlite3.connect('monolith.db') 230 | c = conn.cursor() 231 | latest_message = c.execute('SELECT timestamp, sender FROM messages ORDER BY timestamp DESC LIMIT 1').fetchone() 232 | conn.close() 233 | 234 | if not latest_message: 235 | return jsonify({'is_typing': False}) 236 | 237 | current_time = int(time.time()) 238 | time_diff = current_time - latest_message[0] 239 | 240 | # Show typing indicator 4-7 seconds after the last message if it's from user 241 | is_typing = time_diff >= 4 and time_diff <= 7 and latest_message[1] != 'mentor' 242 | 243 | return jsonify({'is_typing': is_typing}) 244 | 245 | @app.route('/api/mentor-response', methods=['POST']) 246 | def send_mentor_response(): 247 | if 'user_id' not in session: 248 | return jsonify({'error': 'Unauthorized'}), 401 249 | 250 | message_id = request.json.get('message_id') 251 | if not message_id: 252 | return jsonify({'error': 'Message ID required'}), 400 253 | 254 | conn = sqlite3.connect('monolith.db') 255 | c = conn.cursor() 256 | 257 | # Get the original message 258 | original_message = c.execute('SELECT message FROM messages WHERE id = ?', (message_id,)).fetchone() 259 | if not original_message: 260 | conn.close() 261 | return jsonify({'error': 'Original message not found'}), 404 262 | 263 | # Generate and store mentor response 264 | mentor_response = get_mentor_response(original_message[0]) 265 | timestamp = int(time.time()) 266 | c.execute('INSERT INTO messages (sender, message, timestamp, status) VALUES (?, ?, ?, ?)', 267 | ('mentor', mentor_response, timestamp, 'received')) 268 | response_id = c.lastrowid 269 | 270 | conn.commit() 271 | conn.close() 272 | 273 | return jsonify({ 274 | 'status': 'success', 275 | 'message_id': response_id, 276 | 'message': mentor_response, 277 | 'timestamp': timestamp 278 | }) 279 | 280 | @app.route('/api/clear-chat', methods=['POST']) 281 | def clear_chat_endpoint(): 282 | if 'user_id' not in session: 283 | return jsonify({'error': 'Unauthorized'}), 401 284 | clear_chat() 285 | return jsonify({'status': 'success'}) 286 | 287 | @app.route('/monolith/admin/search') 288 | def admin_search(): 289 | if 'user_id' not in session or session['role'] != 'admin': 290 | return redirect('/monolith') 291 | 292 | # Get search query 293 | query = request.args.get('query', '') 294 | 295 | # Intentionally vulnerable to SSTI by directly rendering user input 296 | if query: 297 | # Create a template string with the user's input and render it 298 | template = f''' 299 |
300 |

Результати пошуку для: {query}

301 |
302 | ''' 303 | from flask import render_template_string 304 | result = render_template_string(template) 305 | else: 306 | result = '
[!] Введіть ім\'я сталкера для пошуку...
' 307 | 308 | return render_template('admin_search.html', username=session['username'], search_result=result) 309 | 310 | if __name__ == '__main__': 311 | init_db() # Initialize database on startup 312 | app.run(debug=True, host='0.0.0.0', port=5000) --------------------------------------------------------------------------------