cyberhub-pracenv 1.0.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.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/cyberhub.js +12 -0
- package/data/challenges/crypto-101.yml +16 -0
- package/data/challenges/files/crypto-101/message.txt +5 -0
- package/data/challenges/files/forensics-101/access.log +7 -0
- package/data/challenges/files/web-101/cookies.txt +11 -0
- package/data/challenges/forensics-101.yml +14 -0
- package/data/challenges/web-101.yml +16 -0
- package/package.json +37 -0
- package/src/app.js +85 -0
- package/src/auth.js +42 -0
- package/src/commands.js +442 -0
- package/src/input.js +59 -0
- package/src/repl.js +71 -0
- package/src/store.js +337 -0
- package/src/ui.js +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yoitzmochi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# CyberHub Practice Environment (`cyberhub-pracenv`)
|
|
2
|
+
|
|
3
|
+
A beginner-friendly, **fully terminal-based** cybersecurity (CTF) practice
|
|
4
|
+
platform in the ICOA terminal style. Everything happens inside one interactive
|
|
5
|
+
shell: browse challenges, read prompts, download attached files, and submit
|
|
6
|
+
flags — no web browser, no separate commands.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g cyberhub-pracenv@latest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or install from a local clone:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git clone <this-repo> cyberhub-pracenv
|
|
18
|
+
cd cyberhub-pracenv
|
|
19
|
+
npm install
|
|
20
|
+
npm install -g .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Run
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cyberhub-pracenv # or the short alias: chpe
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You'll see a welcome screen — **press Enter** to drop into the platform shell.
|
|
30
|
+
Type `help` at any time to see every command.
|
|
31
|
+
|
|
32
|
+
## What you can do
|
|
33
|
+
|
|
34
|
+
| Command | Description |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `challenges` (`ls`) | List all available challenges |
|
|
37
|
+
| `open <id>` | View a challenge: prompt, points, attached files |
|
|
38
|
+
| `files <id>` | List files attached to a challenge |
|
|
39
|
+
| `cat <id> <file>` | Print an attached file's contents |
|
|
40
|
+
| `get <id> <file>` | Copy an attached file into your launch directory |
|
|
41
|
+
| `submit <id> <flag>` | Submit a flag to solve a challenge |
|
|
42
|
+
| `stats` (`whoami`) | Your full user breakdown (points, rank, progress) |
|
|
43
|
+
| `admin` | Unlock admin mode (asks for the admin password) |
|
|
44
|
+
| `help` | Show all commands |
|
|
45
|
+
| `exit` (`quit`) | Leave the platform |
|
|
46
|
+
|
|
47
|
+
### Admin (admin mode only)
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `upload` | Wizard that creates a new challenge `.yml` |
|
|
52
|
+
| `addfile <id> <path>` | Attach a file from disk to an existing challenge |
|
|
53
|
+
| `rmchallenge <id>` | Delete a challenge and its files |
|
|
54
|
+
| `passwd` | Change the admin password |
|
|
55
|
+
| `logout` | Leave admin mode |
|
|
56
|
+
|
|
57
|
+
The **default admin password is `admin`** — change it with `passwd` after your
|
|
58
|
+
first login.
|
|
59
|
+
|
|
60
|
+
## Challenge format
|
|
61
|
+
|
|
62
|
+
Challenges are plain `.yml` files (created by the `upload` wizard or by hand):
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
id: web-101
|
|
66
|
+
title: Cookie Monster
|
|
67
|
+
category: web
|
|
68
|
+
difficulty: easy
|
|
69
|
+
points: 100
|
|
70
|
+
description: |
|
|
71
|
+
Multi-line prompt describing the challenge.
|
|
72
|
+
flag: CHPE{example_flag}
|
|
73
|
+
files:
|
|
74
|
+
- cookies.txt
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Where data lives
|
|
78
|
+
|
|
79
|
+
All state is local to your machine, under:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
~/.cyberhub-pracenv/
|
|
83
|
+
config.json # admin password hash + settings
|
|
84
|
+
profile.json # your local profile (points, solved challenges)
|
|
85
|
+
challenges/<id>.yml # challenge definitions
|
|
86
|
+
files/<id>/<file> # files attached to challenges
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Three sample challenges (`web-101`, `crypto-101`, `forensics-101`) are seeded on
|
|
90
|
+
first run.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install
|
|
96
|
+
npm test # runs the node:test suite
|
|
97
|
+
npm start # run without a global install
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/bin/cyberhub.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Entry point for the `cyberhub-pracenv` (alias `chpe`) global command.
|
|
5
|
+
// Keep this thin: delegate everything to src/app.js so the binary stays a launcher.
|
|
6
|
+
|
|
7
|
+
const { main } = require('../src/app');
|
|
8
|
+
|
|
9
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
10
|
+
console.error('Fatal error:', err && err.stack ? err.stack : err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
id: crypto-101
|
|
2
|
+
title: Caesar's Secret
|
|
3
|
+
category: crypto
|
|
4
|
+
difficulty: easy
|
|
5
|
+
points: 100
|
|
6
|
+
description: |
|
|
7
|
+
Intercepted transmission, encrypted with a classic rotation cipher.
|
|
8
|
+
|
|
9
|
+
The attached file (message.txt) contains ciphertext that has been shifted by
|
|
10
|
+
a fixed number of letters (a Caesar cipher). Recover the plaintext to reveal
|
|
11
|
+
the flag.
|
|
12
|
+
|
|
13
|
+
Hint: there are only 25 shifts to try.
|
|
14
|
+
flag: CHPE{rotation_is_not_encryption}
|
|
15
|
+
files:
|
|
16
|
+
- message.txt
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
10.0.0.5 - - [24/Jun/2026:09:11:02 +0000] "GET /index.html HTTP/1.1" 200 1043
|
|
2
|
+
10.0.0.5 - - [24/Jun/2026:09:11:03 +0000] "GET /style.css HTTP/1.1" 200 512
|
|
3
|
+
10.0.0.9 - - [24/Jun/2026:09:12:44 +0000] "POST /login HTTP/1.1" 302 0
|
|
4
|
+
10.0.0.9 - - [24/Jun/2026:09:12:45 +0000] "GET /dashboard HTTP/1.1" 200 8841
|
|
5
|
+
192.168.1.7 - - [24/Jun/2026:09:13:10 +0000] "GET /search?q=CHPE{grep_is_your_friend} HTTP/1.1" 200 233
|
|
6
|
+
10.0.0.5 - - [24/Jun/2026:09:13:55 +0000] "GET /favicon.ico HTTP/1.1" 404 199
|
|
7
|
+
10.0.0.12 - - [24/Jun/2026:09:14:21 +0000] "GET /robots.txt HTTP/1.1" 200 64
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
HTTP/1.1 302 Found
|
|
2
|
+
Date: Tue, 24 Jun 2026 14:03:11 GMT
|
|
3
|
+
Server: nginx/1.24.0
|
|
4
|
+
Set-Cookie: session=8f14e45fceea167a5a36dedd4bea2543; Path=/; HttpOnly
|
|
5
|
+
Set-Cookie: role=Z3Vlc3Q=; Path=/
|
|
6
|
+
Set-Cookie: theme=dark; Path=/
|
|
7
|
+
Location: /login?error=1
|
|
8
|
+
|
|
9
|
+
# Captured during a separate session, an admin response also set:
|
|
10
|
+
# Set-Cookie: role=Q0hQRXtjMDBraWVzX2FyZV9ub3RfZm9yX2F1dGh9; Path=/
|
|
11
|
+
# The "role" cookie is just base64. Decode the admin value to get the flag.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
id: forensics-101
|
|
2
|
+
title: Hidden in Plain Sight
|
|
3
|
+
category: forensics
|
|
4
|
+
difficulty: easy
|
|
5
|
+
points: 150
|
|
6
|
+
description: |
|
|
7
|
+
Sometimes the flag is right there if you know where to look.
|
|
8
|
+
|
|
9
|
+
The attached log file (access.log) looks like an ordinary web server access
|
|
10
|
+
log, but a flag has been smuggled into one of the request lines. Scan through
|
|
11
|
+
it carefully — or use your favourite search tool — to find the CHPE{...} flag.
|
|
12
|
+
flag: CHPE{grep_is_your_friend}
|
|
13
|
+
files:
|
|
14
|
+
- access.log
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
id: web-101
|
|
2
|
+
title: Cookie Monster
|
|
3
|
+
category: web
|
|
4
|
+
difficulty: easy
|
|
5
|
+
points: 100
|
|
6
|
+
description: |
|
|
7
|
+
A login page trusts its own cookies a little too much.
|
|
8
|
+
|
|
9
|
+
The attached capture (cookies.txt) shows the HTTP response headers from the
|
|
10
|
+
site after a failed login. One of the cookies looks like it controls whether
|
|
11
|
+
you are an admin. Decode it and figure out the value the server expects.
|
|
12
|
+
|
|
13
|
+
The flag is the decoded admin cookie value, wrapped as CHPE{...}.
|
|
14
|
+
flag: CHPE{c00kies_are_not_for_auth}
|
|
15
|
+
files:
|
|
16
|
+
- cookies.txt
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cyberhub-pracenv",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A beginner-friendly, fully terminal-based cybersecurity (CTF) practice platform in the ICOA terminal style.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cybersecurity",
|
|
7
|
+
"ctf",
|
|
8
|
+
"cli",
|
|
9
|
+
"terminal",
|
|
10
|
+
"practice",
|
|
11
|
+
"education",
|
|
12
|
+
"icoa"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"cyberhub-pracenv": "bin/cyberhub.js",
|
|
20
|
+
"chpe": "bin/cyberhub.js"
|
|
21
|
+
},
|
|
22
|
+
"main": "src/app.js",
|
|
23
|
+
"files": [
|
|
24
|
+
"bin/",
|
|
25
|
+
"src/",
|
|
26
|
+
"data/",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"start": "node bin/cyberhub.js",
|
|
31
|
+
"test": "node --test"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"chalk": "^4.1.2",
|
|
35
|
+
"js-yaml": "^4.1.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Top-level orchestration: handle non-interactive flags, initialise the data
|
|
4
|
+
// directory, show the welcome screen (exit by pressing Enter), then drop the
|
|
5
|
+
// user into the in-platform shell.
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const pkg = require('../package.json');
|
|
9
|
+
const store = require('./store');
|
|
10
|
+
const ui = require('./ui');
|
|
11
|
+
const { createInput } = require('./input');
|
|
12
|
+
const { startRepl } = require('./repl');
|
|
13
|
+
const { c } = ui;
|
|
14
|
+
|
|
15
|
+
function printVersion() {
|
|
16
|
+
process.stdout.write(`${pkg.name} v${pkg.version}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function printHelp() {
|
|
20
|
+
process.stdout.write(
|
|
21
|
+
[
|
|
22
|
+
`${pkg.name} v${pkg.version}`,
|
|
23
|
+
pkg.description,
|
|
24
|
+
'',
|
|
25
|
+
'Usage:',
|
|
26
|
+
' cyberhub-pracenv Launch the interactive platform',
|
|
27
|
+
' cyberhub-pracenv --help Show this help',
|
|
28
|
+
' cyberhub-pracenv --version',
|
|
29
|
+
'',
|
|
30
|
+
'Once inside, type `help` to see the in-platform commands.',
|
|
31
|
+
`Default admin password: "${store.DEFAULT_ADMIN_PASSWORD}" (change it with \`passwd\`).`,
|
|
32
|
+
'',
|
|
33
|
+
].join('\n')
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build the shared readline interface and install the echo-muting hook used by
|
|
38
|
+
// the masked password prompt.
|
|
39
|
+
function createInterface() {
|
|
40
|
+
const rl = readline.createInterface({
|
|
41
|
+
input: process.stdin,
|
|
42
|
+
output: process.stdout,
|
|
43
|
+
// Terminal mode only when attached to a real TTY; piping input (tests,
|
|
44
|
+
// scripts) needs standard line mode so every line is dispatched.
|
|
45
|
+
terminal: Boolean(process.stdin.isTTY),
|
|
46
|
+
});
|
|
47
|
+
rl.stdoutMuted = false;
|
|
48
|
+
rl._writeToOutput = function _writeToOutput(str) {
|
|
49
|
+
if (rl.stdoutMuted) return; // suppress echo during password entry
|
|
50
|
+
rl.output.write(str);
|
|
51
|
+
};
|
|
52
|
+
return rl;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main(argv = []) {
|
|
56
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
57
|
+
printVersion();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
61
|
+
printHelp();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Capture the directory the platform was launched from so `get`/`addfile`
|
|
66
|
+
// resolve relative paths against it.
|
|
67
|
+
const launchCwd = process.cwd();
|
|
68
|
+
|
|
69
|
+
store.init();
|
|
70
|
+
|
|
71
|
+
const rl = createInterface();
|
|
72
|
+
const input = createInput(rl);
|
|
73
|
+
|
|
74
|
+
// Welcome screen: any Enter proceeds.
|
|
75
|
+
process.stdout.write('\x1b[2J\x1b[H'); // clear screen
|
|
76
|
+
process.stdout.write(ui.welcomeScreen() + '\n');
|
|
77
|
+
await input.ask('');
|
|
78
|
+
|
|
79
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
80
|
+
process.stdout.write(c.title('CyberHub Practice Environment') + '\n\n');
|
|
81
|
+
|
|
82
|
+
await startRepl(rl, input, launchCwd);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { main };
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Admin authentication: scrypt-based password hashing plus a tiny in-memory
|
|
4
|
+
// session flag. The platform has a single local profile; "admin" is a mode you
|
|
5
|
+
// unlock for the current session by entering the admin password.
|
|
6
|
+
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
const KEYLEN = 64;
|
|
10
|
+
|
|
11
|
+
function hashPassword(password, salt) {
|
|
12
|
+
const useSalt = salt || crypto.randomBytes(16).toString('hex');
|
|
13
|
+
const hash = crypto.scryptSync(String(password), useSalt, KEYLEN).toString('hex');
|
|
14
|
+
return { hash, salt: useSalt };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function verifyPassword(password, expectedHash, salt) {
|
|
18
|
+
if (!expectedHash || !salt) return false;
|
|
19
|
+
const { hash } = hashPassword(password, salt);
|
|
20
|
+
const a = Buffer.from(hash, 'hex');
|
|
21
|
+
const b = Buffer.from(expectedHash, 'hex');
|
|
22
|
+
if (a.length !== b.length) return false;
|
|
23
|
+
return crypto.timingSafeEqual(a, b);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Per-session admin state.
|
|
27
|
+
let adminUnlocked = false;
|
|
28
|
+
|
|
29
|
+
function isAdmin() {
|
|
30
|
+
return adminUnlocked;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setAdmin(value) {
|
|
34
|
+
adminUnlocked = Boolean(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
hashPassword,
|
|
39
|
+
verifyPassword,
|
|
40
|
+
isAdmin,
|
|
41
|
+
setAdmin,
|
|
42
|
+
};
|