Python Challenge Notes (Part 1)

Just some notes,http://www.pythonchallenge.com/
Complete source code here

#Contents

Level 1 Level 2 Level 3 Level 4 Level 5
Level 6 Level 7 Level 8 Level 9 Level 10
Level 11 Level 12 Level 13 Level 14 Level 15
Level 16 Level 17 Level 18 Level 19 Level 20

#Level 0

238=274877906944

Change the url to http://www.pythonchallenge.com/pc/def/274877906944.html,and there comes Level 1.

#Level 1

Obviously, by adding an offset to the ascii of each letter in the sentence you'll get the instruction.

1
2
3
4
5
6
7
8
9
10
line = "g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj."
list1 = []
for i in list(line):
if i.isalpha():
j = ord(i)+2
s = chr(j) if j<123 else chr(j-26)
else:
s = i
list1.append(s)
print("".join(list1))

The output

i hope you didnt translate it by hand. thats what computers are for. doing it in by hand is inefficient and that's why this text is so long. using string.maketrans() is recommended. now apply on the url.

OK, apply to the url map, and the url for level 2 is here.

Noted that it mentioned str.maketrans() function, which is not how I solved the problem. So what does maketrans() do? Here is the official documentation. In short, it generates a translating method. str.translate() function then uses this method to translate a giving string. Now let's try this out.

1
2
3
4
5
line = "g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj."
label1 = "abcdefghijklmnopqrstuvwxyz"
label2 = "cdefghijklmnopqrstuvwxyzab"
trans = str.maketrans(label1, label2)
print(line.translate(trans))

i hope you didnt translate it by hand. thats what computers are for. doing it in by hand is inefficient and that's why this text is so long. using string.maketrans() is recommended. now apply on the url.

OK, exactly what we expected.

#Level 2

It's evident that we should take a look at the source code of the page. There's a comment: find rare characters below.

For convenience, save all those charactors to assets/txt_for_2.txt, and find out the occurrence of each charactor.

1
2
3
4
5
6
from collections import Counter

with open("assets/txt_for_2.txt", "r") as f:
string = f.read()
count = Counter(string)
print(count)

Counter({')': 6186, '@': 6157, '(': 6154, ']': 6152, '#': 6115, '_': 6112, '[': 6108, '}': 6105, '%': 6104, '!': 6079, '+': 6066, '$': 6046, '{': 6046, '&': 6043, '*': 6034, '^': 6030, '\n': 1219, 'e': 1, 'q': 1, 'u': 1, 'a': 1, 'l': 1, 'i': 1, 't': 1, 'y': 1})

OK, combine the charactors only show up once, and there you get the url for next level.

#Level 3

Again, obviously, this level demands regx. Taking a look at the source code is a must. Save those charactors to assets/txt_for_3.txt and let's go for it.

1
2
3
4
5
6
7
import re
with open("assets/txt_for_3.txt", "r") as f:
line = f.read()
reg=re.compile('[a-z][A-Z]{3}[a-z][A-Z]{3}[a-z]')
url = reg.findall(line)
for u in url:
print(u[4], end='')

And the output is linkedlist, now change the url. The url for the next level is a little different, as it says.

#Level 4

Nothing on the page or in the source code. Click on the image, it leads us to a new page and says the next nothing is 44827.

Change the url to ...?nothing=44827. Oh, again, the next nothing is 45439. Seems that some http library like requests is needed.

1
2
3
4
5
6
7
8
9
import requests
url='http://www.pythonchallenge.com/pc/def/linkedlist.php?nothing=12345'
r=requests.get(url)
txt=r.text
while True:
url='http://www.pythonchallenge.com/pc/def/linkedlist.php?nothing='+txt.split()[-1]
r=requests.get(url)
txt=r.text
print(txt)

Wait for a reasonable output.

#Level 5

pronounce it.

Go for source code. The image is called peakhell.jpg. And there's something didn't show up on the page.

1
2
3
<peakhell src="banner.p">
<!-- peak hell sounds familiar ? -->
</peakhell>

Changing url to banner.p, there's a load of charactors making no sense at all.

Back to where we started, "pronounce it". There's a module called pickle which is for serializing and de-serializing.

1
2
3
4
5
6
7
from urllib import request
import pickle

url = 'http://www.pythonchallenge.com/pc/def/banner.p'
f = request.urlopen(url)
result = pickle.load(f)
print(result)

