8 minute read

Continuing from this post: The Python Challenge (Levels 0-5)!

Previously on the Python Challenge, we passed the first 6 levels, 0-5. Now, let’s continue exploring the levels!

Level #6

http://www.pythonchallenge.com/pc/def/channel.html

Title: now there are pairs

On the page itself, there is only an image:

channel.jpg

And in the page source, there is one seeming hint (and some information related to the author’s Paypal):

<!– <– zip –>

I have no idea how to continue from here, so I decided to change the html into zip and see what happens: http://www.pythonchallenge.com/pc/def/channel.zip

And surely, it’s a ZIP file!

When I open the ZIP, there seems to be 909 files named <number>.txt and one readme.txt that says,

welcome to my zipped list.

hint1: start from 90052

hint2: answer is inside the zip

90052.txt:

Next nothing is 94191

94191.txt:

Next nothing is 85503

OK, so another linked list, huh?

Input:

import io
import requests
import zipfile

r = requests.get('http://www.pythonchallenge.com/pc/def/channel.zip')
zf = zipfile.ZipFile(io.BytesIO(r.content))

n = b'90052'
while True:
    with zf.open('%d.txt' % int(n)) as f:
        data = f.read()
        n = data.rpartition(b' ')[2]
    if not n.isdigit():
        print('nothing is not digit!', data, n)
        break

Output:

nothing is not digit! b'Collect the comments.' b'comments.'

“Collect the comments.” Comments?

Google tells me that in a ZIP file, each file can have a “comment”. OK. Fine.

Input:

n = b'90052'
c = b''
while True:
    c += zf.NameToInfo['%d.txt' % int(n)].comment
    with zf.open('%d.txt' % int(n)) as f:
        data = f.read()
        n = data.rpartition(b' ')[2]
    if not n.isdigit():
        print('nothing is not digit!', data, n)
        break
print(c.decode())

Output:

nothing is not digit! b'Collect the comments.' b'comments.'
****************************************************************
****************************************************************
**                                                            **
**   OO    OO    XX      YYYY    GG    GG  EEEEEE NN      NN  **
**   OO    OO  XXXXXX   YYYYYY   GG   GG   EEEEEE  NN    NN   **
**   OO    OO XXX  XXX YYY   YY  GG GG     EE       NN  NN    **
**   OOOOOOOO XX    XX YY        GGG       EEEEE     NNNN     **
**   OOOOOOOO XX    XX YY        GGG       EEEEE      NN      **
**   OO    OO XXX  XXX YYY   YY  GG GG     EE         NN      **
**   OO    OO  XXXXXX   YYYYYY   GG   GG   EEEEEE     NN      **
**   OO    OO    XX      YYYY    GG    GG  EEEEEE     NN      **
**                                                            **
****************************************************************
 **************************************************************

“HOCKEY”.

http://www.pythonchallenge.com/pc/def/hockey.html:

it’s in the air. look at the letters.

Looking at the banner, I see that the letters “HOCKEY” are made of, respectively, “OXYGEN”. So that’s our new URL.

Level #7

https://www.pythonchallenge.com/pc/def/oxygen.html

Title: smarty

There is only an image shown on the page:

oxygen.png

And nothing at all in the page source.

Looking closer at the image, there is a strip of greyscaled blocks in the middle. Let’s see… Maybe that contains some data?

Inspecting the image with GIMP, I see the blocks are all 7×9 pixels, except for the first one which is 5×9 pixels. There are 87 of those blocks. Now we can use Python to find out the data!

Input:

from PIL import Image  # pip install pillow

r = requests.get('http://www.pythonchallenge.com/pc/def/oxygen.png')
im = Image.open(io.BytesIO(r.content))

