Execute ‘getflag’ to get the flag.

Developing the Solution

After connecting to the server, we are in a bash. Let’s try getflag:

bash: cannot set terminal process group (21183): Inappropriate ioctl for device
bash: no job control in this shell
bash-5.0$ getflag
There are still 13 locks locked. No flag for you.

But what exactly is getflag anyway? Let’s find out:

bash-5.0$ type getflag
getflag is a shell builtin

This is interesting. They modified the bash and added a new builtin. We need that bash binary to figure out what’s going on, so let’s just dump it:

echo 'gzip -ck9 /bin/bash | base64' | nc 5000 > bash.gz.b64

After removing the first few lines of output as well as the bash prompt at the end, and decompressing the whole thing, we have the bash binary. Let’s run it locally:

% ./ooobash
[error] token not found

Weird. This shell wants to read a token file? Where is it located? Let’s figure it out with strace:

openat(AT_FDCWD, "/etc/ooobash/token", O_RDONLY) = -1 ENOENT (No such file or directory)

I guess we need that file. Let’s extract it with the same method:

% echo 'cat /etc/ooobash/token | base64 -w0' | nc 5000
bash: cannot set terminal process group (5001): Inappropriate ioctl for device
bash: no job control in this shell
bash-5.0$ cat /etc/ooobash/token | base64 -w0
cat: /etc/ooobash/token: Permission denied

Now that’s interesting. But the bash did read it. Let’s try again:

% echo 'cat /etc/ooobash/token | base64 -w0' | nc 5000
bash: cannot set terminal process group (21216): Inappropriate ioctl for device
bash: no job control in this shell
bash-5.0$ cat /etc/ooobash/token | base64

Weird. There seems to be some race condition, and if we are lucky, we can read the file. Good. But maybe there is something else in that /etc/ooobash directory? Let’s find out:

% echo 'ls /etc/ooobash' | nc 5000
bash: cannot set terminal process group (5022): Inappropriate ioctl for device
bash: no job control in this shell
bash-5.0$ ls /etc/ooobash

So let’s extract the other two files too.

The flag file (base64):


The state file (again base64):


Decoding those files only gives us seemingly random data, so how do we use them? Clearly the ooobash binary knows how to do it, so let’s reverse engineer that getflag builtin. The code of the builtin:

int getflag_builtin()
	FILE* file;
	size_t flaglen;
	char* flag;
	char zero[32];
	char out[104];

	memset(zero, 0, 32);
	if(!memcmp(oootoken, zero, 32)) {
		printf("[error] you need to execute this on the remote server\n");
	} else if(leftnum <= 0) {
		file = fopen("/etc/ooobash/flag", "rb");
		if(!file) {
		fseek(file, 0, 2);
		flaglen = ftell(file);
		fseek(file, 0, 0);
		flag = (char *)malloc(flaglen + 1);
		fread(flag, 1, flaglen, file);
		flag[flaglen] = 0;

		out[aes_decrypt(flag + 16, flaglen - 16, ooostate, flag, out)] = 0;
		printf("You are now a certified bash reverser! The flag is %s\n", out);
	} else {
		printf("There are still %d locks locked. No flag for you.\n", leftnum);
	return 0;

This shell builtin checks if all locks are unlocked, and then reads the flag, decrypts it with AES, and prints the result.

But what’s that ooostate? Looking at the xrefs, we find another function, which seems to initialize the ooostate:

void init_ooostate()
	FILE* f;
	int i;

	memset(ooostate, 0, 32);
	f = fopen("/etc/ooobash/state", "rb");
	if(!f) {
		printf("[error] state not found\n");
	fread(ooostate, 32, 1, f);

	for(i = 0; i < LOCKSNUM; i++) {
		locks[i] = 1;

And directly next to that function, there is another one:

void init_oootoken()
	FILE* f;

	memset(oootoken, 0, 32);
	f = fopen("/etc/ooobash/token", "rb");
	if(!f) {
		printf("[error] token not found\n");
	fread(oootoken, 32, 1, f);

Those init functions read the token and the state. So all we have to do now is look at the aes_decrypt function, which looks like this:

int aes_decrypt(char* data, int len, char* key, char* iv, char* out)
	const EVP_CIPHER* type;
	int outm;
	int outl;

	if(!(ctx = EVP_CIPHER_CTX_new()))
	type = EVP_aes_256_cbc();
	if(EVP_DecryptInit_ex(ctx, type, 0, key, iv) != 1)
	if(EVP_DecryptUpdate(ctx, out, &outl, data, len) != 1)
	if(EVP_DecryptFinal_ex(ctx, &out[outl], &outm) != 1)

	return outl + outm;

It’s just a normal AES decryption routine, which decrypts the flag with the ooostate.

So let’s try to just make a simple C program which reads the three files and calls aes_decrypt, without all the lock stuff. Turns out that doesn’t work and we get an error:

140595290311552:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt:crypto/evp/evp_enc.c:583:

Maybe we overlooked something? Yeah, we overlooked the oootoken which isn’t used so far, and we didn’t realize that the ooostate is modified whenever a lock is unlocked.

void update_ooostate(char* keyword, unsigned int idx)
	size_t len;
	int i;
	char hash[32];
	char buf[164];

	assert(strlen(keyword) < 100);
	assert(idx < LOCKSNUM);
	if(locks[idx]) {
		locks[idx] = 0;
		printf("unlocking %s (%d)\n", keyword, idx);
		len = strlen(keyword);
		memcpy(buf, oootoken, 32);
		memcpy(buf + 32, keyword, len + 1);
		memcpy(buf + 32 + len, oootoken, 32);
		SHA256(buf, len + 64, hash);
		for(i = 0; i < 32; i++)
			ooostate[i] ^= hash[i];
	} else {
		printf("lock %d was already unlocked\n", idx);

Now we just need those keywords. We can easily find them by checking all xrefs to update_ooostate. Those are the calls:

update_ooostate("unlockbabylock", 0);
update_ooostate("badr3d1r", 1);
update_ooostate("verysneaky", 2);
update_ooostate("leetness", 3);
update_ooostate("vneooo", 4);
update_ooostate("eval", 5);
update_ooostate("ret", 6);
update_ooostate("n3t", 7);
update_ooostate("sig", 8);
update_ooostate("yo", 9);
update_ooostate("aro", 10);
update_ooostate("fnx", 11);
update_ooostate("ifonly", 12);

If we add this to our decoding program, after init_ooostate and init_oootoken, but before reading/decoding of the flag, we finally get the flag:

You are now a certified bash reverser! The flag is OOO{r3VEr51nG_b4sH_5Cr1P7s_I5_lAm3_bU7_R3vErs1Ng_b4SH_is_31337}

The complete program code is available here: solve.c

… and this is how you solve a pwn challenge without pwn. This was obviously not the intended solution, and it only worked because of the race condition which allowed us to sometimes read the key files.