cd ~/writeups
OverTheWire Easy Linux Linux / Security101

OverTheWire — Bandit: Linux Security Fundamentals

The Bandit wargame is aimed at absolute beginners. It teaches the basics needed to play other wargames — SSH, file manipulation, piping, permissions, cron, git, and more. 33 levels, from zero to shell escape.

33 levels 2023-04-07

# level index

# level 0 — SSH login

Connect to the Bandit server and read the readme file in the home directory.

terminal bash
$ ssh bandit0@bandit.labs.overthewire.org -p 2220
bandit0@bandit:~$ cat readme
NH2SXQwcBdpmTEzi3bvBHMM9H66vVXjL

# level 1 — dashed filename

The filename is a single dash (-). Prefix with ./ to avoid it being interpreted as stdin.

terminal bash
bandit1@bandit:~$ cat ./-
rRGizSaX8Mk1RTb1CNQoXTcYZWU6lgzi

# level 2 — spaces in filename

Escape spaces with backslashes or wrap the filename in quotes.

terminal bash
bandit2@bandit:~$ cat ./spaces\ in\ this\ filename
aBZ0W5EmUfAf7kHTQeOwd8bauFJ2lAiG

# level 3 — hidden file

The file is hidden (dotfile) inside the inhere directory.

terminal bash
bandit3@bandit:~/inhere$ cat .hidden
2EW7BBsr6aMMoJ2HjW067dm8EgX26xNe

# level 4 — find the human-readable file

Multiple files in inhere/. Grep recursively for ASCII content.

terminal bash
bandit4@bandit:~/inhere$ grep -Ri [a-z]
-file07:lrIWWI6bB37kxfiCQZqUdOIYfr6eEeqR

# level 5 — find by file size

Find the file that is exactly 1033 bytes.

terminal bash
bandit5@bandit:~/inhere$ find ./ -size 1033c -exec "cat" {} +
P4L4vucdmLnm8I7Vl7jG1ApGSfjYKqJU

# level 6 — find by owner, group, size

Search the entire filesystem for a file owned by user bandit7, group bandit6, 33 bytes.

terminal bash
bandit6@bandit:~$ find / -group bandit6 -user bandit7 -size 33c -exec "cat" {} + 2>/dev/null
z7WtoNQU2XfjmMtWA8u5rN4vzqu4v99S

# level 7 — grep for keyword

The password is on the line containing the word "millionth" in data.txt.

terminal bash
bandit7@bandit:~$ cat data.txt | grep millionth
millionth       TESKZC0XvTetK0S9xNwm25STk5iWrBvP

# level 8 — unique line

Find the line that occurs only once in data.txt.

terminal bash
bandit8@bandit:~$ grep -vf <(sort data.txt | uniq -d) data.txt
EN632PlfYiZbn3PhVK3XOGSlNInNE00t

# level 9 — strings in binary

Extract human-readable strings and grep for lines starting with ==.

terminal bash
bandit9@bandit:~$ strings data.txt | grep ^==
========== password
========== is
========== G7w8LIi6J3kTb8A7j9LgrywtEUlyyp6s

# level 10 — base64 decode

terminal bash
bandit10@bandit:~$ cat data.txt | base64 -d
The password is 6zPeziLdR2RKNdNYFNb6nVCKzphlXHBM

# level 11 — ROT13

terminal bash
bandit11@bandit:~$ cat data.txt | rot13
The password is JVNBBFSmZwKKOP0XbFXOoW8chDz5yVRv

# level 12 — hexdump + nested compression

A hex dump that has been compressed multiple times. Reverse the hex dump, then repeatedly decompress (gzip, bzip2, tar) until you reach the ASCII password.

terminal bash
# reverse the hex dump
bandit12@bandit:/tmp/13$ cat data.txt | xxd -r > data
bandit12@bandit:/tmp/13$ file data
data: gzip compressed data, was "data2.bin"

# round 1 — gzip
bandit12@bandit:/tmp/13$ mv data data2.gz
bandit12@bandit:/tmp/13$ gunzip data2.gz
bandit12@bandit:/tmp/13$ file data2
data2: bzip2 compressed data, block size = 900k

# round 2 — bzip2
bandit12@bandit:/tmp/13$ mv data2 data3.bz
bandit12@bandit:/tmp/13$ bzip2 -d data3.bz
bandit12@bandit:/tmp/13$ file data3
data3: gzip compressed data, was "data4.bin"