data = []
for i in range(87):
    r,g,b,a = im.getpixel((i*7, im.height//2))
    assert r == g == b, 'Not greyscale pixel'
    data.append(r)
data = bytes(data)
data

Output:

b'smart guy, you made it. the next level is [105, 110, 116, 101, 103, 114, 105, 116, 121]'

🙂

Input:

bytes([105, 110, 116, 101, 103, 114, 105, 116, 121])

Output:

b'integrity'

Level 8

https://www.pythonchallenge.com/pc/def/integrity.html

Title: working hard?

There is an image and caption:

integrity.jpg

Where is the missing link?

I realize that when I click the bee, I will be redirected to http://www.pythonchallenge.com/pc/return/good.html where I am prompted for a username and password.

In the source, there is this little blob at the end:

<!--
un: 'BZh91AY&SYA\xaf\x82\r\x00\x00\x01\x01\x80\x02\xc0\x02\x00 \x00!\x9ah3M\x07<]\xc9\x14\xe1BA\x06\xbe\x084'
pw: 'BZh91AY&SY\x94$|\x0e\x00\x00\x00\x81\x00\x03$ \x00!\x9ah3M\x13<]\xc9\x14\xe1BBP\x91\xf08'
-->

I remember from somewhere that “BZhX” is the header of a bz2 compressed blob. Is this one of them?

Input:

import bz2
un = b'BZh91AY&SYA\xaf\x82\r\x00\x00\x01\x01\x80\x02\xc0\x02\x00 \x00!\x9ah3M\x07<]\xc9\x14\xe1BA\x06\xbe\x084'
pw = b'BZh91AY&SY\x94$|\x0e\x00\x00\x00\x81\x00\x03$ \x00!\x9ah3M\x13<]\xc9\x14\xe1BBP\x91\xf08'
bz2.decompress(un), bz2.decompress(pw)

Output:

(b'huge', b'file')

OK… That was easier than I thought.

Setting the auth variable here for future use:

Input:

auth = 'huge', 'file'

Level 9

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

Title: connect the dots

(I am going to skip the description of the level page from now on. Usually it’s an image, sometimes with a caption.)

good.jpg

Page source:

<!--
first+second=?

first:
146,399,163,403,170,393,169,391,166,386,170,381,170,371,170,355,169,346,167,335,170,329,170,320,170,
310,171,301,173,290,178,289,182,287,188,286,190,286,192,291,194,296,195,305,194,307,191,312,190,316,
190,321,192,331,193,338,196,341,197,346,199,352,198,360,197,366,197,373,196,380,197,383,196,387,192,
389,191,392,190,396,189,400,194,401,201,402,208,403,213,402,216,401,219,397,219,393,216,390,215,385,
215,379,213,373,213,365,212,360,210,353,210,347,212,338,213,329,214,319,215,311,215,306,216,296,218,
290,221,283,225,282,233,284,238,287,243,290,250,291,255,294,261,293,265,291,271,291,273,289,278,287,
279,285,281,280,284,278,284,276,287,277,289,283,291,286,294,291,296,295,299,300,301,304,304,320,305,
327,306,332,307,341,306,349,303,354,301,364,301,371,297,375,292,384,291,386,302,393,324,391,333,387,
328,375,329,367,329,353,330,341,331,328,336,319,338,310,341,304,341,285,341,278,343,269,344,262,346,
259,346,251,349,259,349,264,349,273,349,280,349,288,349,295,349,298,354,293,356,286,354,279,352,268,
352,257,351,249,350,234,351,211,352,197,354,185,353,171,351,154,348,147,342,137,339,132,330,122,327,
120,314,116,304,117,293,118,284,118,281,122,275,128,265,129,257,131,244,133,239,134,228,136,221,137,
214,138,209,135,201,132,192,130,184,131,175,129,170,131,159,134,157,134,160,130,170,125,176,114,176,
102,173,103,172,108,171,111,163,115,156,116,149,117,142,116,136,115,129,115,124,115,120,115,115,117,
113,120,109,122,102,122,100,121,95,121,89,115,87,110,82,109,84,118,89,123,93,129,100,130,108,132,110,
133,110,136,107,138,105,140,95,138,86,141,79,149,77,155,81,162,90,165,97,167,99,171,109,171,107,161,
111,156,113,170,115,185,118,208,117,223,121,239,128,251,133,259,136,266,139,276,143,290,148,310,151,
332,155,348,156,353,153,366,149,379,147,394,146,399

second:
156,141,165,135,169,131,176,130,187,134,191,140,191,146,186,150,179,155,175,157,168,157,163,157,159,
157,158,164,159,175,159,181,157,191,154,197,153,205,153,210,152,212,147,215,146,218,143,220,132,220,
125,217,119,209,116,196,115,185,114,172,114,167,112,161,109,165,107,170,99,171,97,167,89,164,81,162,
77,155,81,148,87,140,96,138,105,141,110,136,111,126,113,129,118,117,128,114,137,115,146,114,155,115,
158,121,157,128,156,134,157,136,156,136

-->

To proceed, first I decided to put this data into Python and see what I can get.

Input:

first = [
146,399,163,403,170,393,169,391,166,386,170,381,170,371,170,355,169,346,167,335,170,329,170,320,170,
310,171,301,173,290,178,289,182,287,188,286,190,286,192,291,194,296,195,305,194,307,191,312,190,316,
190,321,192,331,193,338,196,341,197,346,199,352,198,360,197,366,197,373,196,380,197,383,196,387,192,
389,191,392,190,396,189,400,194,401,201,402,208,403,213,402,216,401,219,397,219,393,216,390,215,385,
215,379,213,373,213,365,212,360,210,353,210,347,212,338,213,329,214,319,215,311,215,306,216,296,218,
290,221,283,225,282,233,284,238,287,243,290,250,291,255,294,261,293,265,291,271,291,273,289,278,287,
279,285,281,280,284,278,284,276,287,277,289,283,291,286,294,291,296,295,299,300,301,304,304,320,305,
327,306,332,307,341,306,349,303,354,301,364,301,371,297,375,292,384,291,386,302,393,324,391,333,387,
328,375,329,367,329,353,330,341,331,328,336,319,338,310,341,304,341,285,341,278,343,269,344,262,346,
259,346,251,349,259,349,264,349,273,349,280,349,288,349,295,349,298,354,293,356,286,354,279,352,268,
352,257,351,249,350,234,351,211,352,197,354,185,353,171,351,154,348,147,342,137,339,132,330,122,327,
120,314,116,304,117,293,118,284,118,281,122,275,128,265,129,257,131,244,133,239,134,228,136,221,137,
214,138,209,135,201,132,192,130,184,131,175,129,170,131,159,134,157,134,160,130,170,125,176,114,176,
102,173,103,172,108,171,111,163,115,156,116,149,117,142,116,136,115,129,115,124,115,120,115,115,117,
113,120,109,122,102,122,100,121,95,121,89,115,87,110,82,109,84,118,89,123,93,129,100,130,108,132,110,
133,110,136,107,138,105,140,95,138,86,141,79,149,77,155,81,162,90,165,97,167,99,171,109,171,107,161,
111,156,113,170,115,185,118,208,117,223,121,239,128,251,133,259,136,266,139,276,143,290,148,310,151,
332,155,348,156,353,153,366,149,379,147,394,146,399
]
second = [
156,141,165,135,169,131,176,130,187,134,191,140,191,146,186,150,179,155,175,157,168,157,163,157,159,
157,158,164,159,175,159,181,157,191,154,197,153,205,153,210,152,212,147,215,146,218,143,220,132,220,
125,217,119,209,116,196,115,185,114,172,114,167,112,161,109,165,107,170,99,171,97,167,89,164,81,162,
77,155,81,148,87,140,96,138,105,141,110,136,111,126,113,129,118,117,128,114,137,115,146,114,155,115,
158,121,157,128,156,134,157,136,156,136
]

len(first), len(second), (min(first), max(first)), (min(second), max(second))

Output:

(442, 112, (77, 403), (77, 220))

I’m somewhat stuck here for a while, not knowing what to do. Then I went back to the problem page: the title is “connect the dots”. What if… The first and second arrays are coordinates on the image? And I should connect them?

Input:

from PIL import ImageDraw
r = requests.get('http://www.pythonchallenge.com/pc/return/good.jpg', auth=auth)
im = Image.open(io.BytesIO(r.content))
d = ImageDraw.Draw(im)
for i in range(0, len(first)-2, 2):
    d.line(((first[i], first[i+1]), (first[i+2], first[i+3])), fill=(255,0,0))
im

That’s a.. bull?

Input:

for i in range(0, len(second)-2, 2):
    d.line(((second[i], second[i+1]), (second[i+2], second[i+3])), fill=(255,0,0))
im

Yes, I am sure that is a bull.

Level 10

http://www.pythonchallenge.com/pc/return/bull.html

Title: what are you looking at?

bull.jpg

len(a[30]) = ?

Clicking on the bull leads to this text page, http://www.pythonchallenge.com/pc/return/sequence.txt:

a = [1, 11, 21, 1211, 111221,

Looking at the array, I notice that each element has a length of a multiple of 2 (except for the first element). How does the next element come from the previous one?

……

OK, I think I get it. After many times of trial and error, I found out that 11 means the previous term has “one 1”, 21 means there are “two 1s”, 1211 means there are “one 2, one 1” and so on.

Googling this sequence, it is actually called the “Look-and-say sequence”. (Wikipedia, OEIS) So, OK. This is a math problem. Let’s do it with Python.

Input:

a = '1'
for i in range(1, 31):
    b = ''
    c = 1
    for j in range(1, len(a)):
        if a[j] != a[j-1]:
            b += '%d%s' % (c, a[j-1])
            c = 1
        else:
            c += 1
    b += '%d%s' % (c, a[-1])
    a = b
    # print('a[%d]: %s' % (i, a))
    print('len(a[%d]): %d' % (i, len(a)))

Output:

len(a[1]): 2
len(a[2]): 2
len(a[3]): 4
len(a[4]): 6
len(a[5]): 6
len(a[6]): 8
len(a[7]): 10
len(a[8]): 14
len(a[9]): 20
len(a[10]): 26
len(a[11]): 34
len(a[12]): 46
len(a[13]): 62
len(a[14]): 78
len(a[15]): 102
len(a[16]): 134
len(a[17]): 176
len(a[18]): 226
len(a[19]): 302
len(a[20]): 408
len(a[21]): 528
len(a[22]): 678
len(a[23]): 904
len(a[24]): 1182
len(a[25]): 1540
len(a[26]): 2012
len(a[27]): 2606
len(a[28]): 3410
len(a[29]): 4462
len(a[30]): 5808

Great! The answer is 5808.

Level 11

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

Title: odd even

cave.jpg

There is nothing in the source worth noting.

Oh geez… This level is hard! It looks hard at least.

“odd even”… What does that mean?

Looking at the image closer, I realize that… how can I explain this… there are blanks between two pixels in the image. What if we remove the blanks?

Input:

r = requests.get('http://www.pythonchallenge.com/pc/return/cave.jpg', auth=auth)
im = Image.open(io.BytesIO(r.content))
im2 = Image.new('RGB', (im.width//2, im.height))
for y in range(im.height):
    for x in range(im.width//2):
        im2.putpixel((x,y), im.getpixel((x*2+y%2,y)))
im2

Alright! There is a faint text in the top right corner that says “evil”.

Level 12

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

Title: dealing evil

evil1.jpg

In the source, interestingly, the image’s filename is evil1.jpg:

<img src="evil1.jpg">

So… What is evil2.jpg?

evil2.jpg

Sure! I changed the .jpg suffix to .gfx, but my browser did not recognize that format and downloaded the file for me.

Running file didn’t help too much:

$ file evil2.gfx
evil2.gfx: data

Fine. What can I do then?

When in doubt, Google it!

I Googled gfx file, and this is what Google tells me:

A GFX file is an animation file used by video games, such as Batman: Arkham Asylum and Mass Effect. It contains vector and raster graphics in a format similar to the . SWF file format. GFX files also may include ActionScript interactive actions.

Source: fileinfo.com

… Not useful at all.

Are there any more evils?

I tried http://www.pythonchallenge.com/pc/return/evil3.jpg and got:

evil3.jpg

Sure… But it seems weird that another image is included but tells me there are no more… This seems off.

http://www.pythonchallenge.com/pc/return/evil4.jpg gave me an empty image… It’s weird that it didn’t just throw a 404 NOT FOUND but instead gave me an OK response. Maybe it’s NOT actually an image?

Input:

r = requests.get('http://www.pythonchallenge.com/pc/return/evil4.jpg', auth=auth)
r.content

Output:

b'Bert is evil! go back!\n'

Bert is evil…? Who’s Bert?

http://www.pythonchallenge.com/pc/return/bert.html:

Yes! Bert is evil!

bert.gif

That’s… not helpful?

I remembered that I still have a evil2.gfx untouched. I feel like there’s something there.

Let’s take a closer look.

Input:

r = requests.get('http://www.pythonchallenge.com/pc/return/evil2.gfx', auth=auth)
r.content[:10], len(r.content)

Output:

(b'\xff\x89G\x89\xff\xd8PIP\xd8', 67575)

Fine, still not helping me.

Going back to the problem page… A person dealing cards into 5 piles? The length of the GFX file is also 5? Something clicked in my brain: what if we deal the file?

Input:

data = [b'' for i in range(5)]
for i in range(5):
    data[i%5] = r.content[i::5]
data[0][:10]

Output:

b'\xff\xd8\xff\xe0\x00\x10JFIF'

Great! It’s an image file (I saw the JFIF header)!

Let’s open these images and take a look…

Input:

from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True  # To prevent OSError: image file is truncated
for d in data:
    display(Image.open(io.BytesIO(d)))

Disproportionality! That’s our next level!

Level 13

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

Title: call him

disprop.jpg

phone that evil

Clicking on the “5” in the image leads us to http://www.pythonchallenge.com/pc/phonebook.php, where there is apparently an XML file…

<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct><member><name>faultCode</name>
<value><int>105</int></value>
</member>
<member>
<name>faultString</name>
<value><string>XML error 5: empty document</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>

Googling up “xml faultcode 105” yields this StackOverflow question https://stackoverflow.com/q/7950297 where the answer states:

Are you just ‘browsing’ to the address of the xmlrpc folder? It looks from the response like you didn’t submit an xml ‘request’ document to the url. XMLRPC requires that you post a request in the form of commands via an xml file.

Oh. So this is XMLRPC… which is? Googling (again) “xmlrpc python”:

XML-RPC is a Remote Procedure Call method that uses XML passed via HTTP as a transport.

Source: https://docs.python.org/3/library/xmlrpc.html

Great! So I just need to use that Python library!

Input:

import xmlrpc.client
xr = xmlrpc.client.ServerProxy('http://www.pythonchallenge.com/pc/phonebook.php')
xr.system.listMethods()

Output:

['phone',
 'system.listMethods',
 'system.methodHelp',
 'system.methodSignature',
 'system.multicall',
 'system.getCapabilities']

Input:

xr.system.methodHelp('phone')

Output:

'Returns the phone of a person'

So I want to “phone” Bert, the evil we found out in the previous level?

Input:

xr.phone('Bert')

Output:

'555-ITALY'

“555-ITALY”? So the next level is “italy”!

That’s it for today, more coming soon!