VolgaCTF 2020 was a pleasant surprise to me as a fan of web application challenges. College and work had taken away my free time to do CTF’s, so coming into these challenges I was extremely rusty. Thankfully it was engaging enough for me to stick around for the whole duration of the CTF and I enjoyed “Library” in particular. As always with my writeups, i’ll try to not just provide the payloads you need, but the logic behind them and how the vulnerabilities were discovered. The challenge started off a bit rough on Friday - people were bruteforcing the website so much, they caused a denial of service and it would refuse connections for half of the first day. Me being the dumbass that I am, thought it was an error that’s part of the challenge, so I spent an unhealthy number of hours trying to get a hold of the token to authenticate. This writeup is intended for beginners, which I myself am, so if you don’t want a detailed explanation of steps, this writeup isn’t for you.
/js/ directory. Lets enumerate the directory to make sure we don’t miss any files.
WFuzz is perfect for it, run it with the following options:
wfuzz –hc 404 -c -u http://library.q.2020.volgactf.ru:7781/js/FUZZ.js -w /usr/share/wordlists/wfuzz/general/big.txt
- --hc 404 hides the responses with code 404,
- -c makes it so that the output is color coded, which is always nice when working with the terminal
- -u specifies the URL with the /js directory. "FUZZ" is where WFuzz will insert the payloads
- -w option specifies the wordlist
Next, make sure you don’t miss any directories and run:
If you don’t specify the wordlist, it will run with /usr/share/wordlists/dirb/common.txt, which will suffice. The output tells us that there aren’t any interesting directories aside from /api.
Logging in and reading common.js tells us that this website uses a JSON Web Token. JWT consists of three parts: header.payload.signature. Header and payload are base64 encoded and the signature is created by combining them with a shared secret through algorithm HS256. JS sourcecode tells us there’s a function jwtDecode and if you call it in the console, you can peer into the token. Spoiler alert: there’s nothing of interest. IAT is when it was issued and EXP is when it expires. To forge our own token we would need to know the secret and knowing that it’s not openly exposed anywhere in the code, there’s only one option left - bruteforcing it. While bruteforcing HS256 with a weak secret is possible and there’s even several terminal utilities available on GitHub that do just that, it’s not what the challenge is about. Don’t bruteforce it, i already tried, lmao.
GraphQL is a query language for API’s. A quick google search and a skim through the documentation informs us that GraphQL has a feature to query information about itself and it has a fitting name - introspection.
The main thing you need to know about GraphQL structure is the schema. The schema is exactly what it sounds like - it’s a schematic of the API objects, queries, mutations and types. You can get the schema by making a POST to API with this query parameter:
It will return a list of all types. Thankfully you don’t need extensive knowledge of GraphQL to get the rest of the information using introspection, because swissky on GitHub has published his payloads that you can use. If you use his query to dump the database schema and beautify the JSON, you will find that there is a hidden query that isn’t listed anywhere in the code. It’s testGetUsersByFilter. From the schema we can see that it takes a filter INPUT_OBJECT called UserFilter and it queries the User LIST OBJECT.
Another type of introspection query you can run in GraphQL is __type. It lets you query objects for their fields with the following payload:
GraphQL replies and tells you that the User object has 3 fields: login, name, email. We now have all we need to do the testGetUsersByFilter query.
Make sure to have a valid token in your Authorization header, as this query requires it. If we use the login query as a template and just write a similar one for testGetUsersByFilter, we get a payload like this:
This query will drop all the logins, names and emails from the User list object that match the string we provide. In this case the string is “admin” and the list returns all the hundreds of fuzz strings other CTF participants have inserted into the name field with the text “admin”. Dropping all the users is cool and all, but it doesn’t give us the flag.
Earlier in our fuzzing of the login query we managed to have it spit out a SQL error. Subsequent SQL injection testing on the login query and the registration mutation(function) shows that even though we may produce SQL errors, none of the parameters are injectible. We may peer from those error messages that the single quote ‘ character is escaped with two \ backslashes. API calls in production environments are often well protected and the low hanging fruit that should be tested immediately is debug and test functions. We do have a hidden query that is only meant to be called for testing purposes and that’s the one we will try and fuzz for SQL injection. Remember that the schema told us there are 3 fields in the User object that the testGetUsersByFilter query can interact with. Previously we only passed the name parameter, but for fuzzing purposes we should test all 3.
Remember how the single quote was escaped with two backslashes? Try that on the 3 parameters. If you do, you’ll notice that a double backslash will return
when in the login parameter. Inserting SQL code after the backslashes into the login parameter doesn’t seem to return any records, so it’s worthwhile to try and pass it in a different parameter. If we pass
the database will return all user names, emails and logins. The output is the same as the last step, but this time we are talking directly to the database, which means we can tell it to spit out other things too.
Last step requires a little bit of guesswork. We get a database error when trying to fish records out of other tables. If we try to specify the fields to extract from the users table, it produces an error. One guess you could make is that the error is caused by the number of fields we request. You can test that hypothesis by making requests like this until you hit something:
If you ask for exactly 6 fields, the database will not issue an error and will respond correctly. You can now enumerate the database and get information about it by passing commands to it, as long as you ask for 6 items.
The code above will yield you the name of the current database user, the database name and its version. You can drop the table schema of the database by requesting the table_name from information_schema.tables like so:
At the very end of the returned list we will see the three main tables this database uses: books, users and flag. Of course our next destination should be the flag table and if we make a request for the flag field like so:
we’ll get what we’re after.