Continuing from this post: The Python Challenge (Levels 6-13)!
More on the Python Challenge!
Let me set the authentication variable here for later use:
auth = ('huge', 'file')
Level 14¶
https://huge:file@www.pythonchallenge.com/pc/return/italy.html
Title: walk around
Below that image is another one (wire.png
) which, when I try to download it and put it here, does not show at all. Here is a screenshot of it:
When I open the image on a separate tab, the browser told me that this image is “10000×1”. Looking back on the page source, the image is written with width="100" height="100"
to set its size, and there is a hint:
<!-- remember: 100*100 = (100+99+99+98) + (... -->
100+99+99+98? That is weird. Thinking about the title “walk around”, and the main image of a curl-y piece of bread (I don’t know the name of that…), I thought of something.
If we think of the 10000×1 image as a wire and curl it into a square shape, with 100 pixels on each side, won’t it become a 100×100 image?
Let’s try it!
import io
from PIL import Image
import requests
r = requests.get('http://www.pythonchallenge.com/pc/return/wire.png', auth=auth)
im = Image.open(io.BytesIO(r.content))
im2 = Image.new('RGB', (100,100))
m = 0 # 0=right 1=down 2=left 3=right
s = 100 # length of current side
d = 99 # length to go
x,y = 0,0
i = 0
while s:
im2.putpixel((x,y), im.getpixel((i,0)))
if m == 0:
x += 1
elif m == 1:
y += 1
elif m == 2:
x -= 1
else:
y -= 1
d -= 1
if d == 0:
if m == 0 or m == 2:
s -= 1
d = s
m = (m+1) % 4
i += 1
im2
It’s… a cat.
http://www.pythonchallenge.com/pc/return/cat.html:
and its name is uzi. you’ll hear from him later.
Level 15¶
http://huge:file@www.pythonchallenge.com/pc/return/uzi.html
Title: whom?
In the source:
<!-- he ain't the youngest, he is the second -->
<!-- todo: buy flowers for tomorrow -->
Sure, so something happened on Jan. 27, 1XX6, involves someone who is second youngest in their family, and we should buy flowers for them?
Googling “jan 27”, I found the Wikipedia article (Wikipedia hooray!) about this day! Going down the events, most of them I don’t know. However, Wolfgang Amadeus Mozart is someone I have remotely heard of. He is the second youngest, and his birth would certainly require me to buy flowers!
Level 16¶
http://huge:file@www.pythonchallenge.com/pc/return/mozart.html
Title: let me get this straight
Alright, it’s an image that looks like garbage. However, there is a pink stripe on (what seems like) each line of pixels, and the title of the page tells me to get it straight. So I should move the line so that the pink things line up in the middle or somewhere?
r = requests.get('http://www.pythonchallenge.com/pc/return/mozart.gif', auth=auth)
im = Image.open(io.BytesIO(r.content))
w = im.width
mid = w // 2 + 2
im2 = Image.new('P', (w*2 + 10, im.height))
im2.putpalette(im.getpalette())
P = 195 # Palette index of the pink color, I used GIMP to get it
def check(x, y):
try:
return all(im.getpixel((x+i,y))==P for i in range(1, 6))
except IndexError:
return False
for y in range(im.height):
for x in range(im.width):
if check(x,y):
shift = mid - x
for i in range(im.width):
im2.putpixel((shift+i, y), im.getpixel((i,y)))
im2
It’s “romance”!
Level 17¶
http://huge:file@www.pythonchallenge.com/pc/return/romance.html
Title: eat?
It’s so clear what the image is hinting at. I hope you’ve all heard of cookies!
An HTTP cookie (web cookie, browser cookie) is a small piece of data that a server sends to a user’s web browser. The browser may store the cookie and send it back to the same server with later requests.
Source: MDN HTTP cookies docs
And sure enough, when I look inside my cookies, I see something:
The cookie has the value: “you should have followed busynothing…” (In URL-encoded strings, %20 represents a space character.)
Alright, this, combined with the hint in the image back to the linked list level, means one thing: let’s go back to linked listing!
n = '12345'
while True:
try:
r = requests.get('http://www.pythonchallenge.com/pc/def/linkedlist.php?busynothing=%s' % n)
except Exception as e:
print('ERROR:', repr(e))
break
print(r.text, end=' ')
n = r.text.rpartition(' ')[-1]
print('nothing:', n)
if not n.isdigit():
print('nothing is not digit!')
print(n)
break
If you came here from level 4 - go back!<br>You should follow the obvious chain...<br><br>and the next busynothing is 44827 nothing: 44827 and the next busynothing is 45439 nothing: 45439 and the next busynothing is 94485 nothing: 94485 and the next busynothing is 72198 nothing: 72198 and the next busynothing is 80992 nothing: 80992 and the next busynothing is 8880 nothing: 8880 and the next busynothing is 40961 nothing: 40961 and the next busynothing is 58765 nothing: 58765 and the next busynothing is 46561 nothing: 46561 and the next busynothing is 13418 nothing: 13418 and the next busynothing is 41954 nothing: 41954 and the next busynothing is 46782 nothing: 46782 and the next busynothing is 92730 nothing: 92730 and the next busynothing is 89229 nothing: 89229 and the next busynothing is 25646 nothing: 25646 and the next busynothing is 74288 nothing: 74288 and the next busynothing is 25945 nothing: 25945 and the next busynothing is 39876 nothing: 39876 and the next busynothing is 8498 nothing: 8498 and the next busynothing is 34684 nothing: 34684 and the next busynothing is 62316 nothing: 62316 and the next busynothing is 71331 nothing: 71331 and the next busynothing is 59717 nothing: 59717 and the next busynothing is 76893 nothing: 76893 and the next busynothing is 44091 nothing: 44091 and the next busynothing is 73241 nothing: 73241 and the next busynothing is 19242 nothing: 19242 and the next busynothing is 17476 nothing: 17476 and the next busynothing is 39566 nothing: 39566 and the next busynothing is 81293 nothing: 81293 and the next busynothing is 25857 nothing: 25857 and the next busynothing is 74343 nothing: 74343 and the next busynothing is 39410 nothing: 39410 and the next busynothing is 5505 nothing: 5505 and the next busynothing is 27104 nothing: 27104 and the next busynothing is 54003 nothing: 54003 and the next busynothing is 23501 nothing: 23501 and the next busynothing is 21110 nothing: 21110 and the next busynothing is 88399 nothing: 88399 and the next busynothing is 49740 nothing: 49740 and the next busynothing is 31552 nothing: 31552 and the next busynothing is 39998 nothing: 39998 and the next busynothing is 19755 nothing: 19755 and the next busynothing is 64624 nothing: 64624 and the next busynothing is 37817 nothing: 37817 and the next busynothing is 43427 nothing: 43427 and the next busynothing is 15115 nothing: 15115 and the next busynothing is 44327 nothing: 44327 and the next busynothing is 7715 nothing: 7715 and the next busynothing is 15248 nothing: 15248 and the next busynothing is 61895 nothing: 61895 and the next busynothing is 54759 nothing: 54759 and the next busynothing is 54270 nothing: 54270 and the next busynothing is 51332 nothing: 51332 and the next busynothing is 63481 nothing: 63481 and the next busynothing is 12362 nothing: 12362 and the next busynothing is 94476 nothing: 94476 and the next busynothing is 87810 nothing: 87810 and the next busynothing is 6027 nothing: 6027 and the next busynothing is 47551 nothing: 47551 and the next busynothing is 79498 nothing: 79498 and the next busynothing is 81226 nothing: 81226 and the next busynothing is 4256 nothing: 4256 and the next busynothing is 62734 nothing: 62734 and the next busynothing is 25666 nothing: 25666 and the next busynothing is 14781 nothing: 14781 and the next busynothing is 21412 nothing: 21412 and the next busynothing is 55205 nothing: 55205 and the next busynothing is 65516 nothing: 65516 and the next busynothing is 53535 nothing: 53535 and the next busynothing is 4437 nothing: 4437 and the next busynothing is 43442 nothing: 43442 and the next busynothing is 91308 nothing: 91308 and the next busynothing is 1312 nothing: 1312 and the next busynothing is 36268 nothing: 36268 and the next busynothing is 34289 nothing: 34289 and the next busynothing is 46384 nothing: 46384 and the next busynothing is 18097 nothing: 18097 and the next busynothing is 9401 nothing: 9401 and the next busynothing is 54249 nothing: 54249 and the next busynothing is 29247 nothing: 29247 and the next busynothing is 13115 nothing: 13115 and the next busynothing is 23053 nothing: 23053 and the next busynothing is 3875 nothing: 3875 and the next busynothing is 16044 nothing: 16044 and the next busynothing is 8022 nothing: 8022 and the next busynothing is 25357 nothing: 25357 and the next busynothing is 89879 nothing: 89879 and the next busynothing is 80119 nothing: 80119 and the next busynothing is 50290 nothing: 50290 and the next busynothing is 9297 nothing: 9297 and the next busynothing is 30571 nothing: 30571 and the next busynothing is 7414 nothing: 7414 and the next busynothing is 30978 nothing: 30978 and the next busynothing is 16408 nothing: 16408 and the next busynothing is 80109 nothing: 80109 and the next busynothing is 55736 nothing: 55736 and the next busynothing is 15357 nothing: 15357 and the next busynothing is 80887 nothing: 80887 and the next busynothing is 35014 nothing: 35014 and the next busynothing is 16523 nothing: 16523 and the next busynothing is 50286 nothing: 50286 and the next busynothing is 34813 nothing: 34813 and the next busynothing is 77562 nothing: 77562 and the next busynothing is 54746 nothing: 54746 and the next busynothing is 22680 nothing: 22680 and the next busynothing is 19705 nothing: 19705 and the next busynothing is 77000 nothing: 77000 and the next busynothing is 27634 nothing: 27634 and the next busynothing is 21008 nothing: 21008 and the next busynothing is 64994 nothing: 64994 and the next busynothing is 66109 nothing: 66109 and the next busynothing is 37855 nothing: 37855 and the next busynothing is 36383 nothing: 36383 and the next busynothing is 68548 nothing: 68548 and the next busynothing is 96070 nothing: 96070 and the next busynothing is 83051 nothing: 83051 that's it. nothing: it. nothing is not digit! it.
O..K? That’s it?
This is confusing.
Something just came to my mind: is there any data hidden in the cookies? Since the level is quite about cookies…
r.cookies
<RequestsCookieJar[Cookie(version=0, name='info', value='%90', port=None, port_specified=False, domain='.pythonchallenge.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=1658024543, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False)]>
Wow! Sure there is! The cookie has a value of “%90” which is the URL-encoded value of something.
So we need to concatenate the values of the “info” cookie?
n = '12345'
info = ''
while True:
try:
r = requests.get('http://www.pythonchallenge.com/pc/def/linkedlist.php?busynothing=%s' % n)
except Exception as e:
print('ERROR:', repr(e))
break
if 'info' in r.cookies:
info += r.cookies['info']
n = r.text.rpartition(' ')[-1]
if not n.isdigit():
print('nothing is not digit!')
print(n)
break
info
nothing is not digit! it.
'BZh91AY%26SY%94%3A%E2I%00%00%21%19%80P%81%11%00%AFg%9E%A0%20%00hE%3DM%B5%23%D0%D4%D1%E2%8D%06%A9%FA%26S%D4%D3%21%A1%EAi7h%9B%9A%2B%BF%60%22%C5WX%E1%ADL%80%E8V%3C%C6%A8%DBH%2632%18%A8x%01%08%21%8DS%0B%C8%AF%96KO%CA2%B0%F1%BD%1Du%A0%86%05%92s%B0%92%C4Bc%F1w%24S%85%09%09C%AE%24%90'
Two things I noticed:
- This is a bz2 data string, and
- This needs to be URL-decoded first.
import bz2
import urllib.parse
bz2.decompress(urllib.parse.unquote_to_bytes(info))
b'is it the 26th already? call his father and inform him that "the flowers are on their way". he\'ll understand.'
OK! So I should call Mozart’s father… (he will NOT understand…) Reminds me of that phonebook level?
Googling yields that Mozart’s father is called Leopold…
import xmlrpc.client
xr = xmlrpc.client.ServerProxy('http://www.pythonchallenge.com/pc/phonebook.php')
xr.phone('Leopold')
'555-VIOLIN'
http://www.pythonchallenge.com/pc/return/violin.html:
no! i mean yes! but ../stuff/violin.php.
http://www.pythonchallenge.com/pc/stuff/violin.php:
Title: it’s me. what do you want?
and an image of Leopold.
The theme of this level: COOKIES! Let’s feed him a cookie and see how he’ll do!
r = requests.get(
'http://www.pythonchallenge.com/pc/stuff/violin.php',
auth=auth,
cookies={'info': 'the flowers are on their way'}
)
r.text
'<html>\n<head>\n <title>it\'s me. what do you want?</title>\n <link rel="stylesheet" type="text/css" href="../style.css">\n</head>\n<body>\n\t<br><br>\n\t<center><font color="gold">\n\t<img src="leopold.jpg" border="0"/>\n<br><br>\noh well, don\'t you dare to forget the balloons.</font>\n</body>\n</html>\n'
“don’t you dare to forget the balloons.” No, I won’t 😉
Level 18¶
http://huge:file@www.pythonchallenge.com/pc/stuff/balloons.html
Redirect to: http://www.pythonchallenge.com/pc/return/balloons.html
Title: can you tell the difference?
No, I can’t. But Python can!
My first thought is, because the image to the right has a darker color, it must have lower RGB values. So what if we subtract the values?
from PIL import Image
import io
r = requests.get('http://www.pythonchallenge.com/pc/return/balloons.jpg', auth=auth)
im = Image.open(io.BytesIO(r.content))
w = im.width//2
im2 = Image.new('RGB', (w,im.height))
def ensure(r,g,b,R,G,B):
return max(min(r-R,255),0), max(min(g-G,255),0), max(min(b-B,255),0)
for y in range(im.height):
for x in range(w):
im2.putpixel((x,y), ensure(*im.getpixel((x,y)), *im.getpixel((x+w,y))))
im2
No dice… Looking at the page source:
it is more obvious that what you might think
OK. What is the difference between the two images? … Brightness?
http://www.pythonchallenge.com/pc/return/brightness.html gives the same page, but in the source:
maybe consider deltas.gz
Alright. Downloading the file, I realize it’s a text file with two columns of similar data.
Let’s tell the difference!
import gzip
r = requests.get('http://www.pythonchallenge.com/pc/return/deltas.gz', auth=auth)
z = gzip.open(io.BytesIO(r.content))
c1 = []
c2 = []
for line in z.readlines():
c1.append(line[:53].decode()+'\n')
c2.append(line[56:].decode())
import difflib
data = {' ': [], '+': [], '-': []}
for l in difflib.Differ().compare(c1, c2):
data[l[0]].append(l[2:])
dat = {' ': b'', '+': b'', '-': b''}
for d, l in data.items():
for x in l:
dat[d] += bytes(map(lambda x: int(x, 16), x.split()))
dat[' '][:64]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02\x8a\x00\x00\x00\xc8\x08\x02\x00\x00\x00\xe0\x19W\x95\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xd5'
Alright! That’s a PNG file! So (I hope) are all of them!
for d, r in dat.items():
im = Image.open(io.BytesIO(r))
display(d)
display(im)
' '
'+'
'-'
So it seems that we have now a new set of username/password!
auth = ('butter', 'fly')
Level 19¶
http://butter:fly@www.pythonchallenge.com/pc/hex/bin.html
Title: please!
The map looks like a map of India. Not extremely helpful though.
In the page source, there is a long comment that starts with:
From: leopold.moz@pythonchallenge.com
Subject: what do you mean by "open the attachment?"
Mime-version: 1.0
Content-type: Multipart/mixed; boundary="===============1295515792=="
It is so much easier for you, youngsters.
Maybe my computer is out of order.
I have a real work to do and I must know what's inside!
--===============1295515792==
Content-type: audio/x-wav; name="indian.wav"
Content-transfer-encoding: base64
UklGRvyzAQBXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YdizAQBABkAMQAtAAEADQAJA
BEAEQAJAAkAGQAVABUAEQApAC0AJQAhAD0APQANADUAFQAVAD0AEQA5ADUAGQAlAAj8PQAVABkAE
QAJACUAFQAQ/CkAKQAg/BEAMQAo/AEABQANABEAAPw1ADEAOPwZADkAHPwBADj8OQAhABT8IQARA
AD8FQAQ/Dz8AQA8/BEAGQAQ/DkAIQBA/B0AMQAU/BEAOPwo/DkAMPw1AC0AFPwhAC0AIQA0/AD8J
Pwo/Cz8IPwc/AT8HPwE/DkACPwdAD0AOPwNAB0APPw8/B0AEQAk/CD8FPwo/DUADPww/DUALPwZA
DkAMPwI/DkAGQA8/DT8IPwFACEAEPwo/Cj8OPwRACD8BPwI/BT8CQAg/BT4LPwNAAEADQAdACj8L
QBBADj8FPw9AB0AIPwA/Cj8OPwc/BT8DPw0/AD8CPwM/BT8IPxA/Az8LPww/DD8NPw5ADD8QPgg+
BEALQAI+Aj4AQAZABj8KPgU/Aj8HPwc+Cj4EPw4/AD8MPwY/AT4JPwQ/BT8LPwNACD8LPwY/Cz8K
PwQ/DD4OPw1ACT8IPw0/Aj4FPw0/CD8FPgg+Az8QPww+Cj4APwM/Az8FPwY/CUADPw0+Bz8APwhA
AUAMPwc+Cj8LPwg/CT8KPgE9AT4IPw4+Dj0KPgo/ED8MPg0+Bz8OPwI/BD8LPwI/Bj8GPw5ABD8C
Pw4/Dz8HPw4+Dz8FPww/BD4KPQo/Cz8HPQE9Cj8HPwM/AD8JPgw9DT4OQA0/Aj0DPQFAD0ALPgY+
DkAAQAI/CD8FQApAAD8HPg49Bj4JQAw+ATwAPgs+Bj8JPgI9Bz4IQAM/DT8BQAlACUAJPwRAAEAJ
QAQ/BD8BQAZBAj8MPg0+AD0KPw9ABz0POwg8Az4FPxA+BT0LPhBACT8APgw/Cj8KPwNABD8APww/
AkAGPwE/Cz8KPgI/C0ADPg49DD4MPgU/DT4OPQ8+Dz8GPgo9Bz0NPg8+Bz4PPQU+DD8JPwM/DT4P
Pgs/A0AMPwo/Dz8OQAhAAD8QPhA+AT4DPgw9ADwDPQw+Bz0BPQM9AD4NPwE/Cz4MPQc/DUAOPwlA
DUAEPw9ADUAJPw4+Bz8JQAJACD8JPQk8Cj0BPg8+BD0LPAM9CUABPw89DD8MQAQ+Bz4FPww/AEAB
PgA8Dj0HQAA/BzwKPAA/CD8AOxA8DT0OPAU8Cz0OPgA+BD0CPQI+BD8NQA0/BT0APQBBD0IPPQc7
AT4OQQ1ACz4GPQA/DEILQgQ/Cj8IQAdBDUIBPwI9DT4BQQZACz4IPQ0/AEEBPwE8CTwEPwY/Bj0D
[...]
On first sight, it looks very much like an e-mail file. Starts with the heading (from, subject, mime-version, etc.) and the text, followed by an attachment named “indian.wav” with a content-type of “audio/x-wav” (a .WAV file) and base64-encoded. So let’s decode it out!
import base64
r = requests.get('http://www.pythonchallenge.com/pc/hex/bin.html', auth=auth)
t = ''.join(r.text.splitlines()[27:1986])
dat = base64.b64decode(t)
len(dat)
111620
import struct
import wave
import IPython.display
with wave.open(io.BytesIO(dat)) as fin:
n = fin.getnframes()
f = fin.readframes(n)
r = fin.getframerate()
IPython.display.Audio(struct.unpack('<%dH'%n, f), rate=r)
It says: “sorry!”
http://www.pythonchallenge.com/pc/hex/sorry.html:
- “what are you apologizing for?”
Uhhh… I don’t know? I’m sorry for… India?
http://www.pythonchallenge.com/pc/hex/india.html:
nnn. what could this mean?
OK, no idea.
The audio file doesn’t seem to have too much information:
$ ffprobe indian.wav
ffprobe version 4.2.7-0ubuntu0.1 Copyright (c) 2007-2022 the FFmpeg developers
[...]
Input #0, wav, from 'indian.wav':
Duration: 00:00:05.06, bitrate: 176 kb/s
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 11025 Hz, 1 channels, s16, 176 kb/s
$ file indian.wav
indian.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 11025 Hz
Then I went back to look at the problem page again. There is a map. Something seems off about it… Why is the sea brown? And the land blue? That’s… REVERSED! Looking back on the file
output… Indian… Little-endian… The dots connected in my head.
Let’s convert the file into big-endian!
out = io.BytesIO()
with wave.open(io.BytesIO(dat)) as fin:
with wave.open(out, 'w') as fout:
fout.setparams(fin.getparams())
n = fin.getnframes()
for i in range(n):
fout.writeframes(fin.readframes(1)[::-1])
out.seek(0)
with wave.open(out) as fin:
n = fin.getnframes()
f = fin.readframes(n)
r = fin.getframerate()
IPython.display.Audio(struct.unpack('<%dH'%n, f), rate=r)
How… DARE you call me an idiot! 😆
http://www.pythonchallenge.com/pc/hex/idiot.html:
Title: -idiot ?
Leopold’s image, “Now you should apologize…”, and link to next level!
Alright, that’s enough challenging for one day!
We’ll do the rest in a separate post!
Made with Jupyter Notebook!