# round 3 — gzip
bandit12@bandit:/tmp/13$ mv data3 data4.gz
bandit12@bandit:/tmp/13$ gunzip data4.gz
bandit12@bandit:/tmp/13$ file data4
data4: POSIX tar archive (GNU)

# round 4 — tar
bandit12@bandit:/tmp/13$ mv data4 data5.tar
bandit12@bandit:/tmp/13$ tar -xf data5.tar
bandit12@bandit:/tmp/13$ file data5.bin
data5.bin: POSIX tar archive (GNU)

# round 5 — tar
bandit12@bandit:/tmp/13$ mv data5.bin data6.tar
bandit12@bandit:/tmp/13$ tar -xf data6.tar
bandit12@bandit:/tmp/13$ file data6.bin
data6.bin: bzip2 compressed data, block size = 900k

# round 6 — bzip2
bandit12@bandit:/tmp/13$ mv data6.bin data6.bz
bandit12@bandit:/tmp/13$ bzip2 -d data6.bz
bandit12@bandit:/tmp/13$ file data6
data6: POSIX tar archive (GNU)

# round 7 — tar
bandit12@bandit:/tmp/13$ mv data6 data6.tar
bandit12@bandit:/tmp/13$ tar -xf data6.tar
bandit12@bandit:/tmp/13$ file data8.bin
data8.bin: gzip compressed data, was "data9.bin"

# round 8 — gzip (final)
bandit12@bandit:/tmp/13$ mv data8.bin data8.gz
bandit12@bandit:/tmp/13$ gunzip -d data8.gz
bandit12@bandit:/tmp/13$ file data8
data8: ASCII text
bandit12@bandit:/tmp/13$ cat data8
The password is wbWdlBxEir4CaE8LaPhauuOo6pwRmrDw

# level 13 — SSH private key

No password this time — use the private key found in the home directory to SSH as bandit14.

terminal bash
bandit13@bandit:~$ ls
sshkey.private

$ chmod 600 id_rsa
$ ssh -i ./.ssh/id_rsa bandit14@bandit.labs.overthewire.org -p 2220

# level 14 — netcat & localhost

Submit the current level's password to localhost port 30000 to receive the next password.

terminal bash
bandit14@bandit:/$ cat /etc/bandit_pass/bandit14
fGrHPx402xGC7U7rXKDaxiWFTOiF0ENq

bandit14@bandit:/$ nc localhost 30000
fGrHPx402xGC7U7rXKDaxiWFTOiF0ENq
Correct!
jN2kgmIXJ6fShzhT2avhotn4Zcka6tnt

# level 15 — OpenSSL

Same concept as level 14 but the connection must be over SSL.

terminal bash
bandit15@bandit:~$ openssl s_client -ign_eof -connect localhost:30001
---
read R BLOCK
jN2kgmIXJ6fShzhT2avhotn4Zcka6tnt
Correct!
JQttfApK4SeyHwDlI9SXGR50qclOAil1

# level 16 — nmap port scanning

Scan ports 31000–32000, identify the SSL service that is not an echo server, and submit the password. The correct port returns an RSA private key for the next level.

terminal bash
bandit16@bandit:~$ nmap -p 31000-32000 localhost
PORT      STATE SERVICE
31046/tcp open  unknown
31518/tcp open  unknown
31691/tcp open  unknown
31790/tcp open  unknown
31960/tcp open  unknown

bandit16@bandit:~$ nmap -A -p 31046,31518,31691,31790,31960 localhost
...
31790/tcp open  ssl/unknown
  → "Wrong! Please enter the correct current password"

# port 31790 is the target (not an echo server)
bandit16@bandit:~$ openssl s_client -connect localhost:31790 -ign_eof
read R BLOCK
JQttfApK4SeyHwDlI9SXGR50qclOAil1
Correct!
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAvmOkuifmMg6HL2YPIOjon6iWfbp7c3jx34YkYWqUH57SUdyJ
... (save as id_rsa, chmod 600, SSH as bandit17)
-----END RSA PRIVATE KEY-----

$ ssh -i ./.ssh/id_rsa bandit17@bandit.labs.overthewire.org -p 2220

# level 17 & 18 — diff & SSH command exec

Use diff to find the changed line between two password files. Level 18's .bashrc kicks you out on login, so execute commands directly via SSH.

terminal — bandit17 bash
bandit17@bandit:~$ diff passwords.new passwords.old
42c42
< hga5tuuCLF6fFzUpnagiMN8ssu9LFrdg
---
> f9wS9ZUDvZoo3PooHgYuuWdawDFvGld2
terminal — bandit18 (remote exec) bash
$ ssh bandit18@bandit.labs.overthewire.org -p 2220 "cat readme"
awhqfNnAbc1naukrpqDYcF95h7HoMTrC