The output turns out to be a two-dimensional list. Take a glance at it. The numbers in the first three lines add up to 95. There's a possibility that the number is the occurrence of the charactor in front.

1
2
3
4
for i in result:
for j in i:
print(j[0] * j[1], end = '')
print('\n')

The output is

channel by # charactor

#Level 6

zip

Change the url to *.zip and we get a file channel.zip. Unzip it, there's a readme.txt. Well, it's similar to level 4.

1
2
3
4
5
6
7
8
9
PATH = 'assets/channel/'
name = '90052'
while True:
filepath = PATH + name + ".txt"
with open(filepath, "r") as f:
text = f.readline()
print(text)
s = text.split(" ")
name = s[-1]

NO, the program runs into an error. In the last file

collect the comments.

Now, what is a zip comment? Google it:
A comment is optional text information that is embedded in a Zip file. ->source

With 7zip, we can see the comment of each file. The lengths of those comments are the same, only one charactor. Now, let's have fun with the zip file. zipfile module is needed, of course.

1
2
3
4
5
6
7
8
9
10
11
12
13
import zipfile
num = "90052"
comments = []
f = zipfile.ZipFile("assets/channel.zip") # open zip file
try:
while True:
filepath = num + ".txt"
line = f.read(filepath).decode("utf-8") # read txt file inside
comment = f.getinfo(filepath).comment.decode("utf-8") # comment of txt file
num = line.split(" ")[-1]
comments.append(comment)
except KeyError:
print("".join(comments))
level6

Replace the url with hockey. It says it's in the air. look at the letters. Well, ya know what to do.

#Level 7

Just a picture, nothing in the source code. The information has to be in the picture, then.

Noted there's a horizontal line which does not fit the picture at all. By observing the line pixel by pixel with Snipaste, we can find out that the color turns different every 7-9 pixels.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import cv2

img = cv2.imread("assets/oxygen.png")
offset1, offset2, offset3 = 7, 8, 9
x, y, z, m = 0, 0, 0, int(img.shape[0] / 2 + 1)
l1, l2, l3 = [], [], []
while x <= img.shape[1]:
px1 = img[m, x]
if px1[0] == px1[1] == px1[2]:
l1.append(px1[0])
x += offset1
while y <= img.shape[1]:
px2 = img[m, y]
if px2[0] == px2[1] == px2[2]:
l2.append(px2[0])
y += offset2
while z <= img.shape[1]:
px3 = img[m, z]
if px3[0] == px3[1] == px3[2]:
l3.append(px3[0])
z += offset3
print(f"offset = 7: {l1}")
print(f"offset = 8: {l2}")
print(f"offset = 9: {l3}")

offset = 7: [115, 109, 97, 114, 116, 32, 103, 117, 121, 44, 32, 121, 111, 117, 32, 109, 97, 100, 101, 32, 105, 116, 46, 32, 116, 104, 101, 32, 110, 101, 120, 116, 32, 108, 101, 118, 101, 108, 32, 105, 115, 32, 91, 49, 48, 53, 44, 32, 49, 49, 48, 44, 32, 49, 49, 54, 44, 32, 49, 48, 49, 44, 32, 49, 48, 51, 44, 32, 49, 49, 52, 44, 32, 49, 48, 53, 44, 32, 49, 49, 54, 44, 32, 49, 50, 49, 93]
offset = 8: [115, 109, 97, 114, 116, 103, 117, 121, 44, 32, 121, 111, 32, 109, 97, 100, 101, 32, 105, 46, 32, 116, 104, 101, 32, 110, 120, 116, 32, 108, 101, 118, 101, 32, 105, 115, 32, 91, 49, 48, 44, 32, 49, 49, 48, 44, 32, 49, 54, 44, 32, 49, 48, 49, 32, 49, 48, 51, 44, 32, 49, 52, 44, 32, 49, 48, 53, 44, 49, 49, 54, 44, 32, 49, 50, 93]
offset = 9: [115, 109, 97, 116, 32, 103, 121, 44, 32, 121, 117, 32, 109, 100, 101, 32, 105, 46, 32, 116, 101, 32, 110, 101, 116, 32, 108, 118, 101, 108, 32, 115, 32, 91, 48, 53, 44, 32, 49, 48, 44, 49, 49, 54, 44, 49, 48, 49, 32, 49, 48, 51, 32, 49, 49, 44, 32, 49, 48, 44, 32, 49, 54, 44, 32, 49, 49, 93]

