Tags: web 

Rating:

# Flag Command

Embark on the "Dimensional Escape Quest" where you wake up in a mysterious forest maze that's not quite of this world. Navigate singing squirrels, mischievous nymphs, and grumpy wizards in a whimsical labyrinth that may lead to otherworldly surprises. Will you conquer the enchanted maze or find yourself lost in a different dimension of magical challenges? The journey unfolds in this mystical escape!

## Writeup

The main site contain this script:

```html

<html lang="en">

<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Flag Command</title>
<link rel="stylesheet" href="/static/terminal/css/terminal.css" />
<link rel="stylesheet" href="/static/terminal/css/commands.css" />
</head>

<body style="color: #94ffaa !important; position: fixed; height: 100vh; overflow: scroll; font-size: 28px;font-weight: 700;">
<div id="terminal-container" style="overflow: auto;height: 90%;">

</div>
<div id="command">
<textarea id="user-text-input" autofocus></textarea>
<div id="current-command-line">
<span></span>
</div>
</div>
<audio id="typing-sound" src="/static/terminal/audio/typing_sound.mp3" preload="auto"></audio>
<script src="/static/terminal/js/commands.js" type="module"></script>
<script src="/static/terminal/js/main.js" type="module"></script>
<script src="/static/terminal/js/game.js" type="module"></script>

<script type="module">
import { startCommander, enterKey, userTextInput } from "/static/terminal/js/main.js";
startCommander();

window.addEventListener("keyup", enterKey);

// event listener for clicking on the terminal
document.addEventListener("click", function () {
userTextInput.focus();
});

</script>
</body>

</html>
```

So we can look at **main.js**:

```
/static/terminal/js/main.js
```

We can found this code:

