Друзья, по сложившейся за последний месяц традиции мы предлагаем вам начать новую неделю с нового райтапа. В этом посте мы подробно разберем задания из направления MISC, куда вошли все задания, не подходящие ни под какую другую категорию. Тут был нужен особенный креатив 😉
Ветка MISC нашла отклик в душе наших игроков — за время соревнований мы получили около 300 флагов. Заметим, что из всех тасков на 1000, задание из этой категории было наиболее популярным — над ним ломали голову многие, но успеха достигли всего несколько человек. Поэтому мы решили пропустить задания на 50 и 100 очков и сразу перейти к более сложным и интересным заданиям. Поехали!
MISC_300. Lithium|Beta
A.U.R.O.R.A.: Lieutenant Friend, seems like this computer is frozen and we don’t have time to fix it. So from now on we have only this calculator interface (nc). I have to admit that your predecessor Lieutenant Petr was a very lazy developer (no idea how he managed to get on this ship) and he failed to complete Compiler Design course. So he wrote calculator in the easiest way using the simplest tools. I know that it’s quite complicated but you have to hurry, we haven’t got much time!
Решение:
Запускаем программу и видим, что это обычный калькулятор.
Как следует из легенды, эту программу писал ленивый разработчик. Скорее всего, это свидетельствует о том, что вместо парсинга математических выражений используется простой eval.
Как видно на скриншоте, есть вывод названий ошибок, но без трейсов:
Судя по ошибкам, в ответ отдается результат, приведенный к типу float. Если результат привести нельзя, то возникает ValueError. Если нет такой функции, то NameError.
Попробуем выяснить список доступных функций методом перебора.
При этом все попытки использовать underscore, например, class, не работают из-за HackingAttempt.
Но, используя ord и str, можно вычислить любую переменную, например, результат dir, который возвращает список всех доступных имен в окружении.
Код бруттера:
#!/usr/bin/python2 import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("95.85.41.197", 8888)) print s.recv(1024) result = '' for i in range(0, 300): request = "ord(str(dir())[%d])" % i # request = "ord(verysecretflag[%d])" % i s.send(request) response = s.recv(32) if 'Err' in response or 'occurred' in response: result += '_' else: result += chr(int(response.split(': ')[1].split('.')[0])) print result
В результате перебора получается следующий список:
['HackingAttempt', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'e', 'f', 'inp', 'print1337', 're', 'regex', 'sys', 'verysecretflag']
Флаг лежит в переменной verysecretflag. Print, он переименован в print1337
Чтобы получить ответ, следует сделать вот так:
Вот и наш флаг!
Ответ: ctfzone{123456}
MISC_500. Archive maniac
A.U.R.O.R.A.: Oh God! Lieutenant, I need you here on the ship control station. Autopilot is broken and we need a secret code to switch to manual control. Only our pilot Chekhov knows it and he is dead drunk, so you have to figure it out. I noticed that he was concerned about storage efficiency and confidentiality. And he also preferred number 32 to 64 with no obvious reason.
Решение:
В этом задании участнику предлагается найти секретный код, однако сложность заключается в том, что этот ключ зашифрован. К сожалению, тот, кто обладает информацией, в ближайшие сутки ничего вразумительного сказать не сможет… Мы знаем, что он старался обеспечить надежное хранение и конфиденциальность, а также почему-то и предпочитал число 32 вместо 64. Попробуем разобраться.
Для начала посмотрим на наши исходные данные. В самом задании дается ссылка на архив arch.tar.gz, в котором содержатся следующие файлы:
[briskly@archlinux tmp]$ tar -xvf arch.tar.gz archive/ archive/flag3.png archive/flag9.png archive/flag7.png archive/flag6.png archive/flag2.png archive/flag4.png archive/flag5.png archive/flag0.png archive/flag1.png archive/flag8.png archive/.gitkeep [briskly@archlinux tmp]$ file archive/* archive/flag0.png: cpio archive archive/flag1.png: bzip2 compressed data, block size = 900k archive/flag2.png: bzip2 compressed data, block size = 900k archive/flag3.png: compress'd data 16 bits archive/flag4.png: LRZIP compressed data - version 0.6 archive/flag5.png: rzip compressed data - version 2.1 (370344 bytes) archive/flag6.png: compress'd data 16 bits archive/flag7.png: Zip archive data archive/flag8.png: ARJ archive data, v11, slash-switched, original name: , os: Unix archive/flag9.png: cpio archive
Похоже, что в архиве 10 файлов с расширением .png, но при этом каждый файл является архивом. Судя по листингу файлов, архивы очень разные. Пришло время учиться пользоваться экзотикой.
Пожалуй, начнем:
[briskly@archlinux archive]$ bzip2 -d flag1.png bzip2: Can't guess original name for flag1.png -- using flag1.png.out [briskly@archlinux archive]$ file flag1.png.out flag1.png.out: LRZIP compressed data - version 0.6
Похоже, что в этих архивах находятся другие архивы. Пробуем расшифровать zip файл. 7z сразу попросил пароль:
[briskly@archlinux archive]$ 7z x flag7.png 7-Zip [64] 16.02: Copyright (c) 1999-2016 Igor Pavlov: 2016-05-21 p7zip Version 16.02 (locale=ru_RU.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs Intel(R) Core(TM) i7-6600U CPU @ 2.60GHz (406E3),ASM,AES-NI) Scanning the drive for archives: 1 file, 385755 bytes (377 KiB) Extracting archive: flag7.png -- Path = flag7.png Type = zip Physical Size = 385755 Enter password (will not be echoed):
В данном случае стоит автоматизировать процесс извлечения из архива, поскольку их очень много:
#!/bin/bash FILE="$1" FLAG="flag.png" TMP_DIR="PNGs" # Dirty: [ ! -d "./$TMP_DIR" ] && mkdir "$TMP_DIR" && echo -e "\n [+] Creating temp folder: $TMP_DIR." [ ! -f "./$FLAG" ] && cp $FILE flag.png && echo -e " [+] Creating temp file: $FLAG.\n" deArch () { CHECK=`file "$FLAG"` if [[ $CHECK == *"rzip compressed data"* ]] then echo -e " [*] Now $FLAG is RZIP data (.rz)\n [+] Extracting $FLAG\n" mv flag.png{,.rz} runzip -d flag.png.rz #rm flag.png.rz sleep 1 deArch elif [[ $CHECK == *"LRZIP compressed data"* ]] then echo -e " [*] Now $FLAG is LRZIP archive (.lrz)\n [+] Extracting $FLAG\n" mv flag.png{,.lrz} lrunzip flag.png.lrz > /dev/null rm flag.png.lrz sleep 1 deArch elif [[ $CHECK == *"bzip2 compressed data"* ]] then echo -e " [*] Now $FLAG is BZIP file (.bz2)\n [+] Extracting $FLAG\n" mv flag.png{,.bz2} bzip2 -d flag.png.bz2 #> /dev/null sleep 1 deArch elif [[ $CHECK == *"compress'd data 16 bits"* ]] then echo -e " [*] Now $FLAG is unix compressed file (.z)\n [+] Extracting $FLAG\n" mv flag.png{,.z} uncompress flag.png.z sleep 1 deArch elif [[ $CHECK == *"7-zip archive data"* ]] then echo -e " [*] Now $FLAG is 7-ZIP archive (.7z)\n [+] Extracting $FLAG\n" mv flag.png{,.7z} 7z x flag.png.7z > /dev/null rm flag.png.7z sleep 1 deArch elif [[ $CHECK == *"ARJ archive data, v11, slash-switched"* ]] then echo -e " [*] Now $FLAG is ARJ archive (.arj)\n [+] Extracting $FLAG\n" mv flag.png{,.arj} arj x flag.png.arj > /dev/null rm flag.png.arj sleep 1 deArch elif [[ $CHECK == *"cpio archive"* ]] then echo -e " [*] Now $FLAG is CPIO archive (.cpio)\n [+] Extracting $FLAG\n" mv flag.png{,.cpio} cpio -idv < flag.png.cpio 2> /dev/null rm flag.png.cpio sleep 1 deArch elif [[ $CHECK == *"current ar archive"* ]] then echo -e " [*] Now $FLAG is AR archive (.a)\n [+] Extracting $FLAG\n" mv flag.png{,.a} ar x flag.png.a rm flag.png.a sleep 1 deArch elif [[ $CHECK == *"Zip archive data"* ]] then echo -e " [*] Now $FLAG is zip archive (.zip)\n [+] Extracting $FLAG\n" mv flag.png{,.zip} #mv flag.png{,.7z} ENC_CHCK=`7z l -slt -- flag.png.zip | grep -ic "Encrypted = +"` if [ "$ENC_CHCK" -eq "1" ] then #exit 1 echo " [!] PASSWORD pretocted archive" zip2john flag.png.zip | awk -F: '{print $2}' > hash.lst rm -rf /root/.john/john.* #ZIP_PASS=`john hash.lst 2>&1 > /dev/null | awk '/\(\?\)/ {print $1}'` ZIP_PASS=`john hash.lst --wordlist=/usr/share/wordlists/rockyou.txt 2>&1 | awk '/\(\?\)/ {print $1}'` if [[ -z "$ZIP_PASS" ]] then #echo -e "$ZIP_PASS" echo -e " [-] Your pass was not found, please, try it manually..." else echo -e " [+] Voila! Your pass is: \e[1;33m$ZIP_PASS\e[0;0m, extracting an archive...\n" 7z x -p"$ZIP_PASS" flag.png.zip > /dev/null rm flag.png.zip sleep 1 deArch fi else 7z x flag.png.zip sleep 1 deArch fi #sleep 1 #deArch elif [[ $CHECK == *"PNG image data"* ]] then echo -e " [\e[1;32m*\e[0;0m] Now $FLAG is PNG image file !!!\n [\e[1;32m+\e[0;0m] Open this: ./$TMP_DIR/$FILE\n" sleep 1 #eog flag.png 2> /dev/null mv $FLAG $TMP_DIR/$FILE exit 0 else echo -e "\n [-] Hernya! $CHECK" fi } deArch
Результат работы скрипта:
bash deArch.sh flag4.png [+] Creating temp folder: PNGs. [+] Creating temp file: flag.png. [*] Now flag.png is ARJ archive (.arj) [+] Extracting flag.png [*] Now flag.png is zip archive (.zip) [+] Extracting flag.png [!] PASSWORD pretocted archive [+] Voila! Your pass is: love123, extracting an archive... [*] Now flag.png is unix compressed file (.z) [+] Extracting flag.png [*] Now flag.png is AR archive (.a) [+] Extracting flag.png [*] Now flag.png is BZIP file (.bz2) [+] Extracting flag.png [*] Now flag.png is CPIO archive (.cpio) [+] Extracting flag.png [*] Now flag.png is RZIP data (.rz) [+] Extracting flag.png [*] Now flag.png is 7-ZIP archive (.7z) [+] Extracting flag.png [*] Now flag.png is LRZIP archive (.lrz) [+] Extracting flag.png [*] Now flag.png is PNG image file !!! [+] Open this: ./PNGs/flag4.png
В результате из файла вытаскивается flag4.png
Итак, получена картинка! Посмотрим, что внутри – может быть, там есть стеганография?
Для этого запустим Stegsolve, и, изменив некоторые настройки, получим что-то вполне разборчивое:
Судя по знакам =, это похоже на base64. Но все буквы заглавные (uppercase). Вспомним легенду, где сказано, что тот человек, который обладал знаниями, предпочитал число 32. Попробуем base32, в результате чего получаем TPAU’XAPDEP.
Выполняем те же действия по отношению к остальным файлам.
В итоге получаем следующее:
thisnotaflag thisisflag,joke noflaghere noo000000op CTFZONE{5dbb39d62d31b1c notflagagain flagwashere 025f3b0e3a987d375}part2 kakoyflag? TPAU'XAPDEP
Вот и наш флаг!
Ответ: ctfzone{5dbb39d62d31b1c025f3b0e3a987d375}part2
MISC_1000. Molibden|Gamma
A.U.R.O.R.A.: Lieutenant, you’ve got to the command center. It’s time to go home and join our comrades! Wait, something is wrong with the systems. Some basic libraries are lost. Computer can’t find the route. You need to help computer make some simple calculations. Quick, we are almost there!
Решение:
Итак, мы практически у цели! Чтобы получить управление кораблем, необходимо исправить ошибку в системе.
Итак, запускаем программу. Пример работы программы мы видим на скриншоте.
Из легенды понятно, что нам нужно интерпретировать код, который присылает сервер.
Для получения решения придется показать свои навыки программирования.
Так как код написан на Python, то первое очевидное решение – это сделать exec и отправить его результат обратно. Код будет выглядеть следующим образом:
from socket import create_connection from time import time sock = create_connection(("95.85.41.197", 8887)) for i in range(10): res = None code = sock.recv(102400) code = code.decode() code = "\n".join(code.split("\n")[:-2]) if "gone wrong" in code: print(code) exit(0) start = time() l = res = None print(code) exec(code) res = res or l print(time() - start) code = None res = str(res).encode() try: sock.send(res + b"\n") except Exception: print(sock.recv(102400)) print(sock.recv(102400)) print(sock.recv(102400)) break
После этого приходит код со sleep, который тормозит исполнение:
from time import sleep k = 96 s = 36 c = 98 mas = [] for i in range(c): sleep(0.1) mas.append(s) s += k res = 0 for el in mas: sleep(0.1) res += el print(res)
Эта проблема решается просто вырезанием sleep по регулярке. Например, вот так:
code = code.replace("sleep(0.1)", "")
Следующее усложнение заключается в том, что sleep переименовывается при импорте:
from time import sleep as JecYvyk
В данном случае можно написать регулярку или просто сделать replace:
code = code.replace("sleep", "gmtime")
В следующий раз решение останавливается на шаге с кодом, в котором используются большие числа:
from time import gmtime as rluVx k = 82194181 s = 55474764 c = 54888629 mas = [] for i in range(c): rluVx(2*0.01) mas.append(s) s += k res = 0 for el in mas: rluVx(2*0.01) res += el print(res)
На данном этапе программа не выполняется по двум причинам — либо заканчивается память, либо сервер выдает ошибку:
Something gone wrong: TimeoutError
Очевидно, что просто решить данную задачу не получится, придется разбираться в коде. При первой оценке кода можно заметить, что это очень похоже на сумму арифметической прогрессии.
Так как функция написана действительно неэффективно, то вместо обычного подсчета линейной формулой, формируется массив со всеми элементами арифметической прогрессии, а потом складывается. Напишем эту формулу:
def solve(k, s, c): return str((2*s + k*(c-1))*c//2)
Выглядит она довольно просто.
Следующая проблема заключается в том, что перестает успевать выполняться код:
from time import sleep as Rkyv from random import shuffle l = [5984807, 6299947, 10119240, 13578507, 14224900, 15238270, 15513380, 16429758] while True: Rkyv(0o10*0.01) shuffle(l) prev = None is_sorted = True for el in l: Rkyv(0o10*0.01) if prev is None: prev = el elif prev >= el: is_sorted = False break prev = el if is_sorted: break print(l)
Придется разобраться и с этой задачей. Очень похоже на monkey_sort, но, судя по всему, этот код выполнить невозможно. Перепишем на обычный sort, который предоставляет нам Python. Но и этого оказывается недостаточно.
В результате наступает момент, когда обычный exec перестает успевать считать ‘страшный’ обфусцированный код:
from time import sleep as EB s = b'Vt\xe9\xe9\xed\x05J\xdaEQ' def func3(s): def func1(OOOOOOOOO0000OOO0 ): "" PI_SUBST =[41 ,46 ,67 ,201 ,162 ,216 ,124 ,1 ,61 ,54 ,84 ,161 ,236 ,240 ,6 ,19 ,98 ,167 ,5 ,243 ,192 ,199 ,115 ,140 ,152 ,147 ,43 ,217 ,188 ,76 ,130 ,202 ,30 ,155 ,87 ,60 ,253 ,212 ,224 ,22 ,103 ,66 ,111 ,24 ,138 ,23 ,229 ,18 ,190 ,78 ,196 ,214 ,218 ,158 ,222 ,73 ,160 ,251 ,245 ,142 ,187 ,47 ,238 ,122 ,169 ,104 ,121 ,145 ,21 ,178 ,7 ,63 ,148 ,194 ,16 ,137 ,11 ,34 ,95 ,33 ,128 ,127 ,93 ,154 ,90 ,144 ,50 ,39 ,53 ,62 ,204 ,231 ,191 ,247 ,151 ,3 ,255 ,25 ,48 ,179 ,72 ,165 ,181 ,209 ,215 ,94 ,146 ,42 ,172 ,86 ,170 ,198 ,79 ,184 ,56 ,210 ,150 ,164 ,125 ,182 ,118 ,252 ,107 ,226 ,156 ,116 ,4 ,241 ,69 ,157 ,112 ,89 ,100 ,113 ,135 ,32 ,134 ,91 ,207 ,101 ,230 ,45 ,168 ,2 ,27 ,96 ,37 ,173 ,174 ,176 ,185 ,246 ,28 ,70 ,97 ,105 ,52 ,64 ,126 ,15 ,85 ,71 ,163 ,35 ,221 ,81 ,175 ,58 ,195 ,92 ,249 ,206 ,186 ,197 ,234 ,38 ,44 ,83 ,13 ,110 ,133 ,40 ,132 ,9 ,211 ,223 ,205 ,244 ,65 ,129 ,77 ,82 ,106 ,220 ,55 ,200 ,108 ,193 ,171 ,250 ,36 ,225 ,123 ,8 ,12 ,189 ,177 ,74 ,120 ,136 ,149 ,139 ,227 ,99 ,232 ,109 ,233 ,203 ,213 ,254 ,59 ,0 ,29 ,57 ,242 ,239 ,183 ,14 ,102 ,88 ,208 ,228 ,166 ,119 ,114 ,248 ,235 ,117 ,75 ,10 ,49 ,68 ,80 ,180 ,143 ,237 ,31 ,26 ,219 ,153 ,141 ,51 ,159 ,17 ,131 ,20 ] O00OOOO0OOOO00000 =OOOOOOOOO0000OOO0 OO00OO000OO0OO000 =len (O00OOOO0OOOO00000 ) O00OOOO0OOOO00000 +=chr (16 -(OO00OO000OO0OO000 %16 )).encode ("utf-8")*(16 -(OO00OO000OO0OO000 %16 )) O00OOOO0O0OOO00O0 =O00OOOO0OOOO00000 OO00OO000OO0OO000 =len (O00OOOO0OOOO00000 ) OOOOO0O0000000OO0 =bytearray (b"\x00"*16 ) OO0O0O000OOOO0O0O =0 for O0OOO0OO000OO00O0 in range (OO00OO000OO0OO000 //16 ): EB(0x3*0.01) for OO00O00OOO000OO00 in range (16 ): EB(0x3*0.01) O0O0OOOO000O0OO0O =O00OOOO0O0OOO00O0 [O0OOO0OO000OO00O0 *16 +OO00O00OOO000OO00 ] OOOOO0O0000000OO0 [OO00O00OOO000OO00 ]=OOOOO0O0000000OO0 [OO00O00OOO000OO00 ]^PI_SUBST [O0O0OOOO000O0OO0O ^OO0O0O000OOOO0O0O ] OO0O0O000OOOO0O0O =OOOOO0O0000000OO0 [OO00O00OOO000OO00 ] OO0OOO00OOO00000O =O00OOOO0O0OOO00O0 +OOOOO0O0000000OO0 OO00OO000OO0OO000 +=16 OOO00O00O0O0O0OO0 =bytearray ([0 ])*48 for O0OOO0OO000OO00O0 in range (OO00OO000OO0OO000 //16 ): for OO00O00OOO000OO00 in range (16 ): EB(0x3*0.01) OOO00O00O0O0O0OO0 [16 +OO00O00OOO000OO00 ]=OO0OOO00OOO00000O [O0OOO0OO000OO00O0 *16 +OO00O00OOO000OO00 ] OOO00O00O0O0O0OO0 [32 +OO00O00OOO000OO00 ]=OOO00O00O0O0O0OO0 [16 +OO00O00OOO000OO00 ]^OOO00O00O0O0O0OO0 [OO00O00OOO000OO00 ] OOOO0O0OO000000OO =0 for OO00O00OOO000OO00 in range (18 ): EB(0x3*0.01) for OOOO00O00OOO0OOOO in range (48 ): EB(0x3*0.01) OOOO0O0OO000000OO =OOO00O00O0O0O0OO0 [OOOO00O00OOO0OOOO ]=OOO00O00O0O0O0OO0 [OOOO00O00OOO0OOOO ]^PI_SUBST [OOOO0O0OO000000OO ] OOOO0O0OO000000OO =(OOOO0O0OO000000OO +OO00O00OOO000OO00 )%256 return bytes (OOO00O00O0O0O0OO0 [:16 ]) OO0OOOO00OO0O0O0O = b'Vt\xe9\xe9\xed\x05J\xdaEQ' def func2(OO0OOOO00OO0O0O0O): O00O00OO000OOO00O ='0123456789abcdef' return b''.join(map(lambda x: x.encode(), map(lambda O00O0OO0O0OOOO0O0 :O00O00OO000OOO00O [(O00O0OO0O0OOOO0O0 >>4 )&0xf ]+O00O00OO000OOO00O [O00O0OO0O0OOOO0O0 &0xf ],func1 (OO0OOOO00OO0O0O0O )))) for i in range(100): OO0OOOO00OO0O0O0O = func2(OO0OOOO00OO0O0O0O) return OO0OOOO00OO0O0O0O.decode() #print(''.join (map (lambda O00O0OO0O0OOOO0O0 :O00O00OO000OOO00O [(O00O0OO0O0OOOO0O0 >>4 )&0xf ]+O00O00OO000OOO00O [O00O0OO0O0OOOO0O0 &0xf ],func1 (OO0OOOO00OO0O0O0O )))) res = func3(s) print(res)
В ходе небольшой деобфускации становится понятно, что скорее всего это какая-то хеш функция, которая последовательно применяется 100 раз. Далее пройти можно двумя способами: либо просто переписать данную функцию на чем-то более быстром (например, C++), либо попробовать поискать таблицу замен, которая захардкожена в коде.
Попробуем погуглить:
Достаточно легко догадаться, что это md2. В PYCRYPTO есть быстрая реализация этой функции:
from Crypto.Hash import MD2 def solve(inp): for i in range(params.get("count")): h = MD2.new() h.update(inp) inp = h.hexdigest() return inp
Результирующий код solver:
import re import json from socket import create_connection from solves import md2 from solves import arifmetic from time import time sock = create_connection(("95.85.41.197", 8887)) r1 = re.compile(r"l = (\[.*\])") r21 = re.compile(r"OO0OOOO00OO0O0O0O = (b('|\").*('|\"))\n") r22 = re.compile(r" for i in range\((\d+)\):") r3 = re.compile(r".*k = (?P<k>\d+)\ns = (?P<s>\d+)\nc = (?P<c>\d+).*") def solve1(code): reverse = True if ">" in code: reverse = False code = code.split("while True:")[0] lst = r1.findall(code)[0] lst = json.loads(lst) lst.sort(reverse=reverse) return lst def solve2(code): data = eval((r21.findall(code))[0][0]) count = int(r22.findall(code)[0]) res = md2.solve({"string": data, "count": count}) return res def solve3(code): for m in r3.finditer(code): print (arifmetic.solve({k: int(v) for k, v in m.groupdict().items()})) return arifmetic.solve({k: int(v) for k, v in m.groupdict().items()}) while True: res = None code = sock.recv(102400) code = code.decode() code = "\n".join(code.split("\n")[:-2]) code = code.replace("sleep(0.1)", "") if "gone wrong" in code: print(code) exit(0) start = time() if "shuffle" in code: res = solve1(code) elif "OO0OOOO00OO0O0O0O" in code: res = solve2(code) elif "mas.append" in code: res = solve3(code) else: print("EXECING") print(code) exec(code) print(sock.recv(102400)) print(sock.recv(102400)) print(sock.recv(102400)) print(sock.recv(102400)) exit(0) print(time()-start) code = None res = str(res).encode() try: sock.send(res + b"\n") except Exception: print(sock.recv(102400)) print(sock.recv(102400)) break
В результате получаем долгожданный флаг!
Ответ: ctfzone{YouRealyHaveSoMuchTime?}
Кстати, по вашим многочисленным просьбам мы выложили оффлайн задания – теперь поиграться с райтапами можно на этом портале. Но не забывайте про наши задания по хайрингу, они будут доступны еще 10 дней до 15.12 – время еще есть!
Если у вас остались какие-то вопросы – оставляйте комментарии и пишите в наш чат в Telegram. Ничего так не вдохновляет, как ваша активность 🙂
Всем добра и удачи!
ссылка на оригинал статьи https://habrahabr.ru/post/316846/
Добавить комментарий