HTB: Ethereal Attacking Password Box
For Ethereal, I found a DOS application, pbox.exe
, and a pbox.dat
file. These were associated with a program called PasswordBox, which was an early password manager program. To solve this box, most people likely just guessed the password, “password”. But what if I had needed to brute force it? The program was not friendly to taking input from stdin, or from running inside python. So I downloaded the source code, installed the FreeBasic compiler, and started hacking at the source until it ran in a way that I could brute force test 1000 passwords in 5 seconds. I’ll walk through my steps and thought process in this post.
Prep
In order to create a modified pbox binary, I needed to figure out how to compile Basic. I’ll be working from my Kali box.
Get Source
I got the source code from here, and unzip it:
root@kali# unzip pbox011-src.zip
Archive: pbox011-src.zip
inflating: changes.txt
inflating: license.txt
inflating: pbox.bas
inflating: pbox.ico
extracting: pbox.png
inflating: pbox.rc
inflating: pbox.svg
inflating: pbox.txt
inflating: reprint.bi
inflating: rijndael.bi
inflating: throwmsg.bi
inflating: wordwrap.bi
Install FreeBasic
First, I’ll make sure I have all the prerequisites for the compiler:
apt install binutils gcc make lib{ncurses5,gpm,x11,xext,xpm,xrandr,xrender,gl1-mesa,ffi}-dev
Next, I’ll get the software from the SourceForge page. Decompress, and run the installer:
root@kali# tar xzf FreeBASIC-1.06.0-linux-x86_64.tar.gz
root@kali# ./install.sh
usage:
./install.sh -i [prefix] install FB into prefix directory
./install.sh -u [prefix] uninstall FB from prefix directory
(default prefix: /usr/local)
root@kali# ./install.sh -i
FreeBASIC compiler successfully installed in /usr/local
Verify Compile
I’ll try to compile the software as is, before I start messing with it. Unfortunately, I get some errors:
root@kali# fbc pbox.bas
/media/sf_CTFs/hackthebox/ethereal-10.10.10.106/temp/rijndael.bi(209) error 148: Suffixes are only valid in -lang fb or deprecated or fblite in 'T(x,y) = Asc(Mid$(text,(((y-1) Shl 2) + x),1))'
/media/sf_CTFs/hackthebox/ethereal-10.10.10.106/temp/rijndael.bi(215) error 148: Suffixes are only valid in -lang fb or deprecated or fblite in 'T(y,x) = Asc(Mid$(text,(((y-1) Shl 2) + x),1))'
/media/sf_CTFs/hackthebox/ethereal-10.10.10.106/temp/rijndael.bi(235) error 148: Suffixes are only valid in -lang fb or deprecated or fblite in 's += Chr$(T(x,y))'
/media/sf_CTFs/hackthebox/ethereal-10.10.10.106/temp/rijndael.bi(247) error 148: Suffixes are only valid in -lang fb or deprecated or fblite in 'ftext += Hex$(Asc(Mid$(convstr,i,1)),2)'
/media/sf_CTFs/hackthebox/ethereal-10.10.10.106/temp/rijndael.bi(259) error 148: Suffixes are only valid in -lang fb or deprecated or fblite in 'f += Chr$(Val("&H"+Mid$(convstr,i,2)))'
/media/sf_CTFs/hackthebox/ethereal-10.10.10.106/temp/rijndael.bi(320) error 148: Suffixes are only valid in -lang fb or deprecated or fblite in 'ptext += Chr$(0)'
/media/sf_CTFs/hackthebox/ethereal-10.10.10.106/temp/rijndael.bi(331) error 148: Suffixes are only valid in -lang fb or deprecated or fblite in 'mtext = Mid$(ptext, ((i-1) Shl 4) + 1, 16)'
/media/sf_CTFs/hackthebox/ethereal-10.10.10.106/temp/rijndael.bi(339) error 148: Suffixes are only valid in -lang fb or deprecated or fblite in 'mtext = Mid$(ptext, ((i-1) Shl 4) + 1, 16)'
Looking at the man page for fbc
, I see:
-lang name Select FB dialect: fb (default), deprecated, qb
I’ll try with -lang deprecated
, and it works:
root@kali# fbc pbox.bas -lang deprecated
root@kali# ./pbox
Enter your master password:
pbox Overview
The pbox
binary (in my case, elf) is pretty simple. I covered a bit of the overview in the Ethereal walkthrough. After putting the pbox.dat
file into /root/.pbox.dat
, I can start the program, and it will prompt for the password:
root@kali# ./pbox
Enter your master password:
I can also run it with the --dump
option, which will change some behavior once I know the password.
Source Code Analysis
Main Program
Other than analysis of VBA in Word document macros, I have no experience with Basic. I’m going to look through the source and see what I can figure out. I know there’s a prompt “Enter your master password:”, so I’ll search for that, and look around it. A bit up from that prompt, I see a comment (which apparently is REM
and also '
in Basic) that says REM * * * Here begins the main program * * *
. That section looks like:
REM * * * Here begins the main program * * *
DIM AS INTEGER x, SelectedEntry = 1, FirstDisplayedEntry = 1
DIM AS STRING LastKey
IF LEN(COMMAND(2)) > 0 THEN About()
IF LEN(COMMAND(1)) > 0 AND LCASE(COMMAND(1)) <> "--dump" THEN About()
ConfigFile = GetConfFile()
DebugOut("ConfigFile = " + ConfigFile)
SELECT CASE CheckDataFile()
CASE 0
PRINT "Invalid datafile. Program aborted."
END(2)
CASE -1
PRINT "No database have been found. Your encrypted database will be initialised now."
PRINT "The database will be stored at the following location:"
PRINT GetConfFile()
PRINT
PRINT "Choose a master password: ";
PassPhrase = GetText("", 30, 0)
IF LEN(PassPhrase) = 0 THEN PRINT "Invalid master password! (can't be empty)." : END(1)
PassPhrase = LEFT((PassPhrase & CHR(3,141,59,26,53,58,97,93,238,46,26,43,38,32,79,50,28,8)), 16)
ModFlag = 1
CASE 1
PRINT "Enter your master password: ";
PassPhrase = GetText("*", 30, 0) ' Hash with "*", max 30 chars, EscapeChar not allowed
PRINT ' Carriage return
IF LEN(PassPhrase) = 0 THEN SLEEP 2000, 1 : PRINT "Password rejected." : END(1)
PassPhrase = LEFT((PassPhrase & CHR(3,141,59,26,53,58,97,93,238,46,26,43,38,32,79,50,28,8)), 16)
IF CheckPassPhrase() <> 1 THEN SLEEP 2000, 1 : PRINT "Password rejected." : END(1)
LoadBox()
END SELECT
Here’s what I see that code doing:
- Verifies the command line arguments, and if there are 2, or if there is 1 and it isn’t
--dump
then callAbout()
.COMMAND(#)
seems to refer to command line arguments. - Calls
CheckDataFile()
which presumably returns 0 for a bad file, -1 if it doesn’t exist, and 1 if it does. - Switches on the result of
CheckDataFile()
. In the case I’m interesting in, where the file exists, the return is one, and I’ll continue on that path. - Prints “Enter your master password: “.
- Calls
GetText("*", 30, 0)
and store the result inPassPhrase
. There’s a comment about hashing with “*”, max 30 characters, and no escape chars. I’ll need to check what “hash” means here. - If the length of
PassPhrase
is 0, sleep for two seconds, print rejection message, and exit. - Append bytes to the end of the string, and then take the first 16 characters.
- Call
CheckPassPhrase()
, and if it isn’t 1, then sleep for two seconds, print rejection message, and exit. - Call the
LoadBox()
function.
CheckPassPhrase
The CheckPassPhrase
function is a good place to start looking. Here it is:
FUNCTION CheckPassPhrase() AS BYTE
REM Returns 1 if the Passphrase decrypted the file's header properly, 0 otherwise.
DIM AS BYTE Result = 0
DIM AS STRING LineBuff = SPACE(16), DecryptedHeader
OPEN ConfigFile FOR BINARY AS #1
GET #1, 13, LineBuff
CLOSE #1
DebugOut("Checking given password...")
DecryptedHeader = RIJNDAEL_Encrypt(LineBuff, PassPhrase, 2)
DebugOut("DecryptedHeader = " + DecryptedHeader)
IF LEFT(DecryptedHeader, 10) = "Monika <3 " THEN Result = 1
RETURN Result
END FUNCTION
I think this function:
- Creates a 16 byte buffer of spaces.
- Reads the first 13 bytes of the config file into that buffer.
- Calls
RIJNDAEL_Encrypt
, giving it the buffer, the password, and the argument 2 (which I’ll guess means decrypt). - If the first ten characters of the decrypted header are “Monika <3 “, then it sets the return value to 1.
Were I better at crypto, I suspect it’d be really easy to write a brute forcer or even some kind of pbox2john
like function. That’s not my best area, so I’ll leave that for now.
Modifying the Code
Password As Command Line Arg
I need some way to pass in the password automatically. In the original state, I can’t pass in command lines from a loop. I’m going to edit the code so that it takes the password as the argument.
GetText
Since I’m going to replace GetText
, I want to make sure I know what it is returning. Since there’s a reference to hashing with “*”, I could see that being some kind of mangling of the string, or just hiding the input from the screen with *s. I’ll add a simple line immediately after it’s called to print it back to me:
PassPhrase = GetText("*", 30, 0) ' Hash with "*", max 30 chars, EscapeChar not allowed
PRINT ' Carriage return -- 0xdf added
PRINT PassPhrase ' -- 0xdf added
PRINT ' Carriage return
Now when I run it, I’ll see that my input, 0xdf0xdf
, comes back:
root@kali# fbc -lang deprecated pbox.bas
root@kali# ./pbox
Enter your master password: ********
0xdf0xdf
Password rejected.
Now that I understand the call, I’ll remove those two added lines.
Remove Check on Args
I’ll comment out the check that requires the first arg be nothing or --dump
so that I can make that my password:
IF LEN(COMMAND(2)) > 0 THEN About()
REM IF LEN(COMMAND(1)) > 0 AND LCASE(COMMAND(1)) <> "--dump" THEN About()
Replace GetText with Arg
Next, I’m going to comment out the call to GetText()
, and set PassPhrase
to be COMMAND(1)
, which seems to be the first arg passed in:
REM PRINT "Enter your master password: ";
REM PassPhrase = GetText("*", 30, 0) ' Hash with "*", max 30 chars, EscapeChar not allowed
PassPhrase = COMMAND(1)
REM PRINT ' Carriage return
Now I can test:
root@kali# fbc -lang deprecated pbox.bas
root@kali# time ./pbox 0xdf
Password rejected.
real 0m2.019s
user 0m0.000s
sys 0m0.007s
Success. I didn’t enter anything. It tested my input, failed, slept for two seconds, and exited.
Remove the Sleeps
If I’m going to brute force on this code, I need to take out these sleeps. That’s easy enough. I’ll comment out the original lines with sleep and message, and add a line that just ends on bad password:
REM IF LEN(PassPhrase) = 0 THEN SLEEP 2000, 1 : PRINT "Password rejected." : END(1)
IF LEN(PassPhrase) = 0 THEN END(1)
PassPhrase = LEFT((PassPhrase & CHR(3,141,59,26,53,58,97,93,238,46,26,43,38,32,79,50,28,8)), 16)
REM IF CheckPassPhrase() <> 1 THEN SLEEP 2000, 1 : PRINT "Password rejected." : END(1)
IF CheckPassPhrase() <> 1 THEN END(1)
Here’s my running the program, giving a blank password before and after that change:
root@kali# time ./pbox 0xdf
real 0m0.006s
user 0m0.003s
sys 0m0.000s
Perfect, it worked.
Message On Success
Currently, I have a program that takes a password as a command line argument, and immediately and silently exits on failure. I just want to update what happens on success. Instead of loading the box, I just want it to print the password it found and exit.
IF CheckPassPhrase() <> 1 THEN END(1)
PRINT "Password Found: ", COMMAND(1)
END(0)
LoadBox()
I’ll print the password as the argument passed in instead of PassPhrase
because the one gets things appended to it. I’ll also have it exit with a 0 for success, as opposed to the END(1)
exits for the failures above.
Brute Force It
Success
Now I’ll write a bash loop to check. Since failure prints nothing, and success prints the password, I can do this:
root@kali# time for pass in $(cat /usr/share/seclists/Passwords/darkweb2017-top1000.txt); do ./pbox $pass; done
Password Found: password
real 0m5.808s
user 0m1.593s
sys 0m1.096s
In under 6 seconds, it checks 1000 passwords.
Better Success
But I can do better. Since the program returns 0 on success and 1 on failure, I can use that. I can use and (&&
) and/or or (||
) to chain commands together based on the return values.
Bash is weird in that a return value of 0 is success. So if I have ./a && ./b
, it will only run ./b
if ./a
returns success, which is 0.
I’ll add an && break
to my loop. If the program is successful (returns 0), the it will also run break and exit the loop. That way I don’t have to keep checking passwords after I find the right one.
Now I can run in less than 0.03 seconds:
root@kali# time for pass in $(cat /usr/share/seclists/Passwords/darkweb2017-top1000.txt); do ./pbox $pass && break; done
Password Found: password
real 0m0.029s
user 0m0.011s
sys 0m0.004s