Getting in tune with bash
Lately I've been learning bash, but this time the right way.
I've just completed this introduction course for Linux by Hack4U. At first I thought that this would be a breeze and that I'll complete it in one day if I really wanted, but boy was I surprised by how much work was actually waiting for me. Turns out being comfortable with something and actually being in tune with it are two very different things.
So, what really happened is that instead of one day, it took me two weeks. Not because I was lazy or something, but because of the sheer volume of practice, hands-on exercises and also all this new information that I did not know before. My naive brain thought that I already knew a pretty big amount of bash. I was perfectly comfortable reading it and analyzing it, but writing it was a totally different story.
As a first, I walked through all the basics, the flow controls, operators and background processes. Then came privileges, permissions, ownership and groups, all stuff I already knew from playing around with HTB machines when I first started dabbling in pentesting. But besides what I already knew, I also discovered many more things I didn't, sparking my thirst for learning more about this amazing environment, language, OS, you name it.
Bandit
After all the basic stuff, I ran through all the Bandit levels from OverTheWire, which challenged my existing skills and sent me looking for new ones a couple of times. The Bandit levels were genuinely the most fun I ever had with bash at that point, and I took the opportunity of refining my documenting abilities, which I did for each level.
An amazing example of some bash scripting beginnings I had during Bandit is this level, for which I had to write my own bash script to solve it. Here is a little code snippet:
#!/bin/bash
function ctrl_c (){
echo -e "\n\n[!] Exiting...\n"
exit 1
}
# Ctrl+C
trap ctrl_c INT
first_file_name="data.gz"
decompressed_file_name="$(7z l $first_file_name | tail -n 3 | head -n 1 | awk 'NF{print $NF}')"
7z x $first_file_name &>/dev/null
while [ $decompressed_file_name ]; do
echo -e "\n[+] Nuevo archivo descomprimido: $decompressed_file_name"
7z x $decompressed_file_name &>/dev/null
decompressed_file_name="$(7z l $decompressed_file_name 2>/dev/null | tail -n 3 | head -n 1 | awk 'NF{print $NF}')"
doneThis little bash script was written for level 12 -> 13. A quick run-through for this level:
There's this data file in the bandit12 home directory. This file is a compressed archive, inside a compressed archive and so on, but at first, it's just a hexdump.
So my first steps were to reverse the hexdump like this.
cat data | xxd -r > data.gzNow that I had this data.gz, I could let the script do the rest.
I declared two variables: first_file_name and decompressed_file_name. decompressed_file_name stores the name of the compressed archive nested inside the one we already have. Then we "LOOP IT!" (iykyk ;) ), while decompressed_file_name has a value, the loop executes.
Each pass grabs the archive name found inside the current archive, extracts that 'insider' archive, and sets it as the new 'target'. Then it checks: is this new target an archive too? If it is, the loop keeps going. The loop does not run infinitely, because at one point 7z x has no more archives to extract.
This is not the most graceful script or methodology, but it worked, and again, this was my first ever bash script that I wrote.
Deep dive into bash scripting
Once I was done with Bandit, in order to complete the course and get the cert, I had to write two different bash scripts as instructed.
The first one is a search script that looks through the HTB machines that S4vitar completed and posted on his page.
These machines have their proper difficulty categories, specific skills that each one touches, the links for the YouTube writeups, all registered on this page. What my code does is the following.
- Downloads the necessary file so all the searching happens locally, and has an update system.
- Searches the machine by name, which responds with all the characteristics of that machine.
- Searches by IP of the machines.
- Searches by their difficulty.
- Searches by OS.
- Searches by a specific skill.
- Filters that show the machines that touch Active Directory.
- Fetches YouTube links for specific machines.
I will post this script onto GitHub eventually for everybody to use as they please.
The second one was a roulette simulator.
Neither I nor this script encourages gambling in any way, shape or form. Its purpose is to prove the exact opposite.
For this one, I scripted a simple roulette simulator which showcases that the house always wins.
[!] It does not have any specific number selection, color selection or section selection. At this moment it has only Odd/Even selection for 'bets'.
Betting follows one of two well-known techniques, out of many known in the industry: Martingale and Reverse Labouchère.
Martingale is the simple one. You double your bet every time you lose, and reset back to your starting bet the moment you win. The idea is that one win, whenever it finally shows up, recovers everything you lost plus your original stake. It sounds foolproof until you watch it happen for real: a losing streak doesn't grow your bet linearly, it grows it exponentially, and it doesn't take many losses in a row before you're looking at a bet size that blows past either your bankroll or the table limit. My script enforces a $2 minimum and a $1000 maximum bet for exactly this reason. Once doubling would push past the cap, the session just ends. No pretending the streak is about to turn around.
Reverse Labouchère was the more interesting one to build. The classic version has you keep a sequence of numbers, bet the sum of the first and last, and add the lost amount back onto the sequence every time you lose, basically chasing the debt. Mine does the opposite. On a loss it shrinks the sequence by removing the first and last numbers, and on a win it grows the sequence by adding the amount you just won. So instead of betting more to dig out of a hole, you bet less when things are going badly and more when they're going well. I also added a profit_play_trigger: once your money climbs 5% above where it last reset, the sequence snaps back to its starting point and the trigger moves up another 5%. It's a small way of locking in gains instead of letting a lucky streak talk you into betting bigger forever.
Neither system beats the house, though, and the script proves it in a way that's honestly kind of satisfying to watch. Every spin comes from $RANDOM % 37, which produces the numbers 0 through 36. Odd or even, red or black, doesn't matter, 0 belongs to neither. That one number is the entire house edge sitting quietly in a modulo operation, and it's the reason "the house always wins" isn't just a line I bolted onto this post. It's the actual math the script runs on every single spin.
I also built out a full stats screen (playStatisticts, yes I misspelled "Statistics" in the function name and only caught it while writing this post) that tracks how many times you played, your longest win and loss streaks, how much money you bet in total, and how many times you specifically got hit by that 0. Run either technique long enough and the pattern is always the same. You can survive for a while, sometimes even build up a nice cushion, but the 0 finds you eventually.
Between Bandit and these two scripts, this course turned into less of a syntax lesson and more of an exercise in actually thinking in bash, the difference between reading a script and knowing, before you run it, what it's about to do. That's what took two weeks instead of one day, and it's not something a crash course hands you for free.
Neither script is finished, and I'm okay with that. The roulette simulator still only knows odd and even. Color, number and section betting are the obvious next additions, though the more interesting problem is probably refactoring the bet-resolution logic before I bolt three more bet types onto two already long if/elif chains. The HTB search script is going up on GitHub once I'm happy with it. Both are staying open on my machine, not because they're done, but because I finally feel comfortable enough in bash to keep pulling at them.
That's really what these two weeks turned into. Getting in tune with it, one messy, occasionally rambling script at a time.