# level 19 — SUID binary

A setuid binary runs commands as bandit20. Use it to read the password file.

terminal bash
bandit19@bandit:~$ ./bandit20-do whoami
bandit20
bandit19@bandit:~$ ./bandit20-do cat /etc/bandit_pass/bandit20
VxCazJaVykI6W36BkBU0mJTCM8rR95XT

# level 20 — netcat listener

The suconnect binary connects to a given port on localhost. If it receives the current password, it sends back the next one. Use two terminals — one running nc as a listener.

terminal 1 bash
bandit20@bandit:~$ ./suconnect 9093
Read: VxCazJaVykI6W36BkBU0mJTCM8rR95XT
Password matches, sending next password
terminal 2 bash
bandit20@bandit:~$ nc -nlvp 9093
Listening on 0.0.0.0 9093
Connection received on 127.0.0.1 53696
VxCazJaVykI6W36BkBU0mJTCM8rR95XT
NvEJF7oVjkddltPSrdKEFOllh9V1IBcq

# level 21 — cron job

A cron job writes the bandit22 password to a file in /tmp.

terminal bash
bandit21@bandit:~$ cat /etc/cron.d/cronjob_bandit22
@reboot bandit22 /usr/bin/cronjob_bandit22.sh &> /dev/null

bandit21@bandit:~$ cat /usr/bin/cronjob_bandit22.sh
#!/bin/bash
chmod 644 /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv
cat /etc/bandit_pass/bandit22 > /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv

bandit21@bandit:~$ cat /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv
WdDozAdTM2z9DiFEQ2mGlwngMfj4EZff

# level 22 — cron + md5 path

The cron script hashes "I am user $myname" with md5sum to derive the tmp path. Substitute bandit23 to find the correct file.

terminal bash
bandit22@bandit:~$ cat /usr/bin/cronjob_bandit23.sh
#!/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"
cat /etc/bandit_pass/$myname > /tmp/$mytarget

bandit22@bandit:~$ echo I am user bandit23 | md5sum | cut -d ' ' -f 1
8ca319486bfbbc3663ea0fbe81326349

bandit22@bandit:~$ cat /tmp/8ca319486bfbbc3663ea0fbe81326349
QYw0Y2aiA672PsMmh9puTQuhoz8SyR2G

# level 23 — cron script injection

The cron job executes and deletes scripts owned by bandit23 from /var/spool/bandit24/foo. Write a script that copies the next password to a readable location.

terminal bash
bandit23@bandit:/tmp/shell23$ cat script.sh
#!/bin/bash
cat /etc/bandit_pass/bandit24 >> /tmp/shell23/bandit24.txt

bandit23@bandit:/tmp/shell23$ chmod 777 script.sh
bandit23@bandit:/tmp/shell23$ cp script.sh /var/spool/bandit24/foo

# wait for cron to execute...

bandit23@bandit:/tmp/shell23$ cat bandit24.txt
VAfGXJ1PBSsPSnvsjI8p759leLZ9GGar

# level 24 — brute force 4-digit pin

A daemon on port 30002 requires the level 24 password plus a 4-digit pin. Brute force all 10,000 combinations.

script.sh bash
#!/bin/bash

for i in {0000..9999}; do
    echo "VAfGXJ1PBSsPSnvsjI8p759leLZ9GGar" $i
done | nc localhost 30002 | grep -v "Wrong"
output bash
bandit24@bandit:/tmp/bf$ ./script.sh
Correct!
The password of user bandit25 is p7TaowMYrmu23Ol8hiZh9UvD0O9hpx8d

# level 25 — more + vim escape

Bandit26's shell is not /bin/bash — it runs more ~/text.txt then exits. Shrink the terminal so more pauses, press v to enter vim, then :set shell=/bin/bash followed by :shell.

terminal bash
bandit25@bandit:~$ cat /etc/passwd | grep bandit26
bandit26:x:11026:11026:bandit level 26:/home/bandit26:/usr/bin/showtext

bandit25@bandit:~$ cat /usr/bin/showtext
#!/bin/sh
export TERM=linux
exec more ~/text.txt
exit 0

# SSH with the key, shrink terminal so more pauses at --More--(83%)
# press v → opens vim
# :set shell=/bin/bash
# :shell

bandit26@bandit:~$ cat /etc/bandit_pass/bandit26
c7GvcKlw9mC7aUQaPx7nwFstuAIBw1o1