Translate these numbers as ascii code

1
2
3
4
5
6
s1 = [chr(i) for i in l1]
s2 = [chr(i) for i in l2]
s3 = [chr(i) for i in l3]
print(f"offset = 7: {''.join(s1)}")
print(f"offset = 8: {''.join(s2)}")
print(f"offset = 9: {''.join(s3)}")

offset = 7: smart guy, you made it. the next level is [105, 110, 116, 101, 103, 114, 105, 116, 121]
offset = 8: smartguy, yo made i. the nxt leve is [10, 110, 16, 101 103, 14, 105,116, 12]
offset = 9: smat gy, yu mde i. te net lvel s [05, 10,116,101 103 11, 10, 16, 11]

It's obvious that result under offset=7 makes sense. Now do the same thing to the numbers in the result.

#level 8

There's a link on the body of the bee. And it leads us to a site which requires logging in. Source code! of course.

1
2
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'

un stands for user name, pw for password, of course. These charactors are compressed by bzip2. Let's de-compress it.

1
2
3
4
5
6
import bz2

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

b'huge'
b'file'

#Level 9

The page is called connect the dots, and there lies an interesting comment, first+second=? followed by a bunch of numbers. Maybe these numbers are coordinates?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import cv2
import numpy as np

img = cv2.imread("assets/good.jpg")
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]

pts1 = np.array(first, np.int32)
pts1 = pts1.reshape((-1,2))
pts2 = np.array(second, np.int32)
pts2 = pts2.reshape((-1,2))

cv2.polylines(img,[pts1, pts2],True,(0,255,255))
cv2.imshow("img", img)
cv2.waitKey(0)

Fact is, we do not need the image good.jpg. A blank picture works just fine.

1
img = np.zeros((512,512,3), np.uint8)

There it comes.

level9

The answer lies in cow/bull/ox/cattle. Take your shots.

#Level 10

