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
|
|
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
|
|
Inspecting the bash script at /usr/bin/cronjob_bandit22.sh
|
|
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:
|
|
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
:
|
|
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:
|
|
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.
|
|
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
|
|
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
.
|
|
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 (?)
|
|
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
|
|
The page has a Refresh page
link to click on. Clicking on that, the page rendered will have a different message:
|
|
We are now at the /index.php
page.
Clicking the button once more will display a different message
|
|
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 withxor
with a hard-coded key in thexor_encrypt
function. The result of the encryption (as performed by thesaveData
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 localdata
variable. The data will be of the same structure as that ofdefaultdata
. - If the
showpassword
field of thedata
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
Sending this using the in-app browser from Burp Suite, and change the POST endpoint to /index.php
. The request will be something like
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.gusername
,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.
|
|
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.
|
|
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:
|
|
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:
|
|
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 :)