[{"content":"","date":"16 May 2026","externalUrl":null,"permalink":"/","section":" ","summary":"","title":" ","type":"page"},{"content":"","date":"16 May 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":" Level 4 -\u0026gt; 5 # Level Goal\n(No hint given.)\nSolution\nConnect as leviathan4:\n[greycipher@remnant ~]$ ssh leviathan4@leviathan.labs.overthewire.org -p 2223 The home directory has a hidden .trash folder:\nleviathan4@leviathan:~$ ls -la dr-xr-x--- 2 root leviathan4 4096 ... .trash leviathan4@leviathan:~$ ls -la .trash/ -r-sr-x--- 1 leviathan5 leviathan4 7352 ... bin Running the SUID binary gives us an unexpected output:\nleviathan4@leviathan:~/.trash$ ./bin 01010100 01101001 01110100 01101000 00110100 01100011 01101111 01101011 01100101 01101001 00001010 That\u0026rsquo;s binary. Each space-separated group of 8 bits represents one character in ASCII. We can convert it directly from the command line using Perl:\nleviathan4@leviathan:~/.trash$ ./bin | perl -lape \u0026#39;$_=pack\u0026#34;(B8)*\u0026#34;,@F\u0026#39; [REDACTED] Notes\nASCII is a standard that maps numbers to characters. The binary 01010100 equals decimal 84, which maps to the letter T. The Perl one-liner pack\u0026quot;(B8)*\u0026quot; interprets each 8-bit group as a binary number and converts it to its ASCII character. You could also do this manually using an online binary-to-ASCII converter. Level 5 -\u0026gt; 6 # Level Goal\n(No hint given.)\nSolution\nConnect as leviathan5:\n[greycipher@remnant ~]$ ssh leviathan5@leviathan.labs.overthewire.org -p 2223 There\u0026rsquo;s a SUID binary in the home directory:\nleviathan5@leviathan:~$ ls -la -r-sr-x--- 1 leviathan6 leviathan5 7634 ... leviathan5 leviathan5@leviathan:~$ ./leviathan5 Cannot find /tmp/file.log It\u0026rsquo;s looking for a file that doesn\u0026rsquo;t exist. ltrace tells us exactly what\u0026rsquo;s happening:\nleviathan5@leviathan:~$ ltrace ./leviathan5 fopen(\u0026#34;/tmp/file.log\u0026#34;, \u0026#34;r\u0026#34;) = 0 puts(\u0026#34;Cannot find /tmp/file.log\u0026#34;) exit(-1) The binary tries to open /tmp/file.log and print its contents. We control that path so we create a symlink there pointing at the password file:\nleviathan5@leviathan:~$ ln -s /etc/leviathan_pass/leviathan6 /tmp/file.log leviathan5@leviathan:~$ ./leviathan5 [REDACTED] The binary opens /tmp/file.log, follows the symlink to /etc/leviathan_pass/leviathan6 and prints it using leviathan6\u0026rsquo;s elevated SUID privileges.\nNotes\nThis is the same symlink trick from Level 2, but in reverse, instead of tricking a binary file into reading the wrong argument , here we\u0026rsquo;re planting a fake file in a path the binary already trusts. Level 6 -\u0026gt; 7 # Level Goal\n(No hint given.)\nSolution\nConnect as leviathan6:\n[greycipher@remnant ~]$ ssh leviathan6@leviathan.labs.overthewire.org -p 2223 Another SUID binary, this time asking for a 4-digit PIN:\nleviathan6@leviathan:~$ ls -la -r-sr-x--- 1 leviathan7 leviathan6 7484 ... leviathan6 leviathan6@leviathan:~$ ./leviathan6 usage: ./leviathan6 \u0026lt;4 digit code\u0026gt; leviathan6@leviathan:~$ ./leviathan6 1234 Wrong ltrace won\u0026rsquo;t reveal the PIN this time, the comparison is handled differently. With only 10.000 possible combinations (0000-9999), brute force is the practical approach. Write a short bash script in /tmp:\nleviathan6@leviathan:/tmp$ mktemp -d /tmp/tmp.zPxIib0Czf leviathan6@leviathan:/tmp$ cd /tmp/tmp.zPxIib0Czf leviathan6@leviathan:/tmp/tmp.zPxIib0Czf$ cat \u0026gt; brute.sh \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; #!/bin/bash for i in $(seq -w 0 9999); do ~/leviathan6 $i done EOF leviathan6@leviathan:/tmp/tmp.zPxIib0Czf $ chmod +x brute.sh leviathan6@leviathan:/tmp/tmp.zPxIib0Czf $ ./brute.sh After a few seconds you\u0026rsquo;ll see Wrong spam stop and a shell prompt appear:\n$ whoami leviathan7 $ cat /etc/leviathan_pass/leviathan7 [REDACTED] Notes\nseq -w 0 9999 generates numbers from 0000 to 9999 with leading zeros preserved, important here since the binary expects exactly 4 digits. This is a textbook brute force attack: systematically trying every possible input until one works. It\u0026rsquo;s only feasible here beacuse the search space is tiny (10.000 combinations). Real PIN systems use lockouts and rate limiting specifically to prevent this. Level 7 # Connect as leviathan7 with the password from the previous level:\n[greycipher@remnant ~]$ ssh leviathan7@leviathan.labs.overthewire.org -p 2223 leviathan7@leviathan:~$ ls CONGRATULATIONS leviathan7@leviathan:~$ cat CONGRATULATIONS Well Done, you seem to have used a *nix system before, now try something more serious. That\u0026rsquo;s Leviathan done.\nWrapping up # Leviahan is a short wargame but a meaningful step up from Bandit. The levels don\u0026rsquo;t give you hints, there are no man page suggestions and you\u0026rsquo;re expected to figure out what a binary does just by running it and tracing it. If you worked through both posts in this series, you\u0026rsquo;ve now used ltrace to intercept hardcoded passwords, exploited a TOCTOU vulnerability, planted symlinks in trusted paths, decoded binary output and written your first brute force script.\n","date":"16 May 2026","externalUrl":null,"permalink":"/posts/otw-leviathan-2/","section":"Posts","summary":"A beginner-friendly walkthrough of levels 4-7 in the Leviathan wargame from OverTheWire. Learn how to decode binary output, abuse symbolic links and write your first brute force script to crack a 4-digit PIN.","title":"Leviathan | Levels 4-7 | Binary Encoding, Symlinks \u0026 Brute Force","type":"posts"},{"content":"","date":"16 May 2026","externalUrl":null,"permalink":"/categories/linux/","section":"Categories","summary":"","title":"Linux","type":"categories"},{"content":"","date":"16 May 2026","externalUrl":null,"permalink":"/categories/overthewire/","section":"Categories","summary":"","title":"OverTheWire","type":"categories"},{"content":"","date":"16 May 2026","externalUrl":null,"permalink":"/series/overthewire---leviathan/","section":"Series","summary":"","title":"OverTheWire - Leviathan","type":"series"},{"content":"","date":"16 May 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"16 May 2026","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"16 May 2026","externalUrl":null,"permalink":"/categories/wargames/","section":"Categories","summary":"","title":"WarGames","type":"categories"},{"content":" Introduction # If Bandit was the Linux bootcamp, Leviathan is where things start to get interesting. Instead of just reading files and navigating directories, you\u0026rsquo;re now dealing with compiled binaries, SUID bits and your first taste of binary analisys. The wargame is rated 1/10 difficulty but a couple of these levels require a real shift in thinking compared to what you\u0026rsquo;ve done before.\nThe key tool you\u0026rsquo;ll be reaching for throughout this series is ltrace. Get comfortable with it.\nLevel 0 -\u0026gt; 1 # Level Goal\nData for the levels can be found in the homedirectories. You can look at /etc/leviathan_pass for the various level passwords.\nSolution\nConnect to the server as leviathan0, note the port is 2223, different from bandit:\n[greycipher@remnant ~]$ ssh leviathan0@leviathan.labs.overthewire.org -p 2223 Once in, list everything including hidden files:\nleviathan0@leviathan:~$ ls -la total 24 drwxr-xr-x 3 root root 4096 Apr 3 15:19 . drwxr-xr-x 150 root root 4096 Apr 3 15:20 .. drwxr-x--- 2 leviathan1 leviathan0 4096 Apr 3 15:19 .backup -rw-r--r-- 1 root root 220 Mar 31 2024 .bash_logout -rw-r--r-- 1 root root 3851 Apr 3 15:10 .bashrc -rw-r--r-- 1 root root 807 Mar 31 2024 .profile leviathan0@leviathan:~$ cd .backup leviathan0@leviathan:~/.backup$ ls bookmarks.html A massive HTML file. Reading it manually would take forever, use grep to filter for that matters:\nleviathan0@leviathan:~/.backup$ grep leviathan1 bookmarks.html \u0026lt;DT\u0026gt;\u0026lt;A HREF=\u0026#34;http://leviathan.labs.overthewire.org/passwordus.html | This will be fixed later, the password for leviathan1 is [REDACTED]\u0026#34; ...\u0026gt;password to leviathan1\u0026lt;/A\u0026gt; Notes\ngrep scans a file for lines matching a pattern, essential when you\u0026rsquo;re dealing with large files. It\u0026rsquo;s common in real-world pentesting to find credentials left in browser exports, config backups and similar files that people forget about. Level 1 -\u0026gt; 2 # Level Goal\n(No hint given, figure it out.)\nSolution\nConnect as leviathan1 and see what\u0026rsquo;s in the home directory:\nleviathan1@leviathan:~$ ls -la total 36 drwxr-xr-x 2 root root 4096 Apr 3 15:19 . drwxr-xr-x 150 root root 4096 Apr 3 15:20 .. -rw-r--r-- 1 root root 220 Mar 31 2024 .bash_logout -rw-r--r-- 1 root root 3851 Apr 3 15:10 .bashrc -r-sr-x--- 1 leviathan2 leviathan1 15088 Apr 3 15:19 check -rw-r--r-- 1 root root 807 Mar 31 2024 .profile There\u0026rsquo;s a SUID binary called check. Running it asks for a password:\nleviathan1@leviathan:~$ ./check password: test Wrong password, Good Bye ... The binary is comparing out input against something hardcoded. This is a job for ltrace, which intercepts and displays library calls made by a program as it runs:\nleviathan1@leviathan:~$ ltrace ./check printf(\u0026#34;password: \u0026#34;) = 10 getchar(...) = ... strcmp(\u0026#34;test\u0026#34;, \u0026#34;sex\u0026#34;) = -1 puts(\u0026#34;Wrong password, Good Bye ...\u0026#34;) The strcmp call gives it away, it\u0026rsquo;s comparing out input against the string \u0026quot;sex\u0026quot;. Now run the binary directly without ltrace, as it drops the elevated privileges:\nleviathan1@leviathan:~$ ./check password: sex $ whoami leviathan2 $ cat /etc/leviathan_pass/leviathan2 [REDACTED] Notes\nltrace traces library calls (C functions like printf, strcmp). This is different from strace, which traces system calls (lower level kernel interactions like read, write). For this kind of challenge, ltrace is almost always more useful. The SUID bit (s in -r-sr-x---) means the binary runs with the permissions of its owner (leviathan1) rather than the user executing it. That\u0026rsquo;s whi getting a shell through it gives us elevated access. Always run SUID binaries directly when exploiting them, tracing tools like ltrace drop those elevated privileges. Level 2 -\u0026gt; 3 # Level Goal\n(No hint given.)\nSolution\nConnect as leviathan2:\nleviathan2@leviathan:~$ ls -la total 36 drwxr-xr-x 2 root root 4096 Apr 3 15:19 . drwxr-xr-x 150 root root 4096 Apr 3 15:20 .. -rw-r--r-- 1 root root 220 Mar 31 2024 .bash_logout -rw-r--r-- 1 root root 3851 Apr 3 15:10 .bashrc -r-sr-x--- 1 leviathan3 leviathan2 15076 Apr 3 15:19 printfile -rw-r--r-- 1 root root 807 Mar 31 2024 .profile A SUID binary called printfile. It takes a filename as an argument and prints it, like cat. Let\u0026rsquo;s trace it:\nleviathan2@leviathan:~$ mkdir /tmp/greycipher \u0026amp;\u0026amp; cd /tmp/greycipher leviathan2@leviathan:/tmp/greycipher$ touch test.txt leviathan2@leviathan:/tmp/greycipher$ ltrace ~/printfile test.txt __libc_start_main(0x80490ed, 2, 0xffffd424, 0 \u0026lt;unfinished ...\u0026gt; access(\u0026#34;test.txt\u0026#34;, 4) = 0 snprintf(\u0026#34;/bin/cat test.txt\u0026#34;, 511, \u0026#34;/bin/cat %s\u0026#34;, \u0026#34;test.txt\u0026#34;) = 17 geteuid() = 12002 geteuid() = 12002 setreuid(12002, 12002) = 0 system(\u0026#34;/bin/cat test.txt\u0026#34; \u0026lt;no return ...\u0026gt; --- SIGCHLD (Child exited) --- \u0026lt;... system resumed\u0026gt; ) = 0 +++ exited (status 0) +++ Two things stand out: access() checks if we have permission to read the file and then system() runs /bin/cat on it. So far so good. But what happens with a filename that contains a space?\nleviathan2@leviathan:/tmp/greycipher$ touch \u0026#34;pass file.txt\u0026#34; leviathan2@leviathan:/tmp/greycipher$ ltrace ~/printfile \u0026#34;pass file.txt\u0026#34; __libc_start_main(0x80490ed, 2, 0xffffd424, 0 \u0026lt;unfinished ...\u0026gt; access(\u0026#34;pass file.txt\u0026#34;, 4) = 0 snprintf(\u0026#34;/bin/cat pass file.txt\u0026#34;, 511, \u0026#34;/bin/cat %s\u0026#34;, \u0026#34;pass file.txt\u0026#34;) = 22 geteuid() = 12002 geteuid() = 12002 setreuid(12002, 12002) = 0 system(\u0026#34;/bin/cat pass file.txt\u0026#34;/bin/cat: pass: No such file or directory /bin/cat: file.txt: No such file or directory \u0026lt;no return ...\u0026gt; --- SIGCHLD (Child exited) --- \u0026lt;... system resumed\u0026gt; ) = 256 +++ exited (status 0) +++ The vulnerability is here: access() checks the whole path \u0026quot;pass file.txt\u0026quot; as a single string, but when system() runs /bin/car pass file.txt, the shell splits it into two separate arguments, pass and file.txt.\nWe can exploit this. Create a symlink named pass pointing to the password file, then trick the binary into reading it:\nleviathan2@leviathan:/tmp/greycipher$ ln -s /etc/leviathan_pass/leviathan3 /tmp/greycipher/pass leviathan2@leviathan:/tmp/greycipher$ ~/printfile \u0026#34;pass file.txt\u0026#34; [REDACTED] /bin/cat: file.txt: No such file or directory access() happily checks \u0026quot;pass file.txt\u0026quot; and finds it readable. Then cat splits the string and reads pass, which is our symlink to the password file, using leviathan3\u0026rsquo;s SUID privileges.\nNotes\nThis is a classic TOCTOU (Time-of-Check to Time-of-Use) vulnerability. The check (access()) and the action (system()) operate on different interpretations of the same input, which opens the door for exploitation. ln -s \u0026lt;target\u0026gt; \u0026lt;link\u0026gt; creates a symbolic link, a pointer to another file. The link is read as if it were the file it points to. The error on file.txt is expected and harmless, we already got what we needed. Level 3 -\u0026gt; 4 # Level Goal\n(No hint given.)\nSolution\nConnect as leviathan3:\nleviathan3@leviathan:~$ ls -la total 40 drwxr-xr-x 2 root root 4096 Apr 3 15:19 . drwxr-xr-x 150 root root 4096 Apr 3 15:20 .. -rw-r--r-- 1 root root 220 Mar 31 2024 .bash_logout -rw-r--r-- 1 root root 3851 Apr 3 15:10 .bashrc -r-sr-x--- 1 leviathan4 leviathan3 18100 Apr 3 15:19 level3 -rw-r--r-- 1 root root 807 Mar 31 2024 .profile Another SUID binary. Running it:\nleviathan3@leviathan:~$ ./level3 Enter the password\u0026gt; test bzzzzzzzzap. WRONG By now you know the drill:\nleviathan3@leviathan:~$ ltrace ./level3 __libc_start_main(0x80490ed, 1, 0xffffd474, 0 \u0026lt;unfinished ...\u0026gt; strcmp(\u0026#34;h0no33\u0026#34;, \u0026#34;kakaka\u0026#34;) = -1 printf(\u0026#34;Enter the password\u0026gt; \u0026#34;) = 20 fgets(Enter the password\u0026gt; test \u0026#34;test\\n\u0026#34;, 256, 0xf7fab5c0) = 0xffffd24c strcmp(\u0026#34;test\\n\u0026#34;, \u0026#34;snlprintf\\n\u0026#34;) = 1 puts(\u0026#34;bzzzzzzzzap. WRONG\u0026#34;bzzzzzzzzap. WRONG ) = 19 +++ exited (status 0) +++ There are two strcmp calls. The first one (h0no33 vs kakaka) is a decoy, it runs before any user input. The second one is what matters: our input is being compared against \u0026quot;snlprintf\u0026quot;. Supply it:\nleviathan3@leviathan:~$ ./level3 Enter the password\u0026gt; snlprintf [You\u0026#39;ve got shell]! $ whoami leviathan4 $ cat /etc/leviathan_pass/leviathan4 [REDACTED] Notes\nThe decoy strcmp is a simple obfuscation attempt, something you\u0026rsquo;ll see more of as challenges get harder. ltrace cuts right through it. Notice that fgets appends a newline character \\n to the input, which is why ltrace shows \u0026quot;snlprintf\\n\u0026quot; rather than just \u0026quot;snlprintf\u0026quot;. The binary handles the comparison accordingly, so you don\u0026rsquo;t need to worry about it when typing the password manually. ","date":"10 May 2026","externalUrl":null,"permalink":"/posts/otw-leviathan-1/","section":"Posts","summary":"A beginner-friendly walkthrough of Levels 0-3 in the Leviathan wargame from OverTheWire. Learn how to trace binary execution with ltrace, understand SUID permissions and exploit a race condition vulnerability, these are the first real steps in binary analisys.","title":"Leviathan | Levels 0-3 | Binary Tracing \u0026 SUID Exploitation","type":"posts"},{"content":" Introduction # Taking screenshots and screen recordings is a normal part of documenting work for blog posts, reports, or sharing with colleagues. The problem is that real environments have real people in them: faces visible in a webcam, bystanders in a dashcam clip, colleagues on a shared screen. Blurring them manually in an image editor gets tedious fast, and doing it frame-by-frame on a video is not realistic.\nI wanted a tool I could run from the terminal that would handle the detection and censoring automatically, with no external API calls and no frames leaving the machine. The result is face_censor.py, a single-file Python script built on OpenCV that processes images, videos, and live webcam feeds.\nWhat it Does # The script detects faces using a computer vision model and applies one of three censor effects to each detected region:\nBlur: Gaussian blur with a 99x99 kernel, smoothing away all identifying detail while keeping the face shape recognizable. Pixelate: shrinks the face region to a 12x12 grid and scales back up, producing the classic news-broadcast mosaic. Blackbox: solid black rectangle, maximum coverage, useful when the output will be processed by another tool and needs a hard mask. All effects include a 10% padding margin around the raw detection box so hairlines and chin edges are covered rather than clipped at the boundary.\nInput can be a static image, a video file or a live webcam feed. In webcam mode the effect is switchable at runtime with a keypress without restarting the script.\nFace Detection # The script uses two detectors and picks the best available one at startup.\nOpenCV DNN - ResNet SSD is the primary detector. It takes each frame, resizes it to 300x300, normalises the pixel values and runs it through a Single Shot MultiBox Detector trained on the ResNet-10 architecture. Each detection comes with a confidence score, only detections about the threshold (default 0.5, adjustable with --confidence) are kept. It handles angled faces, partial occlusion and varying lighting conditions well.\nHaar Cascade is the fallback, used when the DNN model files are not present. It is built into OpenCV so it requires no downloads, but it is front-face only and struggles with low light and non-frontal angles.\nThe DNN model is about 10MB total and is downloaded once with a single command. After that the script works fully offline.\nSetup # Setup the virtual environment and install the dependencies:\n[greycipher@remnant ~]$ python3 -m venv .venv [greycipher@remnant ~]$ ./.venv/bin/activate [greycipher@remnant ~]$ pip install -r requirements.txt Download the DNN model (recommended, one-time):\n[greycipher@remnant ~]$ python3 main.py --download-models Downloading deploy.prototxt... Saved -\u0026gt; models/deploy.prototxt Downloading res10_300x300_ssd_iter_140000.caffemodel... Saved -\u0026gt; models/res10_300x300_ssd_iter_140000.caffemodel Done. If you skip this step the script will falls back to Haar Cascade automatically, no error, just lower accuracy.\nUsage # Images # [greycipher@remnant ~]$ python3 --input photo.jpg --effect blur [INFO] Using OpenCV DNN face detector (ResNet SSD) [OK] 3 face(s) censored -\u0026gt; photo_censored.jpg The output is saved as photo_censored.jpg in the same directory automatically. To specify the output path:\n[greycipher@remnant ~]$ python3 main.py -i photo.jpg -o result.jpg -a pixelate Supported formats: .jpg, .jpeg, .png, .bmp, .tiff, .webp\nVideo Files # [greycipher@remnant ~]$ python3 main.py --input video.mp4 --effect blackbox [INFO] Processing 1800 frames at 30.0 fps … 30/1800 (2%) 60/1800 (3%) ... [OK] Done → video_censored.mp4 Original FPS and resolution are preserved. The output uses the mp4v codec, if your player has trouble , re-encode with FFmpeg:\n[greycipher@remnant ~]$ ffmpeg -i video_censored.mp4 -c:v libx264 output.mp4 Audio is not coped to the output. To re-attach the original:\n[greycipher@remnant ~]$ ffmpeg -i video_censored.mp4 -i original.mp4 -c copy -map 0:v:0 -map 1:a:0 final.mp4 Webcam # [greycipher@remnant ~]$ python3 main.py --webcam --effect blur Opens the default camera. Switch effects without restarting:\nKey Effect B Gaussian blur P Pixelation K Black box Q Quit Tuning Detection # The default confidence threshold of 0.5 works well for most inputs. Two situations wher you will want to adjust it:\nToo many false positives: objects being flagged as faces:\n[greycipher@remnant ~]$ python3 main.py -i video.mp4 -e blur --confidence 0.7 Missing faces: real faces not being detected:\n[greycipher@remnant ~]$ python3 main.py -i video.mp4 -e blur --confidence 0.2 Lowering the threshold catches weaker detections at the cost of more false positives. If faces are still being missed at 0.3, check whether you have the DNN model downloaded, the Haar fallback is significantly less sensitive on angled or partially occluded faces.\nHow the Detection Pipeline Works # For each frame the DNN detector does the following:\nResize the frame to 300x300 pixels Subtract the mean pixel values (104, 177, 123) to normalise Pass the blob through the ResNet SSD network For each detection above the confidence threshold, project the bounding box coordinates back to the original frame dimensions Clip coordinates to the frame boundaries so no region extends outside the image The result is a list of (x, y, w, z) tuples, one per detected face. The censor function then adds the 10% padding margin before applying the chosen effect.\nThe code for the DNN detection step:\ndef detect_faces_dnn(net, frame, confidence_threshold=0.5): \u0026#34;\u0026#34;\u0026#34;Return list of (x, y, w, h) using DNN detector.\u0026#34;\u0026#34;\u0026#34; h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage( cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) net.setInput(blob) detections = net.forward() faces = [] for i in range(detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence \u0026gt; confidence_threshold: box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype(int) x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w - 1, x2), min(h - 1, y2) if x2 \u0026gt; x1 and y2 \u0026gt; y1: faces.append((x1, y1, x2 - x1, y2 - y1)) return faces Limitations # A few things worth knowing before using the tool on real footage:\nThe DNN model was trained on frontal and slightly off-axis faces. Extreme profile angles beyond roughly 45° may not be detected. Very small faces below about 30x30 pixels in the source resolution are generally missed by both detectors. The Haar fallback in front-face only and performs poorly in low light. Audio is not preserved in video output, re-attach with FFmpeg if needed. All CLI Options # Flag Short Default Description --input -i — Input image or video file --output -o auto Output file path --webcam — false Use live webcam feed --effect -e blur Censor effect: blur, pixelate, blackbox --confidence -c 0.5 DNN detection confidence threshold (0–1) --download-models — false Download DNN model files and exit Practical Examples # # Pixelate all faces in a group photo [greycipher@remnant ~]$ python3 main.py -i group_photo.png -e pixelate # Black-box faces in a dashcam video, high confidence only [greycipher@remnant ~]$ python3 main.py -i dashcam.mp4 -o dashcam_safe.mp4 -e blackbox -c 0.65 # Real-time webcam starting with pixelation [greycipher@remnant ~]$ python3 main.py --webcam -e pixelate # Re-download models if they get corrupted [greycipher@remnant ~]$ rm -rf models/ [greycipher@remnant ~]$ python3 main.py --download-models What\u0026rsquo;s Next # A few things I would add in a future version:\nAudio passthrough in video mode without requiring a separate FFmpeg step A --region flag to manually specify additional areas to censor beyond detected faces, useful for license plates or screen content Batch processing a directory of images in one command YOLO-based detection as a third detector option for better coverage on angled faces The Code # Full project on GitHub: GreyCipher-sec/FaceCensor\n","date":"17 April 2026","externalUrl":null,"permalink":"/posts/face-censor/","section":"Posts","summary":"How I built a command-line tool to automatically detect and censor faces in images, videos, and live webcam feeds using OpenCV’s DNN face detector — with blur, pixelation, and black box effects.","title":"Building a Face Censor Tool","type":"posts"},{"content":"","date":"17 April 2026","externalUrl":null,"permalink":"/categories/tooling/","section":"Categories","summary":"","title":"Tooling","type":"categories"},{"content":" Introduction # Levels 27 through 32 are entirely focused on git. Version control is not just a developer tool, git repositories frequently contain leaked credentials, deleted sensitive files, recoverable from history and evidence of how a project evolved. Understanding git from an investigative perspective is a genuine DFIR and OSINT skill. level 33 closes the series with one final escape challenge.\nThese levels are cloned and completed on the local machine, not from the Bandit server.\nLevel 27 -\u0026gt; 28 # Level Goal\nClone the repository at ssh://bandit27-git@bandit.labs.overthewire.org:2220/home/bandi27-git/repo and find the password.\nSolution\n[greycipher@remnant ~]$ mktemp -d \u0026amp;\u0026amp; cd /tmp/tmp.git27 [greycipher@remnant tmp.git27]$ git clone ssh://bandit27-git@bandit.labs.overthewire.org:2220/home/bandit27-git/repo Cloning into \u0026#39;repo\u0026#39;... bandit27-git@bandit.labs.overthewire.org\u0026#39;s password: [bandit27 password] [greycipher@remnant tmp.git27]$ cat repo/README The password to the next level is: [REDACTED] Notes\ngit clone downloads a full copy of a repository including its complete history. The password for bandit27-git is the name as your current level password. In OSINT and red team work, cloning public repositories and searching them for secrets is a standard reconnaissance step.\nLevel 28 -\u0026gt; 29 # Level Goal\nClone the repository and find the password, it may have been removed from the current version.\nSolution\n[greycipher@remnant ~]$ mktemp -d \u0026amp;\u0026amp; cd /tmp/tmp.git28 [greycipher@remnant tmp.git28]$ git clone ssh://bandit28-git@bandit.labs.overthewire.org:2220/home/bandit28-git/repo [greycipher@remnant tmp.git28]$ cat repo/README.md # Bandit Notes ## credentials - username: bandit29 - password: xxxxxxxxxx The password has been redacted in the current version. Check the commit history:\n[greycipher@remnant tmp.git28]$ cd repo [greycipher@remnant tmp.git28]$ git log --oneline adc7f88 (HEAD -\u0026gt; master, origin/master, origin/HEAD) fix info leak a3437bd add missing data cb630ec initial commit of README.md [greycipher@remnant repo]$ git shot a3437bd commit a3437bddd447f2d496731658e86b98cbea9d3c98 Author: Morla Porla \u0026lt;morla@overthewire.org\u0026gt; Date: Fri Apr 3 15:17:54 2026 +0000 add missing data diff --git a/README.md b/README.md index 7ba2d2f..d4e3b74 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,5 @@ Some notes for level29 of bandit. ## credentials - username: bandit29 -- password: \u0026lt;TBD\u0026gt; +- password: [REDACTED] Notes\ngit log --oneline shows a compact commit history. git show \u0026lt;hash\u0026gt; displays the diff introduced by that commit. Credentials that were \u0026ldquo;deleted\u0026rdquo; from a repository are still fully recoverable from history, this is one of the most common real-world secrets exposure vectors. Tools like trufflehog and gitleaks automate this search across entire repositories.\nLevel 29 -\u0026gt; 30 # Level Goal\nClone the repository and find the password, it may be on a different branch.\nSolution\n[greycipher@remnant ~]$ mktemp \u0026amp;\u0026amp; cd /tmp/tmp.git29 [greycipher@remnant tmp.git29]$ git clone ssh://bandit29-git@bandit.labs.overthewire.org:2220/home/bandit29-git/repo [greycipher@remnant tmp.tmpgit29]$ cd repo \u0026amp;\u0026amp; cat README.md # Bandit Notes Some notes for bandit30 of bandit. ## credentials - username: bandit30 - password: \u0026lt;no passwords in production!\u0026gt; List all branches including remote ones:\n[greycipher@remnant repo]$ git branch -a * master remotes/origin/HEAD -\u0026gt; origin/master remotes/origin/dev remotes/origin/master remotes/origin/sploits-dev [greycipher@remnant repo]$ git checkout dev [greycipher@remnant repo]$ cat README.md # Bandit Notes Some notes for bandit30 of bandit. ## credentials - username: bandit30 - password: [REDACTED] Notes\ngit branch -a shows all branches, including remote tracking branches. Development and staging branches frquently contain credentials, debug output or features not yet sanitised for production. During a repository audit always check all branches, not just main or master.\nLevel 30 -\u0026gt; 31 # Level Goal\nClone the repository and find the password, it may be stored in a git tag.\nSolution\n[greycipher@remnant ~]$ mktemp \u0026amp;\u0026amp; cd /tmp/tmp.git30 [greycipher@remnant tmp.git30]$ git clone ssh://bandit30-git@bandit.labs.overthewire.org:2220/home/bandit30-git/repo [greycipher@remnant tmp.git30]$ cd repo \u0026amp;\u0026amp; cat README.md just an empty file... muahaha Check for tags:\n[greycipher@remnant repo]$ git tag secret [greycipher@remnant repo]$ git show secret [REDACTED] Notes\nGit tags mark specific points in history, often used for releases. They are separate from branches and easy to overlook. git tag lists all tags, git show \u0026lt;tagname\u0026gt; displays the tagged object. Tags can contain annotation messages which sometimes hold sentitive information behind by careless developers.\nLevel 31 -\u0026gt; 32 # Level Goal\nClone the repository. Push a file called key.txt containing the text May I come in? to the remote on branch master.\nSolution\n[greycipher@remnant]$ mktemp \u0026amp;\u0026amp; cd /tmp/tmp.git31 [greycipher@remnant tmp.git31]$ git clone ssh://bandit31-git@bandit.labs.overthewire.org:2220/home/bandit31-git/repo [greycipher@remnant tmp.git31]$ cd repo \u0026amp;\u0026amp; cat README.md This time your task is to push a file to the remote repository. File name: key.txt Content: \u0026#39;May I come in?\u0026#39; Branch: master Check .gitignore, *.txt may be ignored:\n[greycipher@remnant repo]$ cat .gitignore *.txt Force-add the file despite the ignore rule:\n[greycipher@remnant repo]$ git add -f key.txt [greycipher@remnant repo]$ git commit -m \u0026#34;add key\u0026#34; [greycipher@remnant repo]$ git push origin master [REDACTED] Notes\n.gitignore tells git which files to skip when staging changes. The -f flag on git add overrides it. In a security context, .gitignore is worth inspecting during a repository audit, it sometimes reveals the names of sensitive files that developers intentionally excluded from version control, like .env, private keys or config files containing credentials.\nLevel 32 -\u0026gt; 33 # Level Goal\nAfter all this git stuff, it\u0026rsquo;s time for another escape.\nSolution\n[greycipher@remnant ~]$ ssh bandit32@bandit.labs.overthewire.org -p 2220 Everything you type is converted to uppercase, making standard commands fail:\nWELCOME TO THE UPPERCASE SHELL \u0026gt;\u0026gt; ls sh: 1: LS: not found Shell special variables are not affected by the uppercasing. $0 expands to the name of the current shell:\n\u0026gt;\u0026gt; $0 $ whoami bandit33 $ cat /etc/bandit_pass/bandit33 [REDACTED] Notes\n$0 is a special shell variable that holds the name or path of the running shell. Because it starts with $ rather than a letter, the uppercase filter does not modify it. Expanding it directly spawns a new shell instance without the restriction. This is a good example of how constraints that seem total often have edge cases, understanding how the shell processes input at a low level reveals the gap.\n","date":"12 April 2026","externalUrl":null,"permalink":"/posts/otw-bandit-7/","section":"Posts","summary":"Walkthrough of Levels 27–33 in the OverTheWire Bandit wargame. Learn essential git commands for security work: cloning, log inspection, branch exploration, tags, stash, and push, then escape an uppercase shell.","title":"Bandit | Levels 27-33 | Git","type":"posts"},{"content":"","date":"12 April 2026","externalUrl":null,"permalink":"/series/overthewire---bandit/","section":"Series","summary":"","title":"OverTheWire - Bandit","type":"series"},{"content":" Introduction # Levels 25 through 27 are about restricted shells and escape techniques. When an account is configured with a non-standard shell, it is often an attempt to limit what the user can do. These levels teach you how to identify what shell is running, understand its constraints and find ways around them, skills that appear in CTF privilege escalation and real-world hardening audits.\nLevel 25 -\u0026gt; 26 # Level Goal\nLogging into bandit26 should be easy, but the shell for bandit26 is not /bin/bash. Find out what it is, how it works, and how to break out of it.\nSolution\n[greycipher@remnant ~]$ ssh bandit25@bandit.labs.overthewire.org -p 2220 A private key for bandit26 is available immediately:\nbandit25@bandit:~$ ls bandit26.sshkey bandit25@bandit:~$ cat /etc/passwd | grep bandit26 bandit26:x:11026:11026::/home/bandit26:/usr/bin/showtext The shell is /usr/bin/showtext, not bash, Check what it does:\nbandit25@bandit:~$ cat /usr/bin/showtext #!/bin/sh export TERM=linux exec more ~/text.txt exit 0 It opens more to display a file and then exits, any normal SSH session closes immediately. The trick is to force more to stay open by making your terminal window small enough that it cannot display the full file at once, triggering its interactive pager mode.\nShrink your terminal window vertically to just a few lines, then connect:\n[greycipher@remnant ~]$ ssh -i bandit26.sshkey bandit26@bandit.labs.overthewire.org -p 2220 more enters pager mode. From inside more, press v to open the current file in vim. From vim, spawn a shell:\n:set shell=/bin/bash :shell You now have a bash shell as bandit26.\nNotes\nThis escape works because more passes control to $EDITOR (vim by default) when you press v. Vim can execute shell commands via :shell or :!command. This is a well-known escape vector, vim, less, man, and other pager-based tools are all listed on GTFOBins as shell escape vectors. Restricting a user\u0026rsquo;s shell to a pager is not effective hardening.\nLevel 26 -\u0026gt; 27 # Level Goal\nYou have a shell as bandit26. Now grab the password for bandit27.\nSolution\nFrom the vim shell you opened in the previous level, you are already inside as bandit26. There is a setuid binary in the home directory:\nbandit26@bandit:~$ ls bandit27-do text.txt bandit26@bandit:~$ ./bandit27-do cat /etc/bandit_pass/bandit27 [REDACTED] Notes\nThis level is intentionally brief, it rewards completingthe previous escape by giving you direct access to the next password via the same setuid pattern seen in level 19. If your shell closed, repeat the level 25 -\u0026gt; 26 escape to get back in.\n","date":"5 April 2026","externalUrl":null,"permalink":"/posts/otw-bandit-6/","section":"Posts","summary":"Walkthrough of Levels 25-27 in te OverTheWire Bandit wargame. Learn how to identify and escape restricted shells, abuse the more pager to spawn a shell, and use vim as a shell escape vector.","title":"Bandit | Levels 25-27 | Escape \u0026 Restricted Shells","type":"posts"},{"content":" Introduction # Levels 21 through 25 are about automation, scheduled tasks, shell scripts, and brute force. Cron jobs are one of the most common persistence mechanisms used by attackers, and understanding how to read and trace them is an essential blue team skill. This section also introduces writing your first shell script, a milestone worth noting.\nLevel 20 -\u0026gt; 21 # Level Goal\nA setuid binary connects to a port you specify, reads a line, and compares it to the current level\u0026rsquo;s password. If correct, it sends the next password.\nSolution\n[greycipher@remnant ~]$ ssh bandit20@bandit.labs.overthewire.org -p 2220 This requires two simultaneous connections. Use \u0026amp; to run the listener in the background:\nbandit20@bandit:~$ echo \u0026#34;[REDACTED]\u0026#34; | nc -lp 4444 \u0026amp; [1] 12345 bandit20@bandit:~$ ./suconnect 4444 Read: [REDACTED] Password matches, sending next password [REDACTED] Notes\nnc -lp 4444 starts netcat in listen mode on port 4444 \u0026amp; sends the process to the background so you can keep using the terminal The binary connects to your listener, reads the password you sent, verifies it, and responds with the next one This pattern — setting up a listener and triggering a connection to it — is fundamental to understanding reverse shells and callback-based malware behaviour Level 21 -\u0026gt; 22 # Level Goal\nA program is running automatically at regular intervals from cron. Look in /etc/cron.d/ for the configuration and see what command is being executed.\nSolution\n[greycipher@remnant ~]$ ssh bandit21@bandit.labs.overthewire.org -p 2220 bandit21@bandit:~$ ls /etc/cron.d/ cronjob_bandit22 cronjob_bandit23 cronjob_bandit24 bandit21@bandit:~$ cat /etc/cron.d/cronjob_bandit22 @reboot bandit22 /usr/bin/cronjob_bandit22.sh \u0026amp;\u0026gt; /dev/null * * * * * bandit22 /usr/bin/cronjob_bandit22.sh \u0026amp;\u0026gt; /dev/null bandit21@bandit:~$ cat /usr/bin/cronjob_bandit22.sh #!/bin/bash chmod 644 /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv cat /etc/bandit_pass/bandit22 \u0026gt; /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv bandit21@bandit:~$ cat /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv [REDACTED] Notes\nCron is the Linux task scheduler. The five * * * * * fields represent minute, hour, day of month, month, and day of week, all stars means \u0026ldquo;run every minute\u0026rdquo;. The script writes the password to a world-readable temp file every minute. In a real environment, cron jobs writing sensitive data to /tmp would be a finding worth reporting.\nLevel 22 -\u0026gt; 23 # Level Goal\nA program is running automatically at regular intervals from cron. Look in /etc/cron.d/ for the configuration and see what command is being executed.\nSolution\n[greycipher@remnant ~]$ ssh bandit22@bandit.labs.overthewire.org -p 2220 bandit22@bandit:~$ cat /etc/cron.d/cronjob_bandit23 * * * * * bandit23 /usr/bin/cronjob_bandit23.sh \u0026amp;\u0026gt; /dev/null bandit22@bandit:~$ cat /usr/bin/cronjob_bandit23.sh #!/bin/bash myname=$(whoami) mytarget=$(echo I am user $myname | md5sum | cut -d \u0026#39; \u0026#39; -f 1) echo \u0026#34;Copying passwordfile /etc/bandit_pass/$myname to /tmp/$mytarget\u0026#34; cat /etc/bandit_pass/$myname \u0026gt; /tmp/$mytarget The script runs as bandit23 and writes its password to /tmp/\u0026lt;md5 hash\u0026gt;. Reproduce the hash manually:\nbandit22@bandit:~$ echo I am user bandit23 | md5sum | cut -d \u0026#39; \u0026#39; -f 1 8ca319486bfbbc3663ea0fbe81326349 bandit22@bandit:~$ cat /tmp/8ca319486bfbbc3663ea0fbe81326349 [REDACTED] Notes\nThis level teaches script analysis, reading someone else\u0026rsquo;s code and understanding what it does well enough to reproduce its output. The key insight is that whoami returns the user running the script, so substituting bandit23 manually lets you predict where the file will be written. This kind of static analysis of scheduled scripts is exactly what you do when investigating persistence mechanisms on a compromised host.\nLevel 23 -\u0026gt; 24 # Level Goal\nA cron job runs scripts from a directory. Write your own script, place it there, and have it copy the password for you.\nSolution\n[greycipher@remnant ~]$ ssh bandit23@bandit.labs.overthewire.org -p 2220 bandit23@bandit:~$ cat /etc/cron.d/cronjob_bandit24 * * * * * bandit24 /usr/bin/cronjob_bandit24.sh bandit23@bandit:~$ cat /usr/bin/cronjob_bandit24.sh #!/bin/bash myname=$(whoami) cd /var/spool/$myname/foo echo \u0026#34;Executing and deleting all scripts in /var/spool/$myname/foo:\u0026#34; for i in * .*; do if [ \u0026#34;$i\u0026#34; != \u0026#34;.\u0026#34; -a \u0026#34;$i\u0026#34; != \u0026#34;..\u0026#34; ]; then echo \u0026#34;Handling $i\u0026#34; owner=\u0026#34;$(stat --format \u0026#34;%U\u0026#34; ./$i)\u0026#34; if [ \u0026#34;${owner}\u0026#34; = \u0026#34;bandit23\u0026#34; ]; then timeout -s 9 60 ./$i fi rm -f ./$i fi done The cron job executes any script in /var/spool/bandit24/foo owned by bandit23. Create a directory for the output, write the script, and wait for cron to run it:\nbandit23@bandit:~$ mkdir /tmp/myoutput bandit23@bandit:~$ chmod 777 /tmp/myoutput bandit23@bandit:~$ nano /var/spool/bandit24/foo/getpass.sh Script contents:\n#!/bin/bash cat /etc/bandit_pass/bandit24 \u0026gt; /tmp/myoutput/password bandit23@bandit:~$ chmod +x /var/spool/bandit24/foo/getpass.sh Wait up to one minute for cron to execute it, then read the output:\nbandit23@bandit:~$ cat /tmp/myoutput/password [REDACTED] Notes\nThis is the first level that requires writing your own shell script. The key points are making the output directory world-writable (777) so bandit24 can write to it, and making the script executable (+x) so cron can run it. The script gets deleted after execution, so keep a copy if you need to rerun it.\nLevel 24 -\u0026gt; 25 # Level Goal\nA daemon on port 30002 requires the current password and a secret 4-digit PIN. Brute-force all 10000 combinations.\nSolution\n[greycipher@remnant ~]$ ssh bandit24@bandit.labs.overthewire.org -p 2220 Write a script to generate all combinations and send them:\nbandit24@bandit:~$ mktemp -d /tmp/tmp.brute123 bandit24@bandit:~$ nano /tmp/tmp.brute123/brute.sh Script contents:\n#!/bin/bash password=\u0026#34;[REDACTED]\u0026#34; for pin in $(seq -w 0000 9999); do echo \u0026#34;$password $pin\u0026#34; done | nc localhost 30002 | grep -v \u0026#34;Wrong\u0026#34; bandit24@bandit:~$ chmod +x /tmp/tmp.brute123/brute.sh bandit24@bandit:~$ /tmp/tmp.brute123/brute.sh Correct! The password of bandit25 is: [REDACTED] Notes\nseq -w 0000 9999 generates zero-padded numbers from 0000 to 9999 All combinations are piped to a single nc connection rather than opening a new connection per attempt — the level hint says this is intentional and required grep -v \u0026quot;Wrong\u0026quot; filters out incorrect attempt responses so only the success message is shown This is a controlled brute force exercise — the same pattern (generate wordlist, pipe to service, filter output) appears in real-world credential stuffing and PIN brute force scenarios ","date":"29 March 2026","externalUrl":null,"permalink":"/posts/otw-bandit-5/","section":"Posts","summary":"Walkthrough of Levels 21–25 in the OverTheWire Bandit wargame. Learn how to read cron jobs, reverse-engineer shell scripts, write your own scripts, and brute-force a 4-digit PIN with a bash loop.","title":"Bandit | Levels 21-25 | Automation \u0026 Scheduling","type":"posts"},{"content":" Introduction # Levels 16 through 20 shift from data manipulation into networking and privileges mechanisms. You will scan for open ports, identify SSL services, compare files to find changes, bypass a tampered shell configuration, and use a setuid binary to read files as another user. These concepts map directly to real-world enumeration, persistence detection and privilege escalation.\nLevel 15 -\u0026gt; 16 # Level Goal\nThe password for the next level can be retrieved by submitting the password of the current level to port 30001 on localhost using SSL/TLS encryption.\nSolution\n[greycipher@remnant ~]$ ssh bandit15@bandit.labs.overthewire.org -p 2220 Plain nc does not support SSL. Use openssl s_client instead:\nbandit15@bandit:~$ openssl s_client -connect localhost:30001 [TLS handshake output...] --- [REDACTED] Correct! [REDACTED] If you see DONE, RENEGOTIATING, or KEYUPDATE after sending the password, add -ign_eof to the command: openssl s_client -connect localhost:30001 -ign_eof.\nNotes\nopenssl s_client is a diagnostic tool for testing SSL/TLS connection. It is the equivalent of nc but with encryption. In security work it is useful for inspecting certificates, testing cipher suites and verifying TLS configuration on servers. The difference between this level and the previous one, plain TCP vs SSL/TLS, is the same difference between HTTP and HTTPS.\nLevel 16 -\u0026gt; 17 # Level Goal\nSubmit the current password to a port in the range 31000-32000 on localhost. Only one post speaks SSL/TLS and will return credentials. The others echo back whatever you send.\nSolution\n[greycipher@remnant ~]$ ssh bandit16@bandit.labs.overthewire.org -p 2220 First scan the port range to find open ports:\nbandit16@bandit:~$ nmap -sV localhost -p 31000-32000 Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-03-22 15:32 UTC Nmap scan report for localhost (127.0.0.1) Host is up (0.00016s latency). Not shown: 996 closed tcp ports (conn-refused) PORT STATE SERVICE VERSION 31046/tcp open echo 31518/tcp open ssl/echo 31691/tcp open echo 31790/tcp open ssl/unknown 31960/tcp open echo Port 31790 speaks SSL and is not an echo service, that is the target:\nbandit16@bandit:~$ openssl s_client -connect localhost:31790 [TLS handshake output...] --- [REDACTED] Correct! -----BEGIN RSA PRIVATE KEY----- [REDACTED PRIVATE KEY] -----END RSA PRIVATE KEY----- Save the key and use it to connect as bandit17:\n[greycipher@remnant ~]$ chmod 600 key [greycipher@remnant ~]$ ssh -i key bandit17@bandit.labs.overthewire.org -p 2220 Notes\nnmap -sV detects service versions, not just open ports, crucial for distunguishing SSL from plain TCP services. SSH private key files must have permissions 600 (owner read/write only). SSH will refuse to use a key file with looser permissions. This level mirrors a real enumeration workflow: scan -\u0026gt; identify -\u0026gt; interesting service -\u0026gt; interact -\u0026gt; extract credentials. Level 17 -\u0026gt; 18 # Level Goal\nThere are two files: password.old and password.new. The password is the only line that changed between them.\nSolution\n[greycipher@remnant ~]$ ssh bandit17@bandit.labs.overthewire.org -p 2220 bandit17@bandit:~$ diff passwords.old passwords.new 42c42 \u0026lt; [OLD LINE] --- \u0026gt; [REDACTED] Notes\ndiff compares two files line by line and shows what changed. The \u0026lt; symbol marks lines from the first file, \u0026gt; marks lines from the second. In this output 42c42 means line 42 was changed, the \u0026gt; line is the new password. diff is a standard tool for change detection, log comparison and config auditing.\nLevel 18 -\u0026gt; 19 # Level Goal\nThe password is stored in a file called readme in the home directory. Someone modified .bashrc to log you out immediately on SSH login.\nSolution\nSince .bashrc runs on interactive login and immediately exits, pass the command directly to SSH instead of opening a shell:\n[greycipher@remnant ~]$ ssh bandit18@bandit.labs.overthewire.org -p 2220 cat readme [REDACTED] Notes\nSSH accepts a command as a trailing argument and executes it on the remote host without spawning an interactive shell, which means .bashrc never runs. This technique is useful any time a shell is restricted or modified. In incident response, a tampered .bashrc or .bash_profile is a common persistence mechanism worth checking when investigating a compromised account.\nLevel 19 -\u0026gt; 20 # Level Goal\nUse the setuid binary in the home directory to read the password from /etc/bandit_pass/bandit20.\nSolution\n[greycipher@remnant ~]$ ssh bandit19@bandit.labs.overthewire.org -p 2220 bandit19@bandit:~$ ls -la -rwsr-x--- 1 bandit20 bandit19 ... bandit20-do bandit19@bandit:~$ ./bandit20-do Run a command as another user. Example: ./bandit20-do id bandit19@bandit:~$ ./bandit20-do cat /etc/bandit_pass/bandit20 [REDACTED] Notes\nThe s in -rwsr-x--- us the setuid bit. When set on an executable, it runs with the permission of the file owner rather than the user who executes it. Here bandit-20-do is owned by bandit20, so any major attack surface in Linux privilege escalation, findind unexpected ones with find / -perm -4000 2\u0026gt;/dev/null is a standard step in any privilege escalation checklist.\n","date":"22 March 2026","externalUrl":null,"permalink":"/posts/otw-bandit-4/","section":"Posts","summary":"Walkthrough of Levels 16–20 in the OverTheWire Bandit wargame. Learn port scanning, SSL/TLS service identification, file diffing, bypassing modified shell configs, and Linux setuid binaries.","title":"Bandit | Levels 16-20 | Networking \u0026 Privileges","type":"posts"},{"content":" Introduction # Levels 10 through 15 introduce encoding schemes and data manipulation. You will decode base64, reverse ROT13 cipher, peel back multiple layers of compression, authenticate with an SSH key instead of a password, and send data directly to a network port. These are all skills that appread regularly in malware analysis and incident response.\nLevel 10 -\u0026gt; 11 # Level Goal\nThe password for the next level is stored in the file data.txt, which contains base64 encoded data.\nSolution\n[greycipher@remnant ~]$ ssh bandit10@bandit.labs.overthewire.org -p 2220 bandit10@bandit:~$ cat data.txt VGhlIHBhc3N3b3JkIGlzIGR0UjE3M2ZaS2IwUlJzREZTR3NnMlJXbnBOVmozcVJyCg== bandit10@bandit:~$ base64 -d data.txt The password is: [REDACTED] Notes\nBase64 is an encoding scheme that represents binary data as ASCII text. It is not encryption, anyone can decode it instantly. It shows up constantly in security work: email attachments, JWT tokens, obfuscated malware payloads, and encoded PowerShell commands all commonly use base64. Recognising it on sight (= padding at the end, alphanumeric characters) is a useful reflex.\nLevel 11 -\u0026gt; 12 # Level Goal\nThe password for the next level is stored in the file data.txt, where all lowercase (a-z) and uppercase (A-Z) letters have been rotated by 13 positions.\nSolution\n[greycipher@remnant ~]$ ssh bandit11@bandit.labs.overthewire.org -p 2220 bandit11@bandit:~$ cat data.txt Gur cnffjbeq vf [REDACTED] bandit11@bandit:~$ cat data.txt | tr \u0026#39;A-Za-z\u0026#39; \u0026#39;N-ZA-Mn-za-m\u0026#39; The password is: [REDACTED] Notes\nROT13 is a Caesar cipher that shifts each letter by 13 positions. Since the alphabet has 26 letters, applying ROT13 twice returns the original text, encoding and decoding are the same operation. tr translates characters by mapping an input set to an output set. The pattern 'A-Za-z' 'N-ZA-Mn-za-m' maps each letter to its ROT13 equivalent.\nLevel 12 -\u0026gt; 13 # Level Goal\nThe password for the next level is stored in the file data.txt, which is a hexdump of a file that has been repeatedly compressed.\nSolution\n[greycipher@remnant ~]$ ssh bandit12@bandit.labs.overthewire.org -p 2220 Create a working directory and copy the file there:\nbandit12@bandit:~$ mktemp -d /tmp/tmp.Xyz123abc bandit12@bandit:~$ cp data.txt /tmp/tmp.Xyz123abc/ bandit12@bandit:~$ cd /tmp/tmp.Xyz123abc Reverse the hexdump back to binary, then peel compression layers one by one using file to identify each format:\nbandit12@bandit:/tmp/tmp.XyZ123abc$ xxd -r data.txt \u0026gt; data bandit12@bandit:/tmp/tmp.XyZ123abc$ file data data: gzip compressed data bandit12@bandit:/tmp/tmp.XyZ123abc$ mv data data.gz \u0026amp;\u0026amp; gunzip data.gz bandit12@bandit:/tmp/tmp.XyZ123abc$ file data data: bzip2 compressed data bandit12@bandit:/tmp/tmp.XyZ123abc$ mv data data.bz2 \u0026amp;\u0026amp; bunzip2 data.bz2 bandit12@bandit:/tmp/tmp.XyZ123abc$ file data data: gzip compressed data bandit12@bandit:/tmp/tmp.XyZ123abc$ mv data data.gz \u0026amp;\u0026amp; gunzip data.gz bandit12@bandit:/tmp/tmp.XyZ123abc$ file data data: POSIX tar archive bandit12@bandit:/tmp/tmp.XyZ123abc$ mv data data.tar \u0026amp;\u0026amp; tar xf data.tar bandit12@bandit:/tmp/tmp.XyZ123abc$ file data5.bin data5.bin: POSIX tar archive bandit12@bandit:/tmp/tmp.XyZ123abc$ tar xf data5.bin bandit12@bandit:/tmp/tmp.XyZ123abc$ file data6.bin data6.bin: bzip2 compressed data bandit12@bandit:/tmp/tmp.XyZ123abc$ mv data6.bin data6.bz2 \u0026amp;\u0026amp; bunzip2 data6.bz2 bandit12@bandit:/tmp/tmp.XyZ123abc$ file data6 data6: POSIX tar archive bandit12@bandit:/tmp/tmp.XyZ123abc$ tar xf data6 bandit12@bandit:/tmp/tmp.XyZ123abc$ file data8.bin data8.bin: gzip compressed data bandit12@bandit:/tmp/tmp.XyZ123abc$ mv data8.bin data8.gz \u0026amp;\u0026amp; gunzip data8.gz bandit12@bandit:/tmp/tmp.XyZ123abc$ file data8 data8: ASCII text bandit12@bandit:/tmp/tmp.XyZ123abc$ cat data8 The password is: [REDACTED] Notes\nxxd -r reverses the hexdump back to binary. file is essential to understand the filetype and use the right tool. the key workflow for layered compressionis: file -\u0026gt; rename with correct extension -\u0026gt; decompress -\u0026gt; repeat. mktemp -d creates a uniquely named temporary directory. Level 13 -\u0026gt; 14 # Level Goal\nThe password for the next level is stored in /etc/bandit_pass/bandit14 and can only be read by user bandit14. You get a private SSH key to log into the next level instead of a password.\nSolution\n[greycipher@remnant ~]$ ssh bandit13@bandit.labs.overthewire.org -p 2220 OverTheWire doesn\u0026rsquo;t allow localhost login, to use the key you will have to log out, copy the key using SCP and then use that from the local machine to log in.\n[greycipher@remnant ~]$ scp -P 2220 bandit13@bandit.labs.overthewire.org:/home/bandit13/sshkey.private . [greycipher@remnant ~]$ chmod 700 sshkey.private [greycipher@remnant ~]$ ssh -i sshkey.private bandit14@bandit.labs.overthewire.org -p 2220 bandit14@bandit:~$ cat /etc/bandit_pass/bandit14 [REDACTED] Notes\nThe -i flag tells SSH to use a specific private key file instead of prompting for a password. Key-based authentication is the standard in production environment, most servers disable password authentication entirely and only accept keys. If you have not read the SSH introduction post yet, now is a good time.\nLevel 14 -\u0026gt; 15 # Level Goal\nThe password for the next level can be retrieved by submitting the password of the current level to port 30000 on localhost.\nSolution\n[greycipher@remnant ~]$ ssh bandit14@bandit.labs.overthewire.org -p 2220 bandit14@bandit:~$ echo \u0026#34;[REDACTED]\u0026#34; | nc localhost 30000 Correct! [REDACTED] Notes\nnc (netcat) opens a raw TCP connection to a host and port. It is often called the \u0026ldquo;Swiss Army Knife\u0026rdquo; of networking, you can use it to send data to services, listen for incoming connections, transfer files, and test port availability. echo pipes the password string directly into nc so it gets sent as soon as the connection opens.\n","date":"8 March 2026","externalUrl":null,"permalink":"/posts/otw-bandit-3/","section":"Posts","summary":"Walkthrough of Levels 10-15 in the OverTheWire Bandit wargame. Learn how to decode base64, reverse ROT13, decompress layered archives, use SSH keys and communicate with local network services.","title":"Bandit | Levels 10-15 | Encoding \u0026 Compression","type":"posts"},{"content":" Introduction # Levels 5 through 10 shift the focus from simply reading files to finding them. The challenges introduce filtering by file properties, searching within file contents, and working with encoded data.\nLevel 5 -\u0026gt; 6 # Level Goal\nThe password for the next level in stored in a file somewhere under the inhere directory and has all of the following properties:\nhuman-readable 1033 bytes in size not executable Solution\n[greycipher@remnant ~]$ ssh bandit5@bandit.labs.overthewire.org -p 2220 Instead of checking every file manually, find lets us filter by all three properties at once:\nbandit5@bandit:~$ find inhere/ -type f -readable ! -executable -size 1033c inhere/maybehere07/.file2 bandit5@bandit:~$ cat inhere/maybehere07/.file2 [REDACTED] Notes\nfind is one of the most useful commands on Linux. The flags used here:\n-type f limits results to files only. -readable matches files readable by the current user. ! -executable excludes executable files. -size 1033c matches exactly 1033 bytes (c stands for bytes). Level 6 -\u0026gt; 7 # Level Goal\nThe password for the next level is stored somewhere on the server and has all of the following properties:\nowned by user bandit7 owned by group bandit6 33 bytes in size Solution\n[greycipher@remnant ~]$ ssh bandit6@bandit.labs.overthewire.org -p 2220 The file could be anywhere on the server, so we search from the root. Redirecting errors to /dev/null keeps the output clean:\nbandit6@bandit:~$ find / -type f -user bandit7 -group bandit6 -size 33c 2\u0026gt;/dev/null /var/lib/dpkg/info/bandit7.password bandit6@bandit:~$ cat /var/lib/dpkg/info/bandit7.password [REDACTED] Notes\n-user and -group filter by file ownership. 2\u0026gt;/dev/null redirects stderr (permission denied errors) to /dev/null, discarding them so only valid results are shown. Searching from / covers the entire filesystem. Level 7 -\u0026gt; 8 # Level Goal\nThe password for the next level is stored in the file data.txt next to the word millionth.\nSolution\n[greycipher@remnant ~]$ ssh bandit7@bandit.labs.overthewire.org -p 2220 data.txt contains thousands of lines. grep finds the one we need instantly:\nbandit7@bandit:~$ grep \u0026#34;millionth\u0026#34; data.txt millionth\t[REDACTED] Notes\ngrep searches for a pattern inside a file and prints matching lines. It is one of the most used commands in log analysis and forensics, searching through large files for a specific string, IP, username, or keyword is a daily task in SOC work.\nLevel 8 -\u0026gt; 9 # Level Goal\nThe password for the next level is stored in the file data.txt and is the only line of text that occurs only once.\nSolution\n[greycipher@remnant ~]$ ssh bandit8@bandit.labs.overthewire.org -p 2220 sort organises identical lines together, then uniq -u filters to lines that appear exactly once:\nbandit8@bandit:~$ sort data.txt | uniq -u [REDACTED] Notes\nuniq only detects duplicates on adjacent lines, which is why sort must come first. The | pipe passes the output of one command directly into the next. -u tells uniq to print only lines with no duplicates. This pattern sort | uniq is a standard one-liner for deduplication and frequency analysis. Level 9 -\u0026gt; 10 # Level Goal\nThe password for the next level is stored in the file data.txt in one of the few human-readable strings, preceded by several = characters.\nSolution\n[greycipher@remnant ~]$ ssh bandit9@bandit.labs.overthewire.org -p 2220 data.txt is a binary file. strings extracts human-readable text from it, then grep filters for lines with =:\nbandit9@bandit:~$ strings data.txt | grep \u0026#34;===\u0026#34; ========== [REDACTED] Notes\nstrings extracts printable character sequences from any file, including binaries. This is one of the first steps in static malware analysis — running strings on a suspicious executable often reveals hardcoded URLs, registry keys, error messages, and other indicators before you even open a disassembler.\n","date":"8 March 2026","externalUrl":null,"permalink":"/posts/otw-bandit-2/","section":"Posts","summary":"Walkthrough of Levels 5–10 in the OverTheWire Bandit wargame. Learn how to search for files by properties, filter text with grep, sort and deduplicate data, extract readable strings from binary files, and decode base64.","title":"Bandit | Levels 5-10 | Finding \u0026 Filtering","type":"posts"},{"content":" Introduction # My Telegram group shares Instagram links constantly. Every time someone posts one, everyone has to leave Telegram, open Instagram, watch the reel, come back. Annoying enough that I decided to fix it.\nBots that do that already exist. I didn\u0026rsquo;t use them. A bot sitting in your group chat reads every message sent in it. I\u0026rsquo;m not interested in handling that to code I didn\u0026rsquo;t write and can\u0026rsquo;t read. So I built my own.\nHow It Works # The flow is straightforward:\nA message arrive in the group containing an Instagram URL The bot extracts the URL using a regex pattern It attempts to download the media via yt-dlp If the post is a photo, it falls back to instaloader The file gets uploaded directly to the chat Everything else in the code handles the edge cases around those five steps.\nThe Stack # Library Role python-telegram-bot Telegram API communication yt-dlp Video and reel downloads instaloader Photo post fallback python-dotenv Secret management Project Structure # tg-instagram-bot/ ├── main.py ├── Dockerfile ├── requirements.txt ├── .env.example ├── .env ├── .gitignore ├── LICENSE └── README.md Setup # 1. Create a Telegram Bot # Message @BotFather on Telegram Send /newbot and follow the prompts Copy the bot token you receive 2. Disable Privacy Mode # In BotFather send /setprivacy → select your bot → Disable. By default Telegram bots in groups only receive messages that start with /. Disabling privacy mode lets the bot read all messages so it can detect Instagram URLs anywhere in the chat.\n3. Install Dependencies # python3 -m venv .venv ./.venv/bin/activate pip install -r requirements.txt 4. Configure Secrets # cp .env.example .env Edit .env and fill in your values:\nTELEGRAM_BOT_TOKEN=your_token_here INSTAGRAM_COOKIES_FILE=/path/to/cookies.txt ALLOWED_CHAT_IDS=-1001234567890 5. Run # python main.py Self-Hosting # To run the bot persistently on a server:\nsudo apt update \u0026amp;\u0026amp; sudo apt install python3 python3-pip -y git clone https://github.com/GreyCipher-sec/tg-instagram-bot.git cd tg-instagram-bot python3 -m venv .venv ./.venv/bin/activate pip3 install -r requirements.txt cp .env.example .env nano .env Create the systemd service:\nsudo nano /etc/systemd/system/instabot.service [Unit] Description=Instagram Telegram Bot After=network.target [Service] WorkingDirectory=/home/user/tg-instagram-bot ExecStart=/home/user/tg-instagram-bot/.venv/bin/python main.py EnvironmentFile=/home/user/tg-instagram-bot/.env Restart=always User=user [Install] WantedBy=multi-user.target ExecStart must point to the virtualenv\u0026rsquo;s Python binary, not the system one. Using the system Python means your installed dependencies are not visible to the service.\nEnable and start it:\nsudo systemctl enable instabot sudo systemctl start instabot sudo systemctl status instabot Monitor live logs:\njournalctl -u instabot -f Docker Deploy # The repo includes a Dockerfile as an alternative to the systemd setup. It runs the bot as a non-root user (botuser) inside a python:3.12-slim container.\nBuild the image:\ndocker build -t instabot . Run it with your .env file passed in:\ndocker run -d --name instabot --env-file .env instabot The -d flag runs the container in the background. To check it is running and watch live logs:\ndocker ps docker logs -f instabot To stop and remove it:\ndocker stop instabot \u0026amp;\u0026amp; docker rm instabot Make sure your .env file is filled in before running the container, the bot will fail to start without a valid TELEGRAM_BOT_TOKEN.\nDocker is the cleaner option if you already have it on your server and want to avoid managing a virtualenv and systemd unit manually. Both approaches work, pick whichever fits your setup.\nInstagram Cookies (Private Posts \u0026amp; Rate Limiting) # By default the bot only downloads public posts. For private posts from accounts your Instagram account follows, or to reduce the chance of rate limiting, provide a cookies file:\nInstall the \u0026ldquo;Get cookies.txt LOCALLY\u0026rdquo; browser extension Log in to Instagram in your browser Export cookies to a file Set the path in .env: INSTAGRAM_COOKIES_FILE=/path/to/instagram_cookies.txt The authenticated account must follow the private profile for private post downloads to work.\nImplementation # Detecting Instagram URLs # INSTAGRAM_URL_PATTERN = re.compile( r\u0026#34;https?://(?:www\\.)?instagram\\.com/\u0026#34; r\u0026#34;(?:p|reel|reels|tv|stories)/[A-Za-z0-9_\\-]+/?(?:\\?[^\\s]*)?\u0026#34; ) def find_instagram_urls(text: str) -\u0026gt; list[str]: return INSTAGRAM_URL_PATTERN.findall(text) The pattern covers posts (/p/), reels and stories. Any matching URLs in the message get extracted and processed in order.\nDownloading Media # yt-dlp handles videos and reels. For photo posts it explicitly raises:\nExtractorError: There is no video in this post That error gets caught and routed to instaloader instead:\ndef download_media(url: str, output_dir: str) -\u0026gt; list[Path]: try: with yt_dlp.YoutubeDL(build_ydl_options(output_dir)) as ydl: info = ydl.extract_info(url, download=True) entries = info.get(\u0026#34;entries\u0026#34;) or [info] paths = [resolve_file_path(ydl, entry) for entry in entries if entry] return [p for p in paths if p is not None] except Exception as exc: if not is_photo_post_error(exc): raise logger.info(\u0026#34;Photo post detected, falling back to instaloader\u0026#34;) return download_images(url, output_dir) Secret Management # Credentials live in a .env file, loaded at runtime by python-dotenv:\nTELEGRAM_BOT_TOKEN=your_token_here INSTAGRAM_COOKIES_FILE=/path/to/cookies.txt ALLOWED_CHAT_IDS=-1001234567890 What Broke # Python version conflicts # Running Python 3.14 locally. python-telegram-bot calls asyncio.get_event_loop() which changed behaviour in 3.14 and raises:\nRuntimeError: There is no current event loop in thread \u0026#39;MainThread\u0026#39;. Fix:\nasyncio.set_event_loop(asyncio.new_event_loop()) Called once in main() before building the application.\nyt-dlp can\u0026rsquo;t download photos # yt-dlp is a video downloader. It raises an explicit error on photo posts rather than silently failing. The dix was catching that specific error string and routing to instaloader, which handles both photos and carousels properly.\nInstagram blocks every scraping approach # Before landing on instaloader, I tried two other approaches for photos:\nHTML scraping: requests fetches the page, regex looks for image URLs. Instagram serves a JavaScript shell. No data in the HTML.\noEmbed API: instagram.com/oembed/?url=... should return JSON with a thumbnail URL. It redirects to the same JavaScript shell.\nEvery approach Instagram had already anticipated. instaloader is a library specifically built to maintain this fight, it handles authentication, rate limiting and API changes. The right call was using it rather than trying to out-engineer Meta\u0026rsquo;s platform team.\nNotes # Private posts will fail regardless of cookies unless the authenticated account follows that profile. The bot returns a clean error message rather than crashing. yt-dlp breaks occasionally when Instagram changes its internals. pip install -U yt-dlp fixes it most of the time. Telegram bots are limited to 50MB per file via the Bot API. Anything larger gets a warning message instead. The Code # Full project on GitHub: GreyCipher-sec/tg-instagram-bot\n","date":"1 March 2026","externalUrl":null,"permalink":"/posts/ig-telegram-bot/","section":"Posts","summary":"A practical writeup on building Instabridge, a Telegram bot that automatically downloads and re-uploads Instagram media to a group chat. Covering the stack, setup, self-hosting, and what actually broke.","title":"Instabridge - Building an Instagram to Telegram Bot","type":"posts"},{"content":"","date":"1 March 2026","externalUrl":null,"permalink":"/categories/projects/","section":"Categories","summary":"","title":"Projects","type":"categories"},{"content":"If you are working through any wargame, CTF, or home lab, SSH is the first tool you need to be comfortable with. This post covers everything from basic connections to config aliases, the things you will actually use.\nWhat SSH Is # SSH (Secure Shell) is a protocol for connecting to remote machines over a network. It encrypts everything in transit, which means your commands and any data you send cannot be read by anyone intercepting the connection.\nWhen you connect to a remote server, a VPS, a lab machine, a wargame server — SSH is almost always how you do it.\nBasic Connection # ssh username@hostname If the server runs SSH on a non-standard port (default is 22), specify it with -p:\nssh username@hostname -p 2220 You will be prompted for a password. Type it and press Enter — the terminal will not show any characters while you type, that is normal.\nThe Known Hosts Warning # The first time you connect to a server you will see something like:\nThe authenticity of host \u0026#39;bandit.labs.overthewire.org\u0026#39; can\u0026#39;t be established. ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Are you sure you want to continue connecting (yes/no/[fingerprint])? This is SSH asking you to confirm you trust the server. Type yes and the server\u0026rsquo;s fingerprint gets saved to ~/.ssh/known_hosts. Next time you connect, SSH checks the fingerprint automatically, if it changed, SSH will warn you, which could indicate something is wrong.\nNever blindly accept fingerprints on machines you don\u0026rsquo;t control in a production context. For lab and wargame servers it is fine.\nPassword vs Key Authentication # SSH supports two ways to authenticate:\nPassword: you type a password when connecting. Simple but weaker. The server has to store password hashes and brute force is possible if the password is weak.\nKey-based: you generate a key pair (public + private). The public key goes on the server, the private key stays on your machine. No password is ever sent over the network. This is the standard for anything serious.\nGenerate a key pair:\nssh-keygen -t ed25519 -C \u0026#34;your comment here\u0026#34; This creates two files in ~/.ssh/:\nid_ed25519: your private key, never share this id_ed25519.pub: your public key, this goes on servers Copy your public key to a server:\nssh-copy-id username@hostname From that point on you connect without a password.\nUseful Flags # Flag What it does -p Specify port -i Specify a private key file -v Verbose output, useful for debugging connection issues -L Local port forwarding -N Do not execute a remote command, useful with -L -T Disable pseudo-terminal allocation The SSH Config File # Typing ssh bandit0@bandit.labs.overthewire.org -p 2220 every time gets old fast. The config file at ~/.ssh/config lets you define aliases for any host.\nCreate or edit the file:\n[greycipher@remnant ~]$ nano ~/.ssh/config Add an entry:\nHost bandit0 HostName bandit.labs.overthewire.org User bandit0 Port 2220 Now you connect with just:\n[greycipher@remnant ~]$ ssh bandit0 You can add as many entries as you want. For the Bandit series you could add one per level, or use a wildcard:\nHost bandit* HostName bandit.labs.overthewire.org Port 2220 Then ssh bandit0, ssh bandit1 and so on all work automatically — SSH fills in the hostname and port, you only specify the user via the host alias.\nCopying Files Over SSH # scp: copies files like cp but over SSH:\n# local to remote scp file.txt username@hostname:/remote/path/ # remote to local scp username@hostname:/remote/file.txt ./local/path/ sftp: interactive file transfer session:\nsftp username@hostname Once connected you get an sftp\u0026gt; prompt where you can ls, cd, get and put files.\nTroubleshooting # Connection refused: the server is not running SSH, the port is wrong, or a firewall is blocking it. Double check the port with -p.\nPermission denied: wrong username, wrong password, or your key is not on the server. Try -v to see exactly where authentication fails.\nHost key verification failed: the server\u0026rsquo;s fingerprint changed since you last connected. If you trust the server, remove the old entry from ~/.ssh/known_hosts and reconnect:\nssh-keygen -R hostname What\u0026rsquo;s Next # If you are working through OverTheWire Bandit, you now have everything you need to get started. SSH is also the foundation for tunneling, proxying, and remote port forwarding, topics worth exploring once the basics are solid.\n","date":"28 February 2026","externalUrl":null,"permalink":"/posts/ssh/","section":"Posts","summary":"A practical guide to SSH covering basic connections, key authentication, the config file, and common troubleshooting for Linux users.","title":"SSH - A Practical Introduction","type":"posts"},{"content":" Introduction # If you\u0026rsquo;re just getting started with Linux, cybersecurity, or Capture The Flag (CTF) challenges, the Bandit wargame from OverTheWire is one of the best hands-on introductions you can find. Designed specifically for beginners, Bandit walks you through the fundamentals of the Linux command line while subtly building the mindset needed for penetration testing and security research.\nIn this post, we’ll walk through Levels 0–4, breaking down not only how to solve them, but also why each command works.\nLevel 0 -\u0026gt; 1 # Level Goal\nThe password for the next level is stored in a file called readme located in the home directory. Use this password to log into bandit1 using SSH. Whenever you find a password for a level, use SSH (on port 2220) to log into that level and continue the game.\nSolution\nConnect to the server as bandit0:\n[greycipher@remnant ~]$ ssh bandit0@bandit.labs.overthewire.org -p 2220 Once inside, list the directory and read the file:\nbandit0@bandit:~$ ls readme bandit0@bandit:~$ cat readme Congratulations on your first steps into the bandit game!! Please make sure you have read the rules at [...] The password you are looking for is: [REDACTED] Notes\ncat reads the contents of a file and prints it to standard output. ls lists the files in the current directory. These are the two most basic commands you will use throughout this series.\nLevel 1 -\u0026gt; 2 # Level Goal\nThe password for the next level is stored in a file called - located in the home directory.\nSolution\nConnect to the server using the password previously acquired and the username bandit1:\n[greycipher@remnant ~]$ ssh bandit1@bandit.labs.overthewire.org -p 2220 From here list the directory and read the content of the target file:\nbandit1@bandit:~$ ls - bandit1@bandit:~$ cat ./- [REDACTED] Notes\nFiles starting with - are treated as flags. Use ./ to reference them as a path.\nLevel 2 -\u0026gt; 3 # Level Goal\nThe password for the next level is stored in a file called \u0026ndash;spaces in this filename\u0026ndash; located in the home directory.\nSolution\nUsing the username bandit2 and the password from the previous level connect to the server:\n[greycipher@remnant ~]$ ssh bandit2@bandit.labs.overthewire.org -p 2220 Read the content of the file by either using quotes \u0026quot;\u0026quot; or by referencing it as a path.\nbandit2@bandit:~$ ls --spaces in this filename-- bandit2@bandit:~$ cat ./--spaces\\ in\\ this\\ filename-- [REDACTED] Notes\nFilenames with spaces need to be quoted or escaped. Either wrap the whole name in quotes or use a backslash before each space: cat spaces\\ in\\ this\\ filename.\nLevel 3 -\u0026gt; 4 # Level Goal\nThe password for the next level is stored in a hidden file in the inhere directory.\nSolution\nUse the username bandit3 and the password from the previous level to connect to the server:\n[greycipher@remnant ~]$ ssh bandit3@bandit.labs.overthewire.org -p 2220 Once connected start looking around for the hidden files using the commands we already know.\nbandit3@bandit:~$ ls inhere bandit3@bandit:~$ cd inhere bandit4@bandit:~/inhere$ ls -a . .. ...Hiding-From-You bandit3@bandit:~/inhere$ cat ...Hiding-From-You [REDACTED] Notes\nHidden files on Linux start with a dot. ls skips them by default, use ls -a to show all files including hidden ones.\nLevel 4 -\u0026gt; 5 # Level Goal\nThe password for the next level is stored in the only human-readable file in the inhere directory. Tip: if your terminal is messed up, try the “reset” command.\nSolution\nWith the username bandit4 and the acquired password connect to the server:\n[greycipher@remnant ~]$ ssh bandit4@bandit.labs.overthewire.org -p 2220 Once inside we can see there are different files in the folders and checking every file by hand can be very slow, but knowing that the file we\u0026rsquo;re looking for is the only one which is also human-readable we can use the file command to check the type of data contained in a file.\nbandit4@bandit:~$ cd inhere/ bandit4@bandit:~/inhere$ ls -file00 -file01 -file02 -file03 -file04 -file05 -file06 -file07 -file08 -file09 bandit4@bandit:~/inhere$ file ./* ./-file00: data ./-file01: OpenPGP Public Key ./-file02: OpenPGP Public Key ./-file03: data ./-file04: data ./-file05: data ./-file06: data ./-file07: ASCII text ./-file08: data ./-file09: data bandit4@bandit:~/inhere$ cat ./-file07 [REDACTED] Notes\nfile identifies the type of data in a file without opening it. On a directory of unknowns, file ./* runs it against everything at once.\n","date":"28 February 2026","externalUrl":null,"permalink":"/posts/otw-bandit-1/","section":"Posts","summary":"A beginner-friendly walkthrough of Levels 0–4 in the Bandit wargame from OverTheWire. Learn essential Linux command-line skills, SSH basics, file navigation, and how to handle hidden and oddly named files, all while building your cybersecurity foundation through hands-on practice.","title":"Bandit | Levels 0-5 | Reading Files","type":"posts"},{"content":"Hello people, GreyCipher here.\nI\u0026rsquo;m building my foundation in cybersecurity, with a focus on defensive security and structured problem-solving. My work revolves around understanding systems, analyzing behavior, and expanding into DFIR, OSINT, reverse engineering, and tool development.\nWhat fascinates me is how complex environments collapse into clear patterns once you know where to look. How systems fail, how they can be read efficiently, how noise becomes signal.\nThis blog is a public notebook. I document what I study, test, and break, not as finished authority, but as work in progress.\nWriting forces clarity. Sharing keeps knowledge from dying in a private folder.\nCuriosity drives the rest.\n","date":"22 February 2026","externalUrl":null,"permalink":"/about/","section":" ","summary":"","title":"About","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"}]