```js
import { START, INFO, INITIAL_OPTIONS, HELP } from "./commands.js";
import { playerLost, playerWon } from "./game.js";

let availableOptions;

let currentStep = 1;
// SELECT HTML ELEMENTS
// ---------------------------------------
export const beforeDiv = document.getElementById("before-div"),
currentCommandLine = document.getElementById("current-command-line"),
commandText = document.getElementById("commad-written-text"),
userTextInput = document.getElementById("user-text-input");

const typingSound = new Audio();
typingSound.src = document.getElementById("typing-sound").src;
typingSound.loop = true;

// COMMANDER VARIABLES
// ---------------------------------------
let currentCommand = 0,
commandHistory = [],
typingSpeed = 10,
typing = true,
playAudio = true,
fetchingResponse = false,
gameStarted = false,
gameEnded = false;

export const startCommander = async () => {
await fetchOptions();
userTextInput.value = "";
commandText.innerHTML = userTextInput.value;

await displayLinesInTerminal({ lines: INFO });

userTextInput.focus();
};

// HTTP REQUESTS
// ---------------------------------------
async function CheckMessage() {
fetchingResponse = true;
currentCommand = commandHistory[commandHistory.length - 1];

if (availableOptions[currentStep].includes(currentCommand) || availableOptions['secret'].includes(currentCommand)) {
await fetch('/api/monitor', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ 'command': currentCommand })
})
.then((res) => res.json())
.then(async (data) => {
console.log(data)
await displayLineInTerminal({ text: data.message });

if(data.message.includes('Game over')) {
playerLost();
fetchingResponse = false;
return;
}

if(data.message.includes('HTB{')) {
playerWon();
fetchingResponse = false;

return;
}

if (currentCommand == 'HEAD NORTH') {
currentStep = '2';
}
else if (currentCommand == 'FOLLOW A MYSTERIOUS PATH') {
currentStep = '3'
}
else if (currentCommand == 'SET UP CAMP') {
currentStep = '4'
}

let lineBreak = document.createElement("br");

beforeDiv.parentNode.insertBefore(lineBreak, beforeDiv);
displayLineInTerminal({ text: '<span>You have 4 options!</span>' })
displayLinesInTerminal({ lines: availableOptions[currentStep] })
fetchingResponse = false;
});

}
else {
displayLineInTerminal({ text: "You do realise its not a park where you can just play around and move around pick from options how are hard it is for you????" });
fetchingResponse = false;
}
}

// TEXT FUNCTIONS
// ---------------------------------------
const typeText = async (element, text) => {
if (playAudio && typingSound.paused) {
typingSound.play();
}

for (let i = 0; i < text.length; i++) {
if (text.charAt(i) === " " && text.charAt(i + 1) === " ") {
element.innerHTML += "  ";
i++;
} else {
element.innerHTML += text.charAt(i);
}
await new Promise((resolve) => setTimeout(resolve, typingSpeed));
}

if (playAudio) {
typingSound.pause();
typingSound.currentTime = 0;
}
};

const createNewLineElement = ({ style = "", addPadding = false }) => {
// remove the current command line until new line is displayed
currentCommandLine.classList.remove("visible");
currentCommandLine.style.opacity = 0;

const nextLine = document.createElement("p");

// add style depending on the type of line
nextLine.className = style + (addPadding ? " spaced-line" : "");

beforeDiv.parentNode.insertBefore(nextLine, beforeDiv);
window.scrollTo(0, document.body.offsetHeight);

return nextLine;
};

// process remaining text with styled and unstyled parts and apply typing effect
const processTextWithTypingEffect = async (nextLine, text) => {
let remainingText = text;

// process remaining text with styled and unstyled parts
while (remainingText) {
const styledElementMatch = remainingText.match(/<(\w+)(?:\s+class=['"]([^'"]*)['"])?>([^<]*)<\/\1>/);
const unstyledText = styledElementMatch ? remainingText.slice(0, styledElementMatch.index) : remainingText;

// handle unstyled text
if (unstyledText) {
await typeText(nextLine, unstyledText);
}

// handle styled text
if (styledElementMatch) {
const [, tagName, className, innerText] = styledElementMatch;
const styledElement = document.createElement(tagName);
if (className) {
styledElement.className = className;
}
nextLine.appendChild(styledElement);
await typeText(styledElement, innerText);
remainingText = remainingText.slice(styledElementMatch.index + styledElementMatch[0].length);
} else {
remainingText = null;
}
}
};

// display a line in the terminal with optional styling and typing effect
export const displayLineInTerminal = async ({ text = "", style = "", useTypingEffect = true, addPadding = false }) => {
typing = true;

// create and style a new line element
const nextLine = createNewLineElement({ style, addPadding });

// use typing effect if enabled
await processTextWithTypingEffect(nextLine, text);

// reset typing flag and make the current command line visible
typing = false;
currentCommandLine.style.opacity = 1;
currentCommandLine.classList.add("visible");
};

// display multiple lines in the terminal with optional styling and typing effect
export const displayLinesInTerminal = async ({ lines, style = "", useTypingEffect = true }) => {
for (let i = 0; i < lines.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 0));

await displayLineInTerminal({ text: lines[i], style: style });
}
};

// EVENT LISTENERS
// ---------------------------------------
// user input keydown event listener
const keyBindings = {
Enter: () => {
// if a response is being fetched, do nothing on Enter
if (fetchingResponse) {
return;
} else {
commandHistory.push(commandText.innerHTML);
currentCommand = commandHistory.length;
displayLineInTerminal({ text: `>> ${commandText.innerHTML}`, useTypingEffect: true, addPadding: true });
commander(commandText.innerHTML.toLowerCase());
commandText.innerHTML = "";
userTextInput.value = "";
}
},

ArrowUp: () => {
if (currentCommand > 0) {
currentCommand -= 1;
commandText.innerHTML = commandHistory[currentCommand];
userTextInput.value = commandHistory[currentCommand];
}
},

ArrowDown: () => {
if (currentCommand < commandHistory.length) {
currentCommand += 1;
if (commandHistory[currentCommand] === undefined) {
userTextInput.value = "";
} else {
userTextInput.value = commandHistory[currentCommand];
}
commandText.innerHTML = userTextInput.value;
}
},
};

// available user commands
export const commandBindings = {
help: () => {
displayLinesInTerminal({ lines: HELP });
},

start: async () => {
await displayLineInTerminal({ text: START });
let lineBreak = document.createElement("br");

beforeDiv.parentNode.insertBefore(lineBreak, beforeDiv);
await displayLinesInTerminal({ lines: INITIAL_OPTIONS });
gameStarted = true;
},
clear: () => {
while (beforeDiv.previousSibling) {
beforeDiv.previousSibling.remove();
}
},

audio: () => {
if (playAudio) {
playAudio = false;
displayLineInTerminal({ text: "Audio turned off" });
} else {
playAudio = true;
displayLineInTerminal({ text: "Audio turned on" });
}
},

restart: () => {
let count = 6;

function updateCounter() {
count--;

if (count <= 0) {
clearInterval(counter);
return location.reload();
}

displayLineInTerminal({
text: `Game will restart in ${count}...`,
style: status,
useTypingEffect: true,
addPadding: false,
});
}

// execute the code block immediately before starting the interval
updateCounter();
currentStep = 1

let counter = setInterval(updateCounter, 1000);
},

info: () => {
displayLinesInTerminal({ lines: INFO });
},
};

// keyup event listener
export const enterKey = (event) => {
if (!typing) {
if (event.key in keyBindings) {
keyBindings[event.key]();
event.preventDefault();
} else {
commandText.innerHTML = userTextInput.value;
}
}
};

// command handler
const commander = (commandText) => {
const cleanCommand = commandText.toLowerCase().trim();

// Possible states:
// 1. game has not started (gameStarted = false)
// 2. game is in progress (gameStarted = true, gameEnded = false)
// 3. game has ended (gameStarted = true, gameEnded = true)

if (cleanCommand in commandBindings) {
if (!gameStarted) {
// game has not started
commandBindings[cleanCommand]();
} else if (gameStarted && !gameEnded) {
// game is in progress
commandBindings[cleanCommand]();
} else {
// game has ended
if (cleanCommand === "restart" || cleanCommand !== "start") {
commandBindings[cleanCommand]();
} else {
displayEndGameMessage();
}
}
} else {
if (gameStarted && !gameEnded) {
CheckMessage();
} else if (gameEnded) {
displayEndGameMessage();
} else {
displayLineInTerminal({
text: `'${cleanCommand}' command not found. For a list of commands, type '<span>help</span>'`,
useTypingEffect: true,
});
}
}
};

const displayEndGameMessage = () => {
displayLineInTerminal({
text: "The game has ended. Please type <span>restart</span> to start a new game or <span>help</span> for a list of commands.",
useTypingEffect: true,
});
};

const fetchOptions = () => {
fetch('/api/options')
.then((data) => data.json())
.then((res) => {
availableOptions = res.allPossibleCommands;

})
.catch(() => {
availableOptions = undefined;
})
}
```

We can send the following answers:

```
HEAD NORTH
FOLLOW A MYSTERIOUS PATH
SET UP CAMP
```

The final answer can be found at:

```
/api/options
```

And is:

```
Blip-blop, in a pickle with a hiccup! Shmiggity-shmack
```

The flag is:

```
HTB{D3v3l0p3r_t00l5_4r3_b35t_wh4t_y0u_Th1nk??!}
```

Original writeup (https://github.com/MicheleMosca/CTF/blob/main/Cyber%20Apocalypse%202024/web/Flag%20Command/README.md).