There's a link on the body of the cow. It's a list which has no end, for now. a = [1, 11, 21, 1211, 111221,.

I didn't know what this list is. Google 1, 11, 21, 1211, 111221, it's called Look-and-say sequence. Now, we know what it is, the problem is simple.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a = ['1']
c1, c2 = '1', ''
for _ in range(30):
count, i, j = 0, 0, 0
while i < len(c1):
while j < len(c1):
if c1[i] == c1[j]:
count += 1
j += 1
else:
break
c2 += str(count) + c1[i]
i = j
count = 0
a.append(c2)
c1 = c2
c2 = ''
print(len(a[30]))

And the url for the next level shows.

#Level 11

odd even, and just a picture. Obviously, we are gonna have fun with the picture.

Try splitting the picture by the odd/even of the sum of x,y of each pixel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import cv2
import numpy as np
img = cv2.imread("assets/cave.jpg")
x, y, _ = img.shape
img1, img2 = np.zeros((x//2,y//2,3), np.uint8), np.zeros((x//2,y//2,3), np.uint8)
for i in range(x):
for j in range(y):
if (i+j)%2 == 1:
img1[i//2][j//2] = img[i][j]
else:
img2[i//2][j//2] = img[i][j]
cv2.imshow("odd", img1)
cv2.imshow("even", img2)
cv2.waitKey(0)

The url for the next level lies in the "even" picture.

#Level 12

dealing evil. What do you mean?

The picture is called evil1.jpg. Maybe there're pictures called evil2, evil3 maybe even evil4, evil5.

evil2.jpg, it says not jpg, _.gfx

What is a .gfx file? Google it!
The GFX file type is primarily associated with Cue Club Image File. ->source
Ah, it's also a image file.

Go on, evil3.jpg. OK, it says no more evils. The information has to be in the evil2.gfx and evil1.jpg then.

FYI: You need to try evil4 and evil5, for problems after.

Open evil2.gfx, and convert it directly to evil2.jpg

1
2
3
4
with open("assets/evil2.gfx", "rb") as f:
text = f.read()
with open("assets/evil2.jpg", "wb") as f:
f.write(text)

Well, it doesn't work.

OH, "dealing" cards. Maybe we should split the evil2.gfx into several files.

Try to split it into 2 files all the way to 5 files. And finally some visible image. The answers are in the pictures.

1
2
3
4
5
with open("assets/evil2.gfx", "rb") as f:
text = f.read()
for i in range(5):
with open(f'assets/{i}.jpg', 'wb') as f:
f.write(text[i::5])

Wait, what does evil1.jpg do? Well, all the cards are 5, could be a hint I ignored...

#Level 13

Click on button 5, and we get a xml file.

To be honest, I know nothing about this problem. I had to look for solutions online. Here are something I learned during the search.

xml-rpc is a POST request in the form of xml. The response is also in the form of xml. There're xmlrpc.client and xmlrpc.server modules in python to perform these kinds of task. As the name indicates, xmlrpc.client is what we need for this problem.

1
2
3
import xmlrpc.client

post = xmlrpc.client.ServerProxy("http://www.pythonchallenge.com/pc/phonebook.php)

First construct a ServerProxy subject, then use system.listMethods method to list all the methods this server provides.

1
print(post.system.listMethods())

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

Among these, phone seems like a unique method of this server, use system.methodHelp to display its description

1
print(post.system.methodHelp("phone"))

Returns the phone of a person

OK now, we've known which method to use. The only condition left is the person. According to some online solutions, there is some information in evil4.jpg. I was too naive to trust its saying. However, because the site was constructed a long while age, only IE browser could get access to this page. We can use curl to read the page. The output indicates this person should be "Bert".

So we "phone" Bert.

1
print(post.phone("Bert"))

555-ITALY

#Level 14

"Walk around", and a picture. Maybe this problem has something to do with circles or spirals. The bar(?) picture should be the picture that we should deal with.

Don't forget the comment of the page

remember: 100*100 = (100+99+99+98) + (...

Noted the information of this wire.png in DevTools:

Rendered size:100 × 100 px
Rendered aspect ratio:1∶1
Intrinsic size:10000 × 1 px
Intrinsic aspect ratio:10000∶1
File size:15.9 kB
Current source:http://www.pythonchallenge.com/pc/return/wire.png

Although it was displayed in the size of 100px*100px, the picture is actually in the size of 10000px*1px.

What is the equation then?

For example, here's a picture of 5px*5px. When you walk from the outside of it, and spirally inside, as shown below.

spiral

the "steps" you take until you make a turn is 5,4,4,3,3,2,2,1,1

Now, let's try 100px*100px, the equation can be deducted.

100*100 = 100+99+99+98+98+97+97+...+3+3+2+2+1+1
=(100+99+99+98)+(98+97+97+96)+...+(5+4+4+3)+(3+2+2+1)+1

So, the solution is to put every pixel of wire.png into a 100px*100px image "spirally".

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cv2
import numpy as np

img = cv2.imread("assets/wire.png")
img1 = np.zeros((300, 300, 3), np.uint8)
img1.fill(200)
dirs = [(0,1), (1,0), (0,-1), (-1,0)]
count, i, pos_x, pos_y = 200, 0, -1, 0
dis = count//2
while dis > 0:
for dir in dirs:
dis = count//2
for _ in range(dis):
pos_x = pos_x + dir[0]
pos_y = pos_y + dir[1]
img1[pos_x, pos_y] = img[0, i]
i += 1
count -= 1
cv2.imshow("result", img1)
cv2.waitKey(0)

Voila~. try cat.html. Oh, he's Uzi.

#Level 15

I'm gonna say it out directly. The musician Mozart.

Why? Look at the comment.

todo: buy flowers for tomorrow

So I searched for celebrities born in Jan 27th. And the first result is WOLFGANG AMADEUS MOZART.

Now, although having solved the problem, we're supposed to analyze the problem as well.

Jan 27th has to be a major day. Look at the bottom-right corner, there're 29 days in Feb that year. So the year 1XX6 is a leap year, Jan 1 that year is a Thursday. What's more

he ain't the youngest, he is the second

Then we're looking at the second nearest leap year ends with number 6.

1
2
3
4
5
6
7
8
9
10
11
import calendar

front, count = 2020, 0
while True:
year = front + 6
if calendar.isleap(year) and calendar.weekday(year, 1, 1) == 3:
count += 1
front -= 10
if count == 2:
print(year)
break

#Level 16

Let me get this straight

Seems that we should align these purple blocks in a straight line? If you observe the image with Snipaste, you are gonna find out there's only one purple block in each line, and each block consists of exactly 5 pixels.

Well, opencv module can't deal with .gif file. I had to spend some time learning the pillow module.

First we need to find out where these blocks situated in each line. The way to do it is to find five consecutive pixels with the same value.

1
2
3
4
5
6
7
8
9
10
11
12
13
from PIL import Image
img = Image.open("assets/mozart.gif")
pixels = [img.getpixel((pos_x, 0)) for pos_x in range(img.size[0])]
flag = 0
for i in range(len(pixels)-5):
for j in range(1, 5):
if pixels[i] == pixels[i+j]:
flag = 1
else:
flag = 0
break
if flag == 1:
print(pixels[i])

195

The value we're looking for is 195. Then we crop all the pixels in front and put them behind.

1
2
3
4
5
6
7
8
9
from PIL import Image
img = Image.open("assets/mozart.gif")
for pos_y in range(img.size[1]):
pixels = [img.getpixel((pos_x, pos_y)) for pos_x in range(img.size[0])]
flag = pixels.index(195)
pixels = pixels[flag:] + pixels[:flag]
for pos_x in range(img.size[0]):
img.putpixel((pos_x,pos_y), pixels[pos_x])
img.show()

What is the answer?

#Level 17

The bottom-left corner seems familiar. Yeah, it's the picture of level 4. So we're supposed to use http request modules for this one?

Cookies, what's in the cookies of this page?

you%20should%20have%20followed%20busynothing...

OK, we should replace the url ...?busynothing=12345 then.

Well, not exactly. All we got is a that's it. That's probably the end point of the request session. Maybe we should look at the cookies during this session as well?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re
import requests
url='http://www.pythonchallenge.com/pc/def/linkedlist.php?busynothing=12345'
r=requests.get(url)
txt=r.text
cookies = r.cookies['info']
while True:
url='http://www.pythonchallenge.com/pc/def/linkedlist.php?busynothing='+txt.split()[-1]
r=requests.get(url)
txt = r.text
print(txt)
cookies += r.cookies['info']
result = re.search('the next busynothing is (\d+)', txt)
if result == None:
break
print(cookies)

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

The beginning seems like the charactors in level 8, ignoring the encoding.

1
2
line = urllib.parse.unquote_to_bytes(cookies)
print(line)

b'BZh91AY&SY\x94:\xe2I\x00\x00!\x19\x80P\x81\x11\x00\xafg\x9e\xa0 \x00hE=M\xb5#\xd0\xd4\xd1\xe2\x8d\x06\xa9\xfa&S\xd4\xd3!\xa1\xeai7h\x9b\x9a+\xbf`"\xc5WX\xe1\xadL\x80\xe8V<\xc6\xa8\xdbH&32\x18\xa8x\x01\x08!\x8dS\x0b\xc8\xaf\x96KO\xca2\xb0\xf1\xbd\x1du\xa0\x86\x05\x92s\xb0\x92\xc4Bc\xf1w$S\x85\t\tC\xae$\x90'

Now, it looks exactly the same as bzip2 compressed content.

1
print(bz2.decompress(line).decode("utf-8"))

is it the 26th already? call his father and inform him that "the flowers are on their way". he'll understand.

call his father, and today is 26th. We are probably looking for Mozart's father. Google it! He's Leopold. Then call should be the phone method in level 13.

1
2
3
4
import xmlrpc.client

post = xmlrpc.client.ServerProxy("http://www.pythonchallenge.com/pc/phonebook.php")
print(post.phone("Leopold"))

555-VIOLIN

Replace the url with .../violin.html.

no! i mean yes! but ../stuff/violin.php.

Go on. Well, the page is called it's me. What do you want? The content of the page is a portrait of Leopold Mozart. It's obvious that we should communicate with him then. Try modifying the cookies of the page.

1
2
3
4
5
6
import requests

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

And we get the result of

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<title>it's me. what do you want?</title>
<link rel="stylesheet" type="text/css" href="../style.css">
</head>
<body>
<br><br>
<center><font color="gold">
<img src="leopold.jpg" border="0"/>
<br><br>
oh well, don't you dare to forget the balloons.</font>
</body>
</html>

So, balloons. Oh, replace the url for level 17 rather than the url for the portrait.

#Level 18

Can you tell the difference?

One is darker?

it is more obvious that what you might think

OK, if you say so. Try luminance/brightness. There's nothing different in brightness.html. However the comment has changed a bit.

maybe consider deltas.gz

Replace the url. A deltas.gz file is downloaded then. We need gzip module then.

1
2
3
4
import gzip
with gzip.open('assets/deltas.gz', 'rb') as f:
content = f.read().decode()
print(content)

A interesting output is here. There're two columns. Maybe we should find out the difference of these two columns. We need difflib module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import gzip, difflib
file1, file2 = [], []
with gzip.open('assets/deltas.gz', 'rb') as f:
content = f.readline().decode()
while content:
file1.append(content[:53]+'\n')
file2.append(content[56:])
content = f.readline().decode()
diff = difflib.Differ().compare(file1, file2)
l1, l2, common = [], [], []
for line in diff:
if line[0] == '-':
l1.append(line[2:-1])
elif line[0] == '+':
l2.append(line[2:-1])
else:
common.append(line[2:-1])
print(f"l1: {l1}")
print(f"l2: {l2}")
print(f"common: {common}")

l1: ['89 50 4e 47 0d 0a 1a 0a ...]
l2: ['89 50 4e 47 0d 0a 1a 0a ...]
common: ['89 50 4e 47 0d 0a 1a 0a ...]

If you are familiar with the file structure of png files, you know what to do.

The first eight bytes of the result is the signature of a png file. ->source

Now we save the results in corresponding png files. The url, username and password lies right there.

1
2
3
4
5
6
7
8
9
with open('assets/lvl18_1.png', 'wb') as f1:
for line in l1:
f1.write(bytes([int(o, 16) for o in line.split(" ") if o]))
with open('assets/lvl18_2.png', 'wb') as f2:
for line in l2:
f2.write(bytes([int(o, 16) for o in line.split(" ") if o]))
with open('assets/lvl18_3.png', 'wb') as f3:
for line in common:
f3.write(bytes([int(o, 16) for o in line.split(" ") if o]))

#Level 19

A map of India, and a email in the comment.

According to the email, we're supposed to get a audio with base64 encoding.

1
2
3
4
5
6
import base64
with open("assets/txt_for_19.txt", "r") as f:
content = f.read()
audio = base64.b64decode(content)
with open("assets/indian.wav", 'wb') as f:
f.write(audio)

All we can hear is

Sorry!

Try sorry.html

  • "what are you apologizing for?"

Doesn't seem right. There has to be something we're missing.

I didn't know much of audios. This happens to be a brilliant chance to learn about wave module.

1
2
3
4
5
import wave

with wave.open('assets/indian.wav', 'rb') as sound:
params = sound.getparams()
print(params)

_wave_params(nchannels=1, sampwidth=2, framerate=11025, nframes=55788, comptype='NONE', compname='not compressed')

The information of the audio is clear as it says.

Noted the color in the map is reversed. Ocean is usually in blue and continent in yellow. Let's try reversing the audio.

1
2
3
4
5
6
7
8
import wave

with wave.open('assets/indian.wav', 'rb') as sound:
params = sound.getparams()
content = sound.readframes(sound.getnframes())
with wave.open('assets/results.wav', 'wb') as output:
output.setparams(params=params)
output.writeframes(content[::-1])

Nothing we can hear in the result. Maybe reverse every frame of the audio.

1
2
3
4
5
6
7
8
import wave

with wave.open('assets/indian.wav', 'rb') as sound:
with wave.open('assets/results.wav', 'wb') as output:
output.setparams(sound.getparams())
for i in range(sound.getnframes()):
frame = sound.readframes(1)
output.writeframes(frame[::-1])

"you are an idiot~ ah, ah, ah, ah ...

try idiot.html. Oh, it's Leopold. He says, "Now you should apologize..." and gives the link to the next level.

OK, We have it.

#Level 20

Well, I have no idea what to do. Again, I learned something during searching online.

response headers

The picture above are the response headers of unreal.jpg. Now if we modify the Range value in the request headers, we'll get access to files of other range.

It's clear that between 0 and 30202 bytes lies unreal.jpg. Let's go on to the next section. Set Range value to bytes=30203-

1
2
3
4
5
6
7
8
9
10
import requests, base64

url = 'http://www.pythonchallenge.com/pc/hex/unreal.jpg'
cred = base64.b64encode(b"butter:fly")
requests.auth.HTTPBasicAuth("butter", "fly")
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=30203-'}

r = requests.get(url, headers=headers)
print(r.headers)
print(r.content.decode())

{'Connection': 'close', 'Content-Length': '34', 'Content-Range': 'bytes 30203-30236/2123456789', 'Content-Transfer-Encoding': 'binary', 'Content-Type': 'application/octet-stream', 'Date': 'Mon, 18 Apr 2022 10:22:00 GMT', 'Server': 'lighttpd/1.4.55'}
Why don't you respect my privacy?

Go on.

1
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=30237-'}

{'Connection': 'close', 'Content-Length': '47', 'Content-Range': 'bytes 30237-30283/2123456789', 'Content-Transfer-Encoding': 'binary', 'Content-Type': 'application/octet-stream', 'Date': 'Mon, 18 Apr 2022 10:55:42 GMT', 'Server': 'lighttpd/1.4.55'}
we can go on in this way for really long time.

1
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=30284-'}

{'Connection': 'close', 'Content-Length': '11', 'Content-Range': 'bytes 30284-30294/2123456789', 'Content-Transfer-Encoding': 'binary', 'Content-Type': 'application/octet-stream', 'Date': 'Mon, 18 Apr 2022 11:07:43 GMT', 'Server': 'lighttpd/1.4.55'}
stop this!

1
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=30295-'}

{'Connection': 'close', 'Content-Length': '18', 'Content-Range': 'bytes 30295-30312/2123456789', 'Content-Transfer-Encoding': 'binary', 'Content-Type': 'application/octet-stream', 'Date': 'Mon, 18 Apr 2022 14:22:22 GMT', 'Server': 'lighttpd/1.4.55'}
invader! invader!

1
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=30313-'}

{'Connection': 'close', 'Content-Length': '34', 'Content-Range': 'bytes 30313-30346/2123456789', 'Content-Transfer-Encoding': 'binary', 'Content-Type': 'application/octet-stream', 'Date': 'Mon, 18 Apr 2022 14:23:12 GMT', 'Server': 'lighttpd/1.4.55'}
ok, invader. you are inside now.

1
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=30347-'}

{'Connection': 'close', 'Content-Type': 'text/html; charset=UTF-8', 'Date': 'Mon, 18 Apr 2022 14:23:50 GMT', 'Server': 'lighttpd/1.4.55', 'Content-Length': '0'}

Nothing here. Maybe we should look for the final section.

1
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=2123456879-'}

{'Connection': 'close', 'Content-Length': '45', 'Content-Range': 'bytes 2123456744-2123456788/2123456789', 'Content-Transfer-Encoding': 'binary', 'Content-Type': 'application/octet-stream', 'Date': 'Mon, 18 Apr 2022 14:25:37 GMT', 'Server': 'lighttpd/1.4.55'}
esrever ni emankcin wen ruoy si drowssap eht

Reverse the output.

the password is your new nickname in reverse

"nickname" should be the "invader" mentioned before. Hence the password should be redavni.

Go forward.

1
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=2123456743-'}

{'Connection': 'close', 'Content-Length': '32', 'Content-Range': 'bytes 2123456712-2123456743/2123456789', 'Content-Transfer-Encoding': 'binary', 'Content-Type': 'application/octet-stream', 'Date': 'Mon, 18 Apr 2022 14:26:54 GMT', 'Server': 'lighttpd/1.4.55'}
and it is hiding at 1152983631.

The required Range value is here.

1
headers = {'Authorization': f"Basic {cred.decode()}", 'Range':'bytes=1152983631-'}

The output was too much. Let's take a look at the first 20 bytes of the output. The signature of file is always hiding there.

1
print(r.content[:20])

{'Connection': 'close', 'Content-Length': '239733', 'Content-Range': 'bytes 1152983631-1153223363/2123456789', 'Content-Transfer-Encoding': 'binary', 'Content-Type': 'application/octet-stream', 'Date': 'Mon, 18 Apr 2022 14:30:03 GMT', 'Server': 'lighttpd/1.4.55'}
b'PK\x03\x04\x14\x00\t\x00\x08\x00;\xa7\xaa2\xac\xe5f\x14\xa9\x00'

The PK\x03\x04 is the signature of a zip file. ->source

Then we should save the output to a zip file and try to unzip it.

1
2
with open('assets/20.zip', 'wb') as f:
f.write(r.content)

There's a readme.txt. OK, we are in level 21 now.

Python Challenge Notes (Part 2) Flashing Pixel Experience into OP 8T
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×