# level 26 — SUID (again)

Same pattern as level 19. A setuid binary runs commands as bandit27.

terminal bash
bandit26@bandit:~$ ./bandit27-do whoami
bandit27
bandit26@bandit:~$ ./bandit27-do cat /etc/bandit_pass/bandit27
YnQpBuifNMas1hcUFk70ZmqkhUU2EuaS

# level 27 — git clone

Clone the git repo and read the README.

terminal bash
bandit27@bandit:/tmp/sahin$ git clone ssh://bandit27-git@localhost:2220/home/bandit27-git/repo
...
Receiving objects: 100% (3/3), done.

bandit27@bandit:/tmp/sahin/repo$ cat README
The password to the next level is: AVanL161y9rsbcJIsFHuw35rjaOM19nR

# level 28 — git log / history

The password was removed in a later commit ("fix info leak"). Check out the earlier commit to read it.

terminal bash
bandit28@bandit:/tmp/sahin28/repo$ git log --oneline
104db85 (HEAD) fix info leak
6c3c5e4 add missing data
cd3b97e initial commit of README.md

bandit28@bandit:/tmp/sahin28/repo$ git checkout 6c3c5e4
bandit28@bandit:/tmp/sahin28/repo$ cat README.md
...
- username: bandit29
- password: tQKvmcwNYcFS6vmPHIUSI3ShmsrQZK8S

# level 29 — git branches / refs

No password in git history — check other branches. The dev branch has the credentials.

terminal bash
bandit29@bandit:/tmp/sahin29/repo$ git show-ref
0afe350 refs/heads/master
0afe350 refs/remotes/origin/HEAD
fbbce0e refs/remotes/origin/dev
0afe350 refs/remotes/origin/master
746e423 refs/remotes/origin/sploits-dev

bandit29@bandit:/tmp/sahin29/repo$ git checkout fbbce0e
HEAD is now at fbbce0e add data needed for development

bandit29@bandit:/tmp/sahin29/repo$ cat README.md
...
- username: bandit30
- password: xbhV3HpNGlTIdnjUrdAlPzc2L6y9EOnS

# level 30 — git tags

No useful commits or branches. The password is stored in a git tag.

terminal bash
bandit30@bandit:/tmp/sahin30/repo$ git tag
secret

bandit30@bandit:/tmp/sahin30/repo$ git show secret
OoffzGDlzhAlerFJ2cAiz1D41JW1Mhmt

# level 31 — git push

The README asks you to push a file named key.txt with content "May I come in?" to master. The .gitignore blocks *.txt, so force-add it.

terminal bash
bandit31@bandit:/tmp/sahin31/repo$ echo "May I come in?" > key.txt
bandit31@bandit:/tmp/sahin31/repo$ git add -f key.txt
bandit31@bandit:/tmp/sahin31/repo$ git commit -m "key.txt"
bandit31@bandit:/tmp/sahin31/repo$ git push origin master
...
remote: .oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.
remote:
remote: Well done! Here is the password for the next level:
remote: rmCBvG56y58BXzv98yZGdO7ATVL5dW8y

# level 32 — uppercase shell escape

The "UPPERCASE SHELL" converts all input to uppercase, breaking every command. Shell variables like $USER still expand. The trick: $0 refers to the current shell binary — executing it spawns a proper shell.

terminal bash
>> $USER
sh: 1: bandit32: not found
>> $PATH
sh: 1: /usr/local/sbin:/usr/local/bin:...: not found
>> $SHELL
WELCOME TO THE UPPERCASE SHELL

# $0 expands to the shell binary — no uppercase issue
>> $0
$ whoami
bandit33
$ cat /etc/bandit_pass/bandit33
odHo63fHiFqcWWJG9rLiLDtPm45KzUKy

# key takeaways

  • File naming tricks (dashes, spaces, dotfiles) are basic but essential Linux knowledge
  • find + file + grep cover 80% of filesystem enumeration needs
  • Compression matryoshka: always check file type after each extraction round
  • SSH keys (id_rsa) must be chmod 600 or SSH refuses them
  • Netcat and OpenSSL s_client are your go-to tools for raw TCP/SSL connections
  • Nmap service detection (-A) reveals what plain port scans miss
  • Cron jobs running as other users are a classic privilege escalation vector
  • Git history, branches, and tags all leak secrets — check everything
  • Shell escapes: $0 bypasses input filters since it is a variable, not a command
  • SUID binaries let you run commands as the file owner — always check with find -perm -4000