13 minute read

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:

Input:

auth = ('huge', 'file')

Level 14

https://huge:file@www.pythonchallenge.com/pc/return/italy.html

Title: walk around

italy.jpg

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:

pythonchallenge-l14-wire.png

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!

Input:

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.

uzi.jpg

Level 15

http://huge:file@www.pythonchallenge.com/pc/return/uzi.html

Title: whom?

whom.jpg

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

mozart.gif

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?

Input:

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?

cookies.jpg

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:

pythonchallenge-l17-cookie.png

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!

Input:

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

Output:

If you came here from level 4 - go back!
You should follow the obvious chain...
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…

Input:

r.cookies

Output:

<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?

Input:

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

Output:

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:

  1. This is a bz2 data string, and
  2. This needs to be URL-decoded first.

Input:

import bz2
import urllib.parse

bz2.decompress(urllib.parse.unquote_to_bytes(info))

Output:

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…

Input:

import xmlrpc.client

xr = xmlrpc.client.ServerProxy('http://www.pythonchallenge.com/pc/phonebook.php')
xr.phone('Leopold')

Output:

'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!

Input:

r = requests.get(
    'http://www.pythonchallenge.com/pc/stuff/violin.php',
    auth=auth,
    cookies={'info': 'the flowers are on their way'}
)
r.text

Output:

'<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?

balloons.jpg

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?

Input:

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!

Input:

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]

Output:

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!

Input:

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!

map-1

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!

Input:

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)

Output:

111620

Input:

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!

Input:

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!