RLotto
6 minutes to read
We are given the source code of a server that gives five numbers and the objective is to guess the next five numbers to win the lottery:
$ nc 46.101.63.21 31102
__/\____/\____/\____/\____/\____/\____/\____/\__
\ /\ /\ /\ /\ /\ /\ /\ /
/_ _\/_ _\/_ _\/_ _\/_ _\/_ _\/_ _\/_ _\
\/ \/ \/ \/ \/ \/ \/ \/
__ _ _ ____ ___ ____ ___
/ / ___ | |_| |_ ___|___ \ / _ \___ \ / _ \
/ / / _ \| __| __/ _ \ __) | | | |__) | | | |
/ /__| (_) | |_| || (_) / __/| |_| / __/| |_| |
\____/\___/ \__|\__\___/_____|\___/_____|\___/
__/\____/\____/\____/\____/\____/\____/\____/\__
\ /\ /\ /\ /\ /\ /\ /\ /
/_ _\/_ _\/_ _\/_ _\/_ _\/_ _\/_ _\/_ _\
\/ \/ \/ \/ \/ \/ \/ \/
------------------------------------------------
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 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 72 73 74 75 76 77 78 79 80
81 82 83 84 85 86 87 88 89 90
------------------------------------------------
[+] EXTRACTION: 11 32 87 35 89
[?] Guess the next extraction!!!
[?] Put here the next 5 numbers: 1 2 3 4 5
Nope! Try again.
Source code analysis
This is the source code:
#!/usr/bin/env python3
import socketserver as sock
import time
import threading
import random
import sys
def build_banner():
banner = ""
banner += "__/\\____/\\____/\\____/\\____/\\____/\\____/\\____/\\__\n"
banner += "\\ /\\ /\\ /\\ /\\ /\\ /\\ /\\ /\n"
banner += "/_ _\\/_ _\\/_ _\\/_ _\\/_ _\\/_ _\\/_ _\\/_ _\\\n"
banner += " \\/ \\/ \\/ \\/ \\/ \\/ \\/ \\/ \n"
banner += " __ _ _ ____ ___ ____ ___ \n"
banner += " / / ___ | |_| |_ ___|___ \\ / _ \\___ \\ / _ \\ \n"
banner += " / / / _ \\| __| __/ _ \\ __) | | | |__) | | | | \n"
banner += "/ /__| (_) | |_| || (_) / __/| |_| / __/| |_| | \n"
banner += "\\____/\\___/ \\__|\\__\\___/_____|\\___/_____|\\___/ \n"
banner += " \n"
banner += "__/\\____/\\____/\\____/\\____/\\____/\\____/\\____/\\__\n"
banner += "\\ /\\ /\\ /\\ /\\ /\\ /\\ /\\ /\n"
banner += "/_ _\\/_ _\\/_ _\\/_ _\\/_ _\\/_ _\\/_ _\\/_ _\\\n"
banner += " \\/ \\/ \\/ \\/ \\/ \\/ \\/ \\/ \n"
banner += "------------------------------------------------"
print(banner)
return banner
def build_game_board():
gboard = ""
gboard += " ".join(str(x) for x in range(1, 11)) + "\n"
gboard += " ".join(str(x) for x in range(11, 21)) + "\n"
gboard += " ".join(str(x) for x in range(21, 31)) + "\n"
gboard += " ".join(str(x) for x in range(31, 41)) + "\n"
gboard += " ".join(str(x) for x in range(41, 51)) + "\n"
gboard += " ".join(str(x) for x in range(51, 61)) + "\n"
gboard += " ".join(str(x) for x in range(61, 71)) + "\n"
gboard += " ".join(str(x) for x in range(71, 81)) + "\n"
gboard += " ".join(str(x) for x in range(81, 91)) + "\n"
gboard += "------------------------------------------------"
print(gboard)
return gboard
def edit_game_board(number):
if number < 0 or number > 90:
return
r = 10 - int((number-1) / 10)
c = int((number-1) % 10)
str_mod = ""
for i in range(r):
print('\033[A', end="")
str_mod += '\033[A'
for i in range(c):
print('\033[C\033[C\033[C\033[C\033[C', end="")
str_mod += '\033[C\033[C\033[C\033[C\033[C'
print('\033[32m\033[1m'+str(number)+'\033[0m', end="")
str_mod += '\033[32m\033[1m'+str(number)+'\033[0m'
for i in range(r):
print('\033[B', end="")
str_mod += '\033[B'
print('\r', end="")
str_mod += '\r'
return str_mod
def build_summary(extracted):
print("\033[31m[+]\033[0m EXTRACTION: ", end="")
summary = "\033[31m[+]\033[0m EXTRACTION: "
for i in extracted:
print(str(i) + " ", end="")
summary += str(i) + " "
print('\r', end="")
summary += '\r'
return summary
class Service(sock.BaseRequestHandler):
allow_reuse_address = True
# Connection handler
def handle(self):
print("[+] Incoming connection")
seed = int(time.time())
print("[+] Seed:", seed)
banner = build_banner()
gboard = build_game_board()
self.send(banner)
self.send(gboard)
extracted = []
next_five = []
# Initialize the (pseudo)random number generator
random.seed(seed)
# First extraction
while len(extracted) < 5:
r = random.randint(1, 90)
if(r not in extracted):
extracted.append(r)
time.sleep(1)
gboard = edit_game_board(r)
self.send(gboard, False)
summary = build_summary(extracted)
self.send(summary, False)
# Next extraction
solution = ""
while len(next_five) < 5:
r = random.randint(1, 90)
if(r not in next_five):
next_five.append(r)
solution += str(r) + " "
solution = solution.strip()
print("\n[+] SOLUTION: " + solution)
question = "\n\033[33m[?]\033[0m Guess the next extraction!!!"
self.send(question)
response = self.receive()
# CHECK
print("[>] Sent:", summary[25:])
print("[<] Recv:", response)
if str(response) == solution:
self.send("Good Job!\nHTB{f4k3_fl4g_f0r_t3st1ng}")
else:
self.send("Nope! Try again.")
# Function to send the challenge to clients
def send(self, string, newline=True):
if newline: string = string + "\n"
self.request.sendall(string.encode())
# Function to receive responses from clients
def receive(self, prompt="\033[33m[?]\033[0m Put here the next 5 numbers: "):
self.send(prompt, newline=False)
return self.request.recv(4096).strip().decode('ASCII')
class ThreadService(sock.ThreadingMixIn, sock.TCPServer, sock.DatagramRequestHandler):
pass
def main():
host = '0.0.0.0'
port = 1337
s = Service
server = ThreadService((host, port), s)
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
print ("[ Server started on port: ", str(port), "]")
while(True): time.sleep(1)
if (__name__=="__main__"):
main()
PRNG
The server uses a Pseudo-Random Number Generator (PRNG) with random
. It can be found what seed it is using to generate the random numbers:
seed = int(time.time())
Setting the same seed and executing the program will return the same first five numbers, and therefore the same expected five numbers.
We can create a script that automates the connection to the server and the transmission of the next five numbers using the time-based seed for the random numbers generation (with a time offset if needed). This is the relevant part of the script:
out = r.recvuntil(b'numbers: ').decode()
extraction, _ = re.findall(r'EXTRACTION: ((\d+ ){5})', out)[0]
ext, dt = '', 0
while ext != extraction.strip():
ext, sol = handle(now + dt)
dt += 1
if dt > 10:
print('Not found...')
sys.exit(1)
r.sendline(sol.encode())
Where handle
is a function that generates extraction numbers with a given PRNG seed:
def handle(seed):
random.seed(seed)
def gen():
numbers = []
while len(numbers) < 5:
r = random.randint(1, 90)
if r not in numbers:
numbers.append(r)
return numbers
extracted, solution = gen(), gen()
return ' '.join(map(str, extracted)), ' '.join(map(str, solution))
In brief, we try different seeds based on the current time until we find that the extraction numbers match with the ones the server has calculated. At this point, we will have the expected numbers to win the lottery.
Flag
If we run the script, we will get the flag:
$ python3 solve.py 46.101.63.21:31102
[+] Opening connection to 46.101.63.21 on port 31102: Done
[+] Receiving all data: Done (77B)
[*] Closed connection to 46.101.63.21 port 31102
HTB{n3v3r_u53_pr3d1c74bl3_533d5_1n_p53ud0-r4nd0m_numb3r_63n3r470r}
The full script can be found in here: solve.py
.