├── .gitignore ├── .nojekyll ├── Makefile ├── README.en.md ├── README.md ├── README.pt.md ├── challenges ├── a_segregated_new_world.en.md ├── a_segregated_new_world.json ├── a_segregated_new_world.pt.md ├── attack_step_first.en.md ├── attack_step_first.json ├── attack_step_first.pt.md ├── attack_step_second.en.md ├── attack_step_second.json ├── attack_step_second.pt.md ├── back_to_bletchley_park.en.md ├── back_to_bletchley_park.json ├── back_to_bletchley_park.pt.md ├── bavarian_hierarchy.en.md ├── bavarian_hierarchy.json ├── bavarian_hierarchy.pt.md ├── bavbin.en.md ├── bavbin.json ├── bavbin.pt.md ├── bergs_club.en.md ├── bergs_club.json ├── bergs_club.pt.md ├── emetacrazy.en.md ├── emetacrazy.json ├── emetacrazy.pt.md ├── g00d_b0y.en.md ├── g00d_b0y.json ├── g00d_b0y.pt.md ├── gcm.en.md ├── gcm.json ├── gcm.pt.md ├── index.json ├── inherit_the_stars.en.md ├── inherit_the_stars.json ├── inherit_the_stars.pt.md ├── mdfm_flag_verifier.en.md ├── mdfm_flag_verifier.json ├── mdfm_flag_verifier.pt.md ├── message_board_pt1.en.md ├── message_board_pt1.json ├── message_board_pt1.pt.md ├── message_board_pt2.en.md ├── message_board_pt2.json ├── message_board_pt2.pt.md ├── message_board_pt3.en.md ├── message_board_pt3.json ├── message_board_pt3.pt.md ├── message_board_pt4.en.md ├── message_board_pt4.json ├── message_board_pt4.pt.md ├── minishell.en.md ├── minishell.json ├── minishell.pt.md ├── noizy_maze.en.md ├── noizy_maze.json ├── noizy_maze.pt.md ├── recovery_the_master_key.en.md ├── recovery_the_master_key.json ├── recovery_the_master_key.pt.md ├── russian_polygons.en.md ├── russian_polygons.json ├── russian_polygons.pt.md ├── sgle.en.md ├── sgle.json ├── sgle.pt.md ├── sum.en.md ├── sum.json ├── sum.pt.md ├── super_storage.en.md ├── super_storage.json ├── super_storage.pt.md ├── too_slow.en.md ├── too_slow.json ├── too_slow.pt.md ├── tpm20.en.md ├── tpm20.json ├── tpm20.pt.md ├── trick_or_treat.en.md ├── trick_or_treat.json ├── trick_or_treat.pt.md ├── ultramegapolyglot.en.md ├── ultramegapolyglot.json ├── ultramegapolyglot.pt.md ├── wildcard.en.md ├── wildcard.json └── wildcard.pt.md ├── container-docker.en.md ├── container-docker.pt.md ├── container-lxc.en.md ├── container-lxc.pt.md ├── ctf ├── frontend ├── css │ └── styles.css ├── js │ ├── app.js │ ├── challenges.js │ ├── functions.js │ ├── home.js │ ├── rank.js │ ├── services.js │ ├── settings.js │ ├── story.js │ └── team.js └── locales │ ├── en.js │ └── pt.js ├── index.html ├── known_hosts ├── lambda_function.py ├── nizkctf ├── __init__.py ├── acceptedsubmissions.py ├── challenge.py ├── cli │ ├── __init__.py │ ├── challenges.py │ ├── localserver.py │ ├── log.py │ ├── news.py │ ├── scoreboard.py │ ├── team.py │ └── teamsecrets.py ├── gen_iso3166 ├── iso3166.py ├── localsettings.py ├── news.py ├── proof.py ├── proposal.py ├── repohost │ ├── __init__.py │ ├── common.py │ ├── github.py │ └── gitlab.py ├── scoring.py ├── serializable.py ├── settings.py ├── six.py ├── subrepo.py ├── team.py └── text.py ├── pip-requirements.txt ├── settings.json └── setup-vpn /.gitignore: -------------------------------------------------------------------------------- 1 | # our own stuff 2 | /submissions 3 | /local-settings.json 4 | /team-secrets.json 5 | 6 | # elm-make 7 | /elm-stuff 8 | 9 | # python 10 | *.pyc 11 | *.pyo 12 | 13 | # wingide 14 | *.wpr 15 | *.wpu 16 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwn2winctf/2018/5329e25e97bf834b5eadd0fb7858e654538b4f88/.nojekyll -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @echo nothing yet 3 | 4 | nizkctf/iso3166.py: nizkctf/gen_iso3166 settings.json 5 | PYTHONPATH="." $< >$@ 6 | 7 | ../lambda.zip: FORCE 8 | rm -f "$@" 9 | zip -ry "$@" * -x 'submissions/*' 10 | 11 | FORCE: 12 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # Pwn2Win CTF 2018 2 | 3 | About our NIZK (Non-Interactive Zero-Knowledge) Platform: [https://arxiv.org/pdf/1708.05844.pdf](https://arxiv.org/pdf/1708.05844.pdf) 4 | 5 | ## Registration 6 | 1. All team members must have a GitHub account and [configure a SSH key in their account settings](https://github.com/settings/keys). 7 | 8 | **Important Note**: If you are unable to follow the installation instructions below, ~~or is simply too lazy to do all the steps~~, we made a [LXD container preloaded with this platform](container-lxc.en.md). If you prefer Docker, we made a [Dockerfile](container-docker.en.md) too. If you want to install directly in your machine (instead of containers), just ignore this note. 9 | 10 | 2. All team members must clone the repository and install the dependencies: 11 | ```bash 12 | git clone git@github.com:pwn2winctf/2018.git 13 | cd 2018 14 | sudo apt-get install libsodium23 15 | curl https://bootstrap.pypa.io/get-pip.py | sudo -H python 16 | sudo -H python -m pip install -r pip-requirements.txt 17 | ``` 18 | 19 | Note: Any libsodium version >= libsodium18 is supported. However, recent pysodium has a bug when used together with old libsodium. Therefore, if you use libsodium18, please change `pip-requirements.txt` second line to `pysodium == 0.6.9.1` in order to use exactly the pysodium version which works correctly with libsodium18. 20 | 21 | 3. All team members must have the git client [correctly set up](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup). If you have never used git before, run: 22 | ```bash 23 | git config --global user.name "John Doe" 24 | git config --global user.email johndoe@example.com 25 | ``` 26 | 27 | 4. If dependencies are installed correctly (or if you used one of our preloaded containers), you should now see the help menu when calling: 28 | ```bash 29 | ./ctf -h 30 | ``` 31 | 32 | 5. The **leader of the team** must execute the following command and follow the instructions to register the team: 33 | ```bash 34 | ./ctf init 35 | ``` 36 | 37 | 6. The **other members of the team** must login to GitHub without registering a new team, by running: 38 | ```bash 39 | ./ctf login 40 | ``` 41 | 42 | 7. After that, **the leader** must share the `team-secrets.json` with the members of the team. The **other members of the team** must place the `team-secrets.json` file shared by the leader in their `2018` directory. 43 | 44 | ## Challenges 45 | 46 | Challenges are available on https://pwn2.win/2018. 47 | 48 | If you prefer to browse them locally, you may also run a local webserver by typing `./ctf serve`, or list challenges through the command line interface: 49 | ```bash 50 | ./ctf challs 51 | ``` 52 | 53 | ## Flag submission 54 | 55 | To submit a flag: 56 | ```bash 57 | ./ctf submit --chall chall-id 'CTF-BR{flag123}' 58 | ``` 59 | 60 | You may omit `--chall chall-id` from the command, however it will be slower to run this way. In this case, we will look for the flag in every challenge released until now. 61 | 62 | ## VPN 63 | 64 | To get the VPN credentials after your team has unlocked it (by solving at least 6 challenges - see the [rules](https://pwn2win.party/rules) page for more details): 65 | ```bash 66 | ./ctf news --pull 67 | ``` 68 | 69 | ## Scoreboard 70 | 71 | You can see the scoreboard in the game link (https://pwn2.win/2018), locally (if you ran the local webserver) or through the command line interface: 72 | ```bash 73 | ./ctf score --names --pull 74 | ``` 75 | 76 | ## Support 77 | 78 | You may reach us through #pwn2win at irc.freenode.net. 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | README.en.md -------------------------------------------------------------------------------- /README.pt.md: -------------------------------------------------------------------------------- 1 | # Pwn2Win CTF 2018 2 | 3 | Sobre a nossa NIZK (Non-Interactive Zero-Knowledge) Platform: https://arxiv.org/pdf/1708.05844.pdf 4 | 5 | ## Registro 6 | 1. Todos os membros do time devem ter uma conta no GitHub e [configurar uma chave SSH nas suas configurações de conta](https://github.com/settings/keys). 7 | 8 | **Nota**: Se você não conseguir seguir as instruções de instalação abaixo, ~~ou simplesmente é preguiçoso demais para fazer os passos~~, preparamos para você um [contêiner LXD com a plataforma pré-instalada](container-lxc.pt.md). Se você prefere Docker, disponibilizamos também um [Dockerfile](container-docker.pt.md). Caso queira instalar diretamente na sua máquina (sem contêineres), ignore esta nota. 9 | 10 | 2. Todos os membros do time devem clonar o repositório e instalar as dependências: 11 | ```bash 12 | git clone git@github.com:pwn2winctf/2018.git 13 | cd 2018 14 | sudo apt-get install libsodium18 15 | curl https://bootstrap.pypa.io/get-pip.py | sudo -H python 16 | sudo -H python -m pip install -r pip-requirements.txt 17 | ``` 18 | 19 | Nota: Qualquer versão do libsodium >= libsodium18 é suportada. No entanto, versões recentes do pysodium têm um bug quando usadas com a libsodium antiga. Então, se você usar libsodium18, por favor mude a segunda linha do `pip-requirements.txt` para `pysodium == 0.6.9.1`, de forma a usar exatamente a versão do pysodium que funciona corretamente com a libsodium18. 20 | 21 | 3. Todos os membros do time devem ter um cliente git [corretamente configurado](https://git-scm.com/book/pt-br/v2/Começando-Configuração-Inicial-do-Git). Se você nunca usou git antes, execute: 22 | ```bash 23 | git config --global user.name "Fulano de Tal" 24 | git config --global user.email fulanodetal@exemplo.com.br 25 | ``` 26 | 27 | 4. Se as dependências estiverem corretamente instaladas (ou se você usou uma das nossas imagens), você deve conseguir ver o menu de ajuda executando: 28 | ```bash 29 | ./ctf -h 30 | ``` 31 | 32 | 5. **O líder do time** deve executar o seguinte comando e seguir as instruções para registrar a equipe: 33 | ```bash 34 | ./ctf init 35 | ``` 36 | 37 | 6. **Os demais membros** devem se logar com o GitHub sem criar um novo time: 38 | ```bash 39 | ./ctf login 40 | ``` 41 | 42 | 7. Após isso, **o líder** deve compartilhar o arquivo `team-secrets.json` com os demais mebros. **Os demais membros** devem colocar o arquivo `team-secrets.json` na pasta `2018` clonada. 43 | 44 | ## Challenges 45 | 46 | Os challenges ficarão disponíveis em [https://pwn2.win/2018](https://pwn2.win/2018). 47 | 48 | Se você preferir, pode consultar localmente subindo um servidor usando `./ctf serve`, ou listar os challenges na Interface de Linha de Comando: 49 | ```bash 50 | ./ctf challs 51 | ``` 52 | 53 | ## Submissão de flags 54 | 55 | Para submeter uma flag: 56 | ```bash 57 | ./ctf submit --chall chall-id 'CTF-BR{flag123}' 58 | ``` 59 | 60 | Você pode omitir o `--chall chall-id` do comando, mas vai demorar mais para submeter. Nesse caso, será tentada a flag para cada um dos challenges liberados até então. 61 | 62 | ## VPN 63 | 64 | Para pegar as credenciais da VPN, quando seu time desbloqueá-la, após resolver 6 challenges (veja a página de [regras](https://pwn2win.party/rules) pra entender melhor): 65 | ```bash 66 | ./ctf news --pull 67 | ``` 68 | 69 | ## Placar 70 | 71 | Voc pode ver o scoreboard no link do game ([https://pwn2.win/2018](https://pwn2.win/2018)), localmente (se você rodar o webserver) ou através da CLI: 72 | ```bash 73 | ./ctf score --names --pull 74 | ``` 75 | 76 | ## Suporte 77 | 78 | Podemos tentar ajudá-lo via IRC: #pwn2win @ freenode. 79 | -------------------------------------------------------------------------------- /challenges/a_segregated_new_world.en.md: -------------------------------------------------------------------------------- 1 | 2 | This letter is being broadcasted to all hackers in the world, from the group **Rebellious Fingers**. Read it with care, the situation is chaotic, and we need your help! 3 | 4 | In 2027, what we now call "**The Order**", also known as "**The Bavarian**", came to be. They're a select group of men working as a hidden branch of the OCU (United Countries Organization). The "**bavs**", as their members are called, are high-ranked officers of the government in developed countries, leaders of multinational companies in many fields of activity, like food, automotive and pharmaceutic industry, scientists and hackers. They usually gather annually in a place called **Berg's Club**, but we haven't found its location yet. 5 | 6 | We know one of its leaders is called Wilson, a person that has become extremely obsessed with their projects, and he plans on making use of the "bavs" to carry out two of his greatest objectives, the implantation of a globally unified government, and the reduction of the global population to an order of around 1 billion people, before the end of this century. In order to reach the second objective, they are creating a virus called **H1N337**, projected to sterilize 80% of the population in the world, making the birth rate plummet, in relation to the mortality rate. Another person in charge is known only as Mr. Deadcow, but we don't have his name. We suspect he uses this name because of his involvement with the creation of the infectious agent of the Mad Cow disease, while working for the american government in the 80s, and moving on to the United Kingdom as his "testing laboratory". 7 | 8 | We have found the propagation of the H1N337 will happen in basically 3 ways: 9 | 10 | **1** - through food, from companies like Kraftsico, present in various countries; 11 | 12 | **2** - through air, from exhausting pipes of some brands like Wolkslet containing the virus, which will be released in a specific moment, according to their programming; 13 | 14 | **3** - through drugs, from many pharmaceutical companies like Valeanton (famous for heavily overpricing medicine in a global scale). 15 | 16 | This entire operation was planned to be very stealthy! The engineers of Wolkslet, for one, created an "electronic cheating device", called "toxiclean", which is extremely effective in avoiding the tests of air pollutant emissions, and does not release the virus when it detects it is inside a test environment. This virus is strongly resistant to heat, so the high temperatures of exhausting pipes does no harm to its structure at the time of release. 17 | 18 | We must be very careful with our identities, as the method of work of The Bavarian is extremely sadistic, with murder and tortures being present whenever they can (they are the shoot first, ask questions later kind of people). They have access to national healthcare bases in many different countries, and are able to change people's medical records to cover their murders, making them look like suicide or illness. They can also access general SmarTVs and IoT devices like routers, through backdoors, in order to sniff traffic, or public cameras of various brands in different countries, such as the street ones, for spionage. Using advanced facial recognition technologies in such cameras they can find their opponents almost anywhere. They can also track people by other means, such as: 19 | 20 | **1** - gathering bank transactions (including ATMs); 21 | 22 | **2** - through telephone networks; 23 | 24 | **3** - through apparently "safe" message applications, with end-to-end cryptography. 25 | 26 | For the global government plan, the first step taken will be to elect their candidates in the main countries where the "bavs" haven't taken control yet. For this task, they will fraud the elections using the electronic voting machines made by the Dieitalic company, which are not auditable. The idea of adopting an unique and centralized criptocurrency is also being worked on, and they're going to call it globalcoin. 27 | 28 | A model of digital lock is being produced by a company of the Order called RayHeed, and their ambition is to begin installing it in the houses of every country already controlled by "bavs", by using some artificial gadget to induce civilians into doing it. We still don't know their objective in doing so, but we'll try to infiltrate their next annual meeting at Berg's Club, after we find out its location, as matters of this kind aren't discussed through e-mails or other means of communication we can try to get access to. Between all 7 levels of security in which their files, conversations and other intel is categorized, this one is ranked the highest, called SS7. 29 | 30 | That being said, we have the complex mission of stopping the most structured and dangerous group we have ever come across, reply with ```CTF-BR{I_know_about_th3_r1sks!}```, if you're willing to put your life on the line to give the human race a chance to keep on existing by joining the Rebellious Fingers! 31 | 32 | 33 | -------------------------------------------------------------------------------- /challenges/a_segregated_new_world.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "A Segregated New World [Read first]", "tags": ["Story"], "pk": "TYm6RGsWMizbH8TcmwVqYupXjzdHJG8Cp0FXR7VvgpA=", "salt": "T4iZvpPHFix+U6LA907J5btbtb1h5jE7u1a08KLb4tE=", "id": "a_segregated_new_world"} 2 | -------------------------------------------------------------------------------- /challenges/a_segregated_new_world.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Essa carta está sendo enviada para todos os hackers do mundo, via broadcast, pelo grupo **Rebellious Fingers**. Leia com atenção, a situação é caótica e precisamos de sua ajuda! 3 | 4 | Em 2027, foi fundado o que chamamos de **"A Ordem"**, também conhecidos como **"The Bavarian"**, um grupo seleto de homens operando como uma ramificação oculta da OPU (Organização dos Países Unidos). Os **"bavs"**, como são conhecidos seus membros, são pessoas do alto escalão do governo de países desenvolvidos e de empresas multinacionais de diversos ramos, como alimentício, automobilístico e farmacêutico, além de cientistas e hackers. Costumam se reunir anualmente em um local chamado **Berg's Club**, cujo não sabemos a localização, ainda. 5 | 6 | Sabemos que um de seus líderes se chama Wilson, uma pessoa que se tornou extremamente obcecada por seus planos, e ele pretende utilizar os "bavs" para realizar dois de seus grandes objetivos, que são a implantação de um governo global unificado, e a redução da população mundial para a ordem de 1 bilhão de pessoas, até o fim do século. Para alcançar o segundo objetivo, estão criando um vírus chamado **H1N337**, que foi projetado para esterilizar 80% da população mundial, fazendo com que a taxa de natalidade caia bruscamente, em relação à de mortalidade. Outra pessoa que está no comando, responde pelo codinome de Sr. deadcow, mas não sabemos seu nome. Suspeita-se que use esse nome pois esteve envolvido na criação do agente infeccioso da doença da vaca louca, quando trabalhou para o governo americano, na década de 80, usando posteriormente o Reino Unido como "laboratório de testes". 7 | 8 | Descobrimos que a propagação do H1N337 se dará basicamente de 3 formas: 9 | 10 | **1** - por alimentos, através de empresas como a Kraftsico, que está presente em diversos países; 11 | 12 | **2** - pelo ar, com o vírus indo dentro de canos de escapamentos de algumas marcas, como a Wolkslet, e sendo liberado no momento certo, conforme sua programação; 13 | 14 | **3** - através de medicamentos de diversas farmacêuticas, como a Valeanton (famosa por manipular abusivamente o preço de medicamentos em escala global). 15 | 16 | A operação como um todo foi projetada por eles pra ser muito stealth! Os engenheiros da Wolkslet, por exemplo, criaram um "dispositivo eletrônico de trapaça", chamado "toxiclean", que é extremamente eficaz, burlando os testes de emissões de poluentes, e não liberando o vírus quando detecta estar em ambientes de testes. Esse vírus é resistente ao calor, então a alta temperatura do escapamento não prejudica sua estrutura na hora da propagação. 17 | 18 | Temos que ter muita cautela em relação às nossas identidades, pois o método de trabalho do The Bavarian é extremamente sádico, envolvendo assassinatos e torturas em todos que acharem necessário (é o tipo de gente que primeiro atira, depois pergunta). Eles têm acesso às bases nacionais de saúde de vários países, e conseguem modificar o histórico clínico das pessoas, pra respaldar homicídios, se passando por suicídio ou outras doenças. Através de backdoors, têm acesso a SmarTVs e dispositivos IoT em geral, como roteadores, para sniffar o tráfego a critério de espionagem, e câmeras, de diversas marcas em diversos países (inclusive as de vias públicas), e com uma avançada tecnologia de reconhecimento facial, conseguem achar seus desafetos em quase qualquer lugar. Também conseguem localizar pessoas de outras formas, como: 19 | 20 | **1** - monitorando transações bancárias (isso inclui ATMs); 21 | 22 | **2** - através das redes de telefonia; 23 | 24 | **3** - por meio de aplicativos de mensagens ditos "seguros", com criptografia "end-to-end". 25 | 26 | Para a ideia de governo global, o primeiro passo que eles pretendem concretizar, é eleger seus candidatos nos principais países que ainda não possuem controle através de "bavs". Para isso, vão fraudar eleições por meio de urnas eletrônicas da empresa Dieitalic, que não são auditáveis. A ideia da adoção de uma criptomoeda única e centralizada também está sendo desenvolvida por eles, e se chamará globalcoin. 27 | 28 | Um modelo de fechadura digital está sendo produzido por uma empresa da Ordem chamada RayHeed, e a ambição deles é começar a implantá-la nas casas de todos os países que já possuem "bavs" no comando, utilizando algum artifício artificialmente gerado para induzir os civis a tal. Ainda não sabemos muito bem qual o objetivo disso, mas tentaremos nos infiltrar na próxima reunião anual no Berg's Club, após descobrirmos sua localização, pois esse tipo de assunto eles não tratam por e-mail ou outros meios de comunicação que possamos tentar conseguir acesso. Entre os 7 níveis de segurança com que são categorizados seus documentos, conversas e demais informações, esse possui o mais alto, chamado SS7. 29 | 30 | Dito isso, temos a complexa missão de deter o grupo mais bem estruturado e perigoso que já enfretamos, responda com ```CTF-BR{I_know_about_th3_r1sks!} ```, se está disposto a arriscar sua vida para ajudar à continuidade da raça humana juntando-se ao Rebellious Fingers! 31 | 32 | -------------------------------------------------------------------------------- /challenges/attack_step_first.en.md: -------------------------------------------------------------------------------- 1 | 2 | We have gotten information that The Bavarian have their own authentication system, which allows through their firewall only people that manage to pass their sophisticated handshake, leading to a private News System of the group. There are also rumors that they manage their botnet through a control panel located in their internal network, and that they have control of a very important Russian computer. We must try to access this machine/victim, directly in the currently logged user's screen. Help us in this long and complex mission! 3 | 4 | We figured out there is a *Control Packet* that is sent for a random port. It's fundamental to understand that handshake. 5 | 6 | **This challenge's steps include:** 7 | 8 | Networking -> Networking[**1st flag here**] -> Networking -> Web -> Pwnage[**2nd flag here**] 9 | 10 | Submit here the **first** flag! 11 | 12 | **Server:** nc 200.136.252.32 9449 13 | -------------------------------------------------------------------------------- /challenges/attack_step_first.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Attack Step [First Flag]", "tags": ["Attack Step"], "pk": "qKYsnAQlermr+HQINetA8UORh6BseIq3UCSW5X98G4c=", "salt": "03l5IW21Id9N1gD6fp0LxqrZLjXZah+b7J6ssqs5334=", "id": "attack_step_first"} 2 | -------------------------------------------------------------------------------- /challenges/attack_step_first.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Descobrimos que o "The Bavarian" tem um sistema próprio de autenticação, que libera seu firewall somente para pessoas autorizadas que passem do seu sofisticado *handshake*, permitindo assim acesso a um Sistema de News privado do grupo. Há boatos que eles possuem um painel de gerenciamento de uma *botnet* em sua rede interna, e estão controlando um computador Russo muito importante. Precisamos tentar conseguir acesso a essa máquina/vítima, diretamente na tela do usuário que está logado. Nos ajude nessa longa e complexa missão! 3 | 4 | Nós descobrimos por aí que há um *Pacote de Controle* enviado para uma porta randômica. É fundamental entender o handshake. 5 | 6 | **Essa etapa envolve**: 7 | 8 | Networking -> Networking [**1st flag here**] -> Networking -> Web -> Pwnage [**2nd flag here**] 9 | 10 | Submeta a **primeira** flag aqui! 11 | 12 | **Server:** nc 200.136.252.32 9449 13 | -------------------------------------------------------------------------------- /challenges/attack_step_second.en.md: -------------------------------------------------------------------------------- 1 | 2 | We have gotten information that The Bavarian have their own authentication system, which allows through their firewall only people that manage to pass their sophisticated handshake, leading to a private News System of the group. There is also word that they manage their botnet through a control panel located in their internal network, and that they have control of a very important Russian computer. We must try to access this machine/victim, directly in the currently logged user's screen. Help us in this long and complex mission! 3 | 4 | We figured out there is a *Control Packet* that is sent for a random port. It's fundamental to understand that handshake. 5 | 6 | **This challenge's steps include:** 7 | 8 | Networking -> Networking[**1st flag here**] -> Networking -> Web -> Pwnage[**2nd flag here**] 9 | 10 | Submit here the **second** flag! 11 | 12 | **Server:** nc 200.136.252.32 9449 13 | -------------------------------------------------------------------------------- /challenges/attack_step_second.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Attack Step [Second Flag]", "tags": ["Attack Step"], "pk": "CcfRDhBig+jvrgAlTqAbmjbWc9d2WWAgEVitmmlnH/w=", "salt": "jm+SOaYwr0S+UgYjLY5zAWr5E9+OxkUU1o7YHwx7ktI=", "id": "attack_step_second"} 2 | -------------------------------------------------------------------------------- /challenges/attack_step_second.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Descobrimos que o "The Bavarian" tem um sistema próprio de autenticação, que libera seu firewall somente para pessoas autorizadas que passem do seu sofisticado *handshake*, permitindo assim acesso a um Sistema de News privado do grupo. Há boatos que eles possuem um painel de gerenciamento de uma *botnet* em sua rede interna, e estão controlando um computador Russo muito importante. Precisamos tentar conseguir acesso a essa máquina/vítima, diretamente na tela do usuário que está logado. Nos ajude nessa longa e complexa missão! 3 | 4 | Nós descobrimos por aí que há um *Pacote de Controle* enviado para uma porta randômica. É fundamental entender o handshake. 5 | 6 | **Essa etapa envolve**: 7 | 8 | Networking -> Networking [**1st flag here**] -> Networking -> Web -> Pwnage [**2nd flag here**] 9 | 10 | Submeta a **segunda** flag aqui! 11 | 12 | **Server:** nc 200.136.252.32 9449 13 | -------------------------------------------------------------------------------- /challenges/back_to_bletchley_park.en.md: -------------------------------------------------------------------------------- 1 | 2 | We have gone quantum. A collaborator of ours built this **quantum circuit** to compute the factorization of a Bavs RSA key and took note of the results. They are now stored in her server ([https://qc.pwn2.win](https://qc.pwn2.win)) inside the `/a/N` path, where `a` is the group generator and `N` is the modulus. Unfortunately we could not get in touch with her lately, so we need your help understanding what she did. Once you figure out how the circuit works and discover what are the values of `a` and `N`, we can see the results from her calculations and use them to decrypt the message. 3 | 4 | Choose a version of the challenge below (we will accept any of them). 5 | 6 | **640 bit version** 7 | 8 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/back-to-bletchley-park_e27c9e079019f76ac913fa01933fc65147e1c06adf49d70b8c3cc868593eadbb.tar.gz) 9 | 10 | [Mirror](https://static.pwn2win.party/back-to-bletchley-park_e27c9e079019f76ac913fa01933fc65147e1c06adf49d70b8c3cc868593eadbb.tar.gz) 11 | 12 | **1024 bit version** 13 | 14 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/back-to-bletchley-park_b349f5d19905bcdf8f4abd01f321f2e05adf0979dcf3b435465deaabbc913dec.tar.xz) 15 | 16 | [Mirror](https://static.pwn2win.party/back-to-bletchley-park_b349f5d19905bcdf8f4abd01f321f2e05adf0979dcf3b435465deaabbc913dec.tar.xz) 17 | 18 | **Note**: We know that the message was originally encrypted with the following commands: 19 | 20 | ``` 21 | openssl rsautl -encrypt -oaep -pubin -inkey public.pem -in aes256.key -out aes256.key.enc 22 | openssl aes-256-cbc -base64 -in secret_message.txt -out secret_message.enc -k $(cat aes256.key) 23 | ``` 24 | -------------------------------------------------------------------------------- /challenges/back_to_bletchley_park.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Back to Bletchley Park", "tags": ["Cryptography", "Reversing"], "pk": "v/IHkvf2uuNYn9D/mS9BVT4+d2XHQsYz3EQFT8FqCmA=", "salt": "Wjs0lKzOnTrnTUGPKn8XLcjv276AELi6lf5dc3ZTFBg=", "id": "back_to_bletchley_park"} 2 | -------------------------------------------------------------------------------- /challenges/back_to_bletchley_park.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Agora temos computadores quânticos. Uma colaboradora nossa construiu este circuito quântico para calcular a fatoração de uma chave RSA dos Bavs 3 | e anotou os resultados. Eles estão no servidor dela ([https://qc.pwn2.win](https://qc.pwn2.win)) no caminho `/a/N`, onde `a` é o gerador do grupo e `N` é o módulo. Infelizmente não conseguimos falar com ela nos últimos dias, então precisamos de sua ajuda para entender o que ela fez. Quando você descobrir como o circuito funciona e os valores de `a` e `N`, poderemos encontrar os seus resultados e usá-los para decifrar a mensagem. 4 | 5 | Escolha uma versão do desafio abaixo (aceitaremos qualquer uma delas). 6 | 7 | **versão de 640 bits** 8 | 9 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/back-to-bletchley-park_e27c9e079019f76ac913fa01933fc65147e1c06adf49d70b8c3cc868593eadbb.tar.gz) 10 | 11 | [Mirror](https://static.pwn2win.party/back-to-bletchley-park_e27c9e079019f76ac913fa01933fc65147e1c06adf49d70b8c3cc868593eadbb.tar.gz) 12 | 13 | **versão de 1024 bits** 14 | 15 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/back-to-bletchley-park_b349f5d19905bcdf8f4abd01f321f2e05adf0979dcf3b435465deaabbc913dec.tar.xz) 16 | 17 | [Mirror](https://static.pwn2win.party/back-to-bletchley-park_b349f5d19905bcdf8f4abd01f321f2e05adf0979dcf3b435465deaabbc913dec.tar.xz) 18 | 19 | **Nota**: Nós sabemos que a mensagem foi originalmente cifrada com os seguintes comandos: 20 | 21 | ``` 22 | openssl rsautl -encrypt -oaep -pubin -inkey public.pem -in aes256.key -out aes256.key.enc 23 | openssl aes-256-cbc -base64 -in secret_message.txt -out secret_message.enc -k $(cat aes256.key) 24 | ``` 25 | -------------------------------------------------------------------------------- /challenges/bavarian_hierarchy.en.md: -------------------------------------------------------------------------------- 1 | 2 | Rebellious Fingers hackers got a file that showed the bavs hierarchy that connected two certain companies. So they decided to monitor everyone who linked these two companies. Given a person's hierarchy related to these companies and the IDs of an employee of each company, they want to know how many people at least need to be monitored so that all connections between the companies are monitored. A person connects two companies if he/she is boss (directly or indirectly) of members of both companies simultaneously. 3 | 4 | The input begins with a line with two integers N and M, splited by one space. The next N lines are composed of two strings A and B, showing that A is a direct boss of B. For each of the next M lines, two strings are given indicating the aforementioned employees. For each of these M lines, there must be an output line, indicating at least how many people must be monitored in this situation. 5 | 6 | **Example:** 7 | 8 | input: 9 | 10 | 5 2 11 | 12 | 5 1 13 | 14 | 1 3 15 | 16 | 5 6 17 | 18 | 6 4 19 | 20 | 4 2 21 | 22 | 2 3 23 | 24 | 3 2 25 | 26 | output: 27 | 28 | 1 29 | 30 | 1 31 | 32 | **Server:** openssl s_client -connect programming.pwn2.win:9003 33 | 34 | **Connection template:** 35 | 36 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/the-bavarian-hierarchy-template_7684169bed405726993790ac237e21e67d71c532fc1c81005332f605b2de4548.tar.gz) 37 | 38 | [Mirror](https://static.pwn2win.party/the-bavarian-hierarchy-template_7684169bed405726993790ac237e21e67d71c532fc1c81005332f605b2de4548.tar.gz) 39 | -------------------------------------------------------------------------------- /challenges/bavarian_hierarchy.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "The Bavarian Hierarchy", "tags": ["PPC-M"], "pk": "/JPq+JcIUvIzj3ZlruMzFiSV4XG8Bykgy5h9nbvNswc=", "salt": "nY3YKFbOGzlT88CwaufZZ1ttjicNXHP7Hw20MxLppKM=", "id": "bavarian_hierarchy", "optional": "platform"} 2 | -------------------------------------------------------------------------------- /challenges/bavarian_hierarchy.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Hackers do Rebellious Fingers interceptaram um arquivo que mostrava a hierarquia de bavs que ligavam duas determinadas empresas. Com isso, decidiram monitorar todos os indivíduos que ligavam essas duas empresas. Dada uma hierarquia de pessoas nas empresas e as identificações de um funcionário de cada uma dessas empresas, querem saber quantas pessoas precisam ser monitoradas no mínimo para toda ligação entre as duas empresa estejam monitoradas. Uma pessoa liga as duas empresas se é superior (direta ou indiretamente) a membros das duas empresas simultaneamente. 3 | 4 | A entrada começa com uma linha contendo dois inteiros N e M, separados por um espaço. As N linhas seguintes contém cada uma duas strings A e B, indicando que A é superior direto de B. Para cada uma das M linhas seguintes, são dados duas strings indicando os funcionários citados. Para cada uma dessas M linhas, deve haver uma linha de saída, indicando quantas pessoas no mínimo devem ser monitoradas nessa situação. 5 | 6 | **Exemplo:** 7 | 8 | input: 9 | 10 | 5 2 11 | 12 | 5 1 13 | 14 | 1 3 15 | 16 | 5 6 17 | 18 | 6 4 19 | 20 | 4 2 21 | 22 | 2 3 23 | 24 | 3 2 25 | 26 | output: 27 | 28 | 1 29 | 30 | 1 31 | 32 | **Server:** openssl s_client -connect programming.pwn2.win:9003 33 | 34 | **Connection template:** 35 | 36 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/the-bavarian-hierarchy-template_7684169bed405726993790ac237e21e67d71c532fc1c81005332f605b2de4548.tar.gz) 37 | 38 | [Mirror](https://static.pwn2win.party/the-bavarian-hierarchy-template_7684169bed405726993790ac237e21e67d71c532fc1c81005332f605b2de4548.tar.gz) 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /challenges/bavbin.en.md: -------------------------------------------------------------------------------- 1 | 2 | An informant infiltrated into The Bavarians got his hands on information about an in-house service that is being used by its members to exchange private texts. He also informed us that recently the admin used the service to exchange confidential information regarding their next operation. 3 | 4 | We need you to recover this message so we can stop them before it's too late. 5 | 6 | **Server**: [http://200.136.252.39](http://200.136.252.39) 7 | 8 | **Automated tools are NOT required and NOT allowed, it's a technical challenge!** 9 | -------------------------------------------------------------------------------- /challenges/bavbin.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "BavBin", "tags": ["Web"], "pk": "pf7mV7DEONhPsKrU3j8k9N5F+rvbeoq+o/gAxo4fUjg=", "salt": "oC72LOA0EjH8qKGUNqAf0U8Zt2Zw1yF7cBqoe9iT8RU=", "id": "bavbin"} -------------------------------------------------------------------------------- /challenges/bavbin.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Um informante infiltrado nos bavs conseguiu acesso à informações sobre um serviço interno que está sendo usado por seus membros para trocar mensagens privadas. Ele também nos informou que recentemente o administrador usou o serviço para trocar informações confidenciais sobre sua próxima operação. 3 | 4 | Precisamos que você recupere esta mensagem para que possamos detê-los antes que seja tarde demais. 5 | 6 | **Server**: [http://200.136.252.39](http://200.136.252.39) 7 | 8 | **Tools automatizadas NÃO são necessárias e NÃO são permitidas, esse é um challenge técnico!** 9 | -------------------------------------------------------------------------------- /challenges/bergs_club.en.md: -------------------------------------------------------------------------------- 1 | 2 | Hey, Rebellious. 3 | 4 | We just created a webapp to let you inform us where is the Berg's Club location. Oh, and we only accept JPG for now, sorry. Any image is vital for us! 5 | 6 | [http://200.136.252.42/](http://200.136.252.42/) 7 | 8 | **Automated tools are NOT required and NOT allowed, it's a technical challenge!** 9 | -------------------------------------------------------------------------------- /challenges/bergs_club.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Berg\u2019s Club", "tags": ["Web"], "pk": "wNJVo353NJJ69/hfuDH8Mq9hBw2JK/HBgBbmkiOhKzg=", "salt": "RY6TLw0qqo0+L/2VsjID3cpM/LjT7GenkzRit7paOiA=", "id": "bergs_club"} -------------------------------------------------------------------------------- /challenges/bergs_club.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Hey, Rebellious! 3 | 4 | Nós criamos um webapp para deixar você nos informar onde é a localização do Berg's Club. Oh, estamos apenas aceitamos JPG, por enquanto. Qualquer image será vital para nós! 5 | 6 | [http://200.136.252.42/](http://200.136.252.42/) 7 | 8 | **Tools automatizadas NÃO são permitidas e NÃO necessárias, esse é um challenge técnico!** 9 | -------------------------------------------------------------------------------- /challenges/emetacrazy.en.md: -------------------------------------------------------------------------------- 1 | 2 | One of our "The Order" infiltrated hackers managed to implant a backdoor in one of the "bavs" computer. 3 | 4 | However, aparently his operational system has extra security layers that are not allowing our hackers to extract sensitive information from this computer. 5 | 6 | Your mission, if you decide to accept it, is to extract the information of "confidential.txt" file from this computer. 7 | 8 | **Server:** nc emetacrazy.southcentralus.cloudapp.azure.com 8080 9 | 10 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/EMETaCrazy_11fc688acf9d057cf626f86cd5cc128346d17ca39f8be1ef935a62fd4d013c44.tar.gz) 11 | 12 | [Mirror](https://static.pwn2win.party/EMETaCrazy_11fc688acf9d057cf626f86cd5cc128346d17ca39f8be1ef935a62fd4d013c44.tar.gz) 13 | -------------------------------------------------------------------------------- /challenges/emetacrazy.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "EMETaCrazy", "tags": ["Exploitation"], "pk": "8sWoEnkQ8epe6qzxQ4LRXpQv5NSZNAGqQJXTiGCCGVI=", "salt": "Uyggtm4/CVU0JK61IR2OSDAJiCetDfceQp7PEOCTWa0=", "id": "emetacrazy"} -------------------------------------------------------------------------------- /challenges/emetacrazy.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Um dos nossos hackers infiltrados na "A Ordem" conseguiu implantar uma backdoor no computador de um dos "bavs". 3 | 4 | Porém aparentemente o sistema operacional dele está com camadas extras de segurança que não estão permitindo que nossos hackers extraiam as informações sigilosas desse computador. 5 | 6 | Sua missão, se decidiar aceitá-la, é extrair as informações do arquivo "confidential.txt" desse computador. 7 | 8 | **Server:** nc emetacrazy.southcentralus.cloudapp.azure.com 8080 9 | 10 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/EMETaCrazy_11fc688acf9d057cf626f86cd5cc128346d17ca39f8be1ef935a62fd4d013c44.tar.gz) 11 | 12 | [Mirror](https://static.pwn2win.party/EMETaCrazy_11fc688acf9d057cf626f86cd5cc128346d17ca39f8be1ef935a62fd4d013c44.tar.gz) 13 | -------------------------------------------------------------------------------- /challenges/g00d_b0y.en.md: -------------------------------------------------------------------------------- 1 | 2 | Now prove you were a good kid and show you learned the most basic lesson in CTFs!! 3 | -------------------------------------------------------------------------------- /challenges/g00d_b0y.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "g00d b0y", "tags": ["Bonus"], "pk": "EgD6l/zyjyLq7T6WwR07FhOmTcugkQXGp58gZFnvUfg=", "salt": "q09SAEI6p27uekmQFsfsXaadD1etrN8bw28wwISb91Q=", "id": "g00d_b0y"} -------------------------------------------------------------------------------- /challenges/g00d_b0y.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Dessa vez, prove que você foi um bom garoto e aprendeu a lição básica dos CTFs!! 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /challenges/gcm.en.md: -------------------------------------------------------------------------------- 1 | 2 | There is only one method of performing bank transactions that can not be monitored by The Bavarian. It's not an ATM, but a secret system. 3 | 4 | **Server:** nc 200.136.252.51 5555 5 | 6 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/gcm_878047832c354fb1ad1b40c8fdab9c63f363314c45571fd84e4cfd9bf9dfeb25.tar.gz) 7 | 8 | [Mirror](https://static.pwn2win.party/gcm_878047832c354fb1ad1b40c8fdab9c63f363314c45571fd84e4cfd9bf9dfeb25.tar.gz) 9 | -------------------------------------------------------------------------------- /challenges/gcm.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "GCM", "tags": ["Cryptography"], "pk": "Eaqa6XmLDVJIblAlC+Z6JM7EyQdYX2fPgQi47N7QCwM=", "salt": "Yas3ayZTXt7qnL2cgCqKcWY4uAxZt89F+ai5HUNun7U=", "id": "gcm"} -------------------------------------------------------------------------------- /challenges/gcm.pt.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Há apenas um método para fazer transações bancárias que não podem ser monitorados pelo The Bavarian. Isso não é um ATM, mas um sistema secreto. 4 | 5 | **Server:** nc 200.136.252.51 5555 6 | 7 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/gcm_878047832c354fb1ad1b40c8fdab9c63f363314c45571fd84e4cfd9bf9dfeb25.tar.gz) 8 | 9 | [Mirror](https://static.pwn2win.party/gcm_878047832c354fb1ad1b40c8fdab9c63f363314c45571fd84e4cfd9bf9dfeb25.tar.gz) 10 | -------------------------------------------------------------------------------- /challenges/index.json: -------------------------------------------------------------------------------- 1 | ["a_segregated_new_world", "attack_step_first", "attack_step_second", "back_to_bletchley_park", "bavarian_hierarchy", "bavbin", "bergs_club", "emetacrazy", "g00d_b0y", "gcm", "inherit_the_stars", "mdfm_flag_verifier", "message_board_pt1", "message_board_pt2", "message_board_pt3", "message_board_pt4", "minishell", "noizy_maze", "recovery_the_master_key", "russian_polygons", "sgle", "sum", "super_storage", "too_slow", "tpm20", "trick_or_treat", "ultramegapolyglot", "wildcard"] 2 | -------------------------------------------------------------------------------- /challenges/inherit_the_stars.en.md: -------------------------------------------------------------------------------- 1 | 2 | The binary yields the flag as an HTTP answer (port 8000) to a specific hidden endpoint when executed in the correct set of nodes belonging to a 10.0.0.0/8 network. Not every node the binary tries to contact should be online: we expect some of the nodes to be offline. If you run the binary in more or less nodes than the exact set of nodes expected, it will answer with random garbage. The user must request the flag only from the main node. 3 | 4 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/inherit-the-stars_29e2203521cd788c1e467497b7af81bdea3cfe495ccb297e50a863a650c81b93.tar.gz) 5 | 6 | [Mirror](https://static.pwn2win.party/inherit-the-stars_29e2203521cd788c1e467497b7af81bdea3cfe495ccb297e50a863a650c81b93.tar.gz) 7 | 8 | [MIPS Toolchain](https://downloads.openwrt.org/releases/17.01.6/targets/ar71xx/generic/lede-sdk-17.01.6-ar71xx-generic_gcc-5.4.0_musl-1.1.16.Linux-x86_64.tar.xz) 9 | -------------------------------------------------------------------------------- /challenges/inherit_the_stars.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Inherit the Stars", "tags": ["Reversing"], "pk": "RBzaOE8A+7hC6/Ihakevt1uWimtIxPg92VAcumpM/E8=", "salt": "6dkjRHJfXgOTPaKZS/wk5dhsYhfx/kX4LFwZN8Rc33A=", "id": "inherit_the_stars"} -------------------------------------------------------------------------------- /challenges/inherit_the_stars.pt.md: -------------------------------------------------------------------------------- 1 | 2 | O binário fornece a flag como resposta HTTP (porta 8000) a um certo endpoint oculto quando é executado no conjunto correto de nós pertencentes a uma rede 10.0.0.0/8. Nem todos os nós que o binário tenta conectar devem estar de pé: esperamos que alguns dos nós estejam fora do ar. Se você executar o binário em um único nó a mais ou em um único nó a menos do que é esperado, ele responderá com lixo aleatório. O usuário deve solicitar a flag apenas ao nó principal. 3 | 4 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/inherit-the-stars_29e2203521cd788c1e467497b7af81bdea3cfe495ccb297e50a863a650c81b93.tar.gz) 5 | 6 | [Mirror](https://static.pwn2win.party/inherit-the-stars_29e2203521cd788c1e467497b7af81bdea3cfe495ccb297e50a863a650c81b93.tar.gz) 7 | 8 | [MIPS Toolchain](https://downloads.openwrt.org/releases/17.01.6/targets/ar71xx/generic/lede-sdk-17.01.6-ar71xx-generic_gcc-5.4.0_musl-1.1.16.Linux-x86_64.tar.xz) 9 | -------------------------------------------------------------------------------- /challenges/mdfm_flag_verifier.en.md: -------------------------------------------------------------------------------- 1 | 2 | In this challenge you will have access to a file written for the **Manchester Dataflow Machine** assembly language. This machine is based on a tagged token dataflow paradigm, and we found some interesting stuff (below) that can help you in this mission. This technology is used a lot by them, so we need to learn it fast as it'll be very useful in the future. 3 | 4 | * [Architecture docs](http://www.butcher.team/mdfmdocs.pdf) 5 | * [Hardware implementation](https://github.com/jtsjunior/piflow) 6 | 7 | The file provided supplies a fixed output, and you will have to interpret the execution of this program to get the flag. 8 | 9 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/mdfm-flag-verifier_e0c4479716d92ef5930dbc8c97acb14f81de47b626b3b2ca99dfc9cec6a7058f.tar.gz) 10 | 11 | [Mirror](https://static.pwn2iwn.party/mdfm-flag-verifier_e0c4479716d92ef5930dbc8c97acb14f81de47b626b3b2ca99dfc9cec6a7058f.tar.gz) 12 | -------------------------------------------------------------------------------- /challenges/mdfm_flag_verifier.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "MDFM Flag Verifier", "tags": ["Reversing"], "pk": "J0ohPUPMJ2UaWYnV6dknbWXkq8SbLXT2bcw/VEIspds=", "salt": "EPI/RKi/OGO9NqZq2ia9v7qBIFx1mKMTojVuAYcfziw=", "id": "mdfm_flag_verifier"} 2 | -------------------------------------------------------------------------------- /challenges/mdfm_flag_verifier.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Neste desafio você terá acesso um arquivo no formato da linguagem assembly da **Máquina Dataflow de Manchester**. Uma máquina baseada no modelo de fluxo de dados dinâmico, e nós encontramos algumas coisas interessantes (abaixo) que podem ajudá-lo nessa missão. Essa tecnologia é muito usada pelos bavs, precisamos dominá-la pois será muito úlil no futuro. 3 | 4 | * [Documentação da arquitetura](http://www.butcher.team/mdfmdocs.pdf) 5 | * [Implementação de hardware](https://github.com/jtsjunior/piflow) 6 | 7 | O arquivo disponibilizado fornece uma saída fixa, você deve conseguir reverter o código para obter a flag. 8 | 9 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/mdfm-flag-verifier_e0c4479716d92ef5930dbc8c97acb14f81de47b626b3b2ca99dfc9cec6a7058f.tar.gz) 10 | 11 | [Mirror](https://static.pwn2iwn.party/mdfm-flag-verifier_e0c4479716d92ef5930dbc8c97acb14f81de47b626b3b2ca99dfc9cec6a7058f.tar.gz) 12 | -------------------------------------------------------------------------------- /challenges/message_board_pt1.en.md: -------------------------------------------------------------------------------- 1 | 2 | We have discovered the bavs are using a Message Board to communicate, and we believe they keep their secret messages in a log server. Follow the tracks and bring us all the secrets you can! 3 | 4 | Submit here the **first** flag! 5 | 6 | [http://10.133.70.7:8080](http://10.133.70.7:8080) 7 | 8 | After you get access to the VPN, wait 5 minutes before trying this challenge (time for deploy of the environment). 9 | 10 | **Automated tools are NOT required and NOT allowed, it's a technical challenge!** 11 | -------------------------------------------------------------------------------- /challenges/message_board_pt1.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Message Board [First Flag]", "tags": ["Web"], "pk": "1OP9A4lPVohJOijG6S79Gi6MofZSZ7MjS/fwcGo5QWI=", "salt": "gygnjq4O4jZdi0hzCo3V92A4IOo7h4UpoEYZfdx5Dp4=", "id": "message_board_pt1", "optional": "Isolated"} 2 | -------------------------------------------------------------------------------- /challenges/message_board_pt1.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Descobrimos que os bavs estão usando um Message Board para se comunicar, e acreditamos que eles guardem suas mensagens secretas em um servidor de logs. Vá até o fim e nos traga todos os segredos que conseguir! 3 | 4 | Submeta aqui a **primeira** flag! 5 | 6 | [http://10.133.70.7:8080](http://10.133.70.7:8080) 7 | 8 | Após liberar a VPN, espere 5 minutos para que o deploy do ambiente seja finalizado! 9 | 10 | **Tools automatizadas NÃO são necessárias e NÃO são permitidas, esse é um challenge técnico!** 11 | -------------------------------------------------------------------------------- /challenges/message_board_pt2.en.md: -------------------------------------------------------------------------------- 1 | 2 | We have discovered the bavs are using a Message Board to communicate, and we believe they keep their secret messages in a log server. Follow the tracks and bring us all the secrets you can! 3 | 4 | Submit here the **second** flag! 5 | 6 | [http://10.133.70.7:8080](http://10.133.70.7:8080) 7 | 8 | After you get access to the VPN, wait 5 minutes before trying this challenge (time for deploy of the environment). 9 | 10 | **Automated tools are NOT required and NOT allowed, it's a technical challenge!** 11 | -------------------------------------------------------------------------------- /challenges/message_board_pt2.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Message Board [Second Flag]", "tags": ["Web"], "pk": "DFYfzxxBw7llu1WpAW/wkqgW1rByJR3B3i7NxX6/QqE=", "salt": "XIi7RY2j5ZEgCMEmMMxf2tIRo6FYcAyFuzzpPsMfuSo=", "id": "message_board_pt2", "optional": "Isolated"} 2 | -------------------------------------------------------------------------------- /challenges/message_board_pt2.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Descobrimos que os bavs estão usando um Message Board para se comunicar, e acreditamos que eles guardem suas mensagens secretas em um servidor de logs. Vá até o fim e nos traga todos os segredos que conseguir! 3 | 4 | Submeta aqui a **segunda** flag! 5 | 6 | [http://10.133.70.7:8080](http://10.133.70.7:8080) 7 | 8 | Após liberar a VPN, espere 5 minutos para que o deploy do ambiente seja finalizado! 9 | 10 | **Tools automatizadas NÃO são necessárias e NÃO são permitidas, esse é um challenge técnico!** 11 | -------------------------------------------------------------------------------- /challenges/message_board_pt3.en.md: -------------------------------------------------------------------------------- 1 | 2 | We have discovered the bavs are using a Message Board to communicate, and we believe they keep their secret messages in a log server. Follow the tracks and bring us all the secrets you can! 3 | 4 | Submit here the **third** flag! 5 | 6 | [http://10.133.70.7:8080](http://10.133.70.7:8080) 7 | 8 | After you get access to the VPN, wait 5 minutes before trying this challenge (time for deploy of the environment). 9 | 10 | **Automated tools are NOT required and NOT allowed, it's a technical challenge!** 11 | -------------------------------------------------------------------------------- /challenges/message_board_pt3.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Message Board [Third Flag]", "tags": ["Web"], "pk": "NYHMyqcFrkgjVV3U2d2TX1xFzvi7/PS9PstHMrA2v74=", "salt": "73P8sV/cMBXzHQBpn5XUPw7M0PmkExKIC/Y1cnObFUo=", "id": "message_board_pt3", "optional": "Isolated"} 2 | -------------------------------------------------------------------------------- /challenges/message_board_pt3.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Descobrimos que os bavs estão usando um Message Board para se comunicar, e acreditamos que eles guardem suas mensagens secretas em um servidor de logs. Vá até o fim e nos traga todos os segredos que conseguir! 3 | 4 | Submeta aqui a **terceira** flag! 5 | 6 | [http://10.133.70.7:8080](http://10.133.70.7:8080) 7 | 8 | Após liberar a VPN, espere 5 minutos para que o deploy do ambiente seja finalizado! 9 | 10 | **Tools automatizadas NÃO são necessárias e NÃO são permitidas, esse é um challenge técnico!** 11 | -------------------------------------------------------------------------------- /challenges/message_board_pt4.en.md: -------------------------------------------------------------------------------- 1 | 2 | We have discovered the bavs are using a Message Board to communicate, and we believe they keep their secret messages in a log server. Follow the tracks and bring us all the secrets you can! 3 | 4 | Submit here the **fourth** flag! 5 | 6 | [http://10.133.70.7:8080](http://10.133.70.7:8080) 7 | -------------------------------------------------------------------------------- /challenges/message_board_pt4.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Message Board [Fourth Flag]", "optional": "Isolated", "tags": ["Forensics"], "pk": "CPvmbiRwG2H29/oRKfTJQFGdwGhSyPHNWBwVNltK2PM=", "salt": "y7CAQoQbexMDOai1loeWPcM/VewJCbicOWLx/ExwiRg=", "id": "message_board_pt4"} 2 | -------------------------------------------------------------------------------- /challenges/message_board_pt4.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Descobrimos que os bavs estão usando um Message Board para se comunicar, e acreditamos que eles guardem suas mensagens secretas em um servidor de logs. Vá até o fim e nos traga todos os segredos que conseguir! 3 | 4 | Submeta aqui a **quarta** flag! 5 | 6 | [http://10.133.70.7:8080](http://10.133.70.7:8080) 7 | -------------------------------------------------------------------------------- /challenges/minishell.en.md: -------------------------------------------------------------------------------- 1 | 2 | We figured out that the H1N337 viruses has a virtual variant too, and a sample has been encapsulated in this sandbox. Can you write a code to pass through the sandbox without being infected? 3 | 4 | **Server:** nc 200.136.252.34 4545 5 | 6 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/minishell_bd74b36c70709a5e2738780a29aa2c5fb9489f9545661fe0d5d2e2a3ad490292.tar.gz) 7 | 8 | [Mirror](https://static.pwn2win.party/minishell_bd74b36c70709a5e2738780a29aa2c5fb9489f9545661fe0d5d2e2a3ad490292.tar.gz) 9 | -------------------------------------------------------------------------------- /challenges/minishell.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Minishell", "tags": ["Exploitation"], "pk": "r7VB+3v/e3oUeJqluOHE47+rUyNiPibccL6w8Hl8q3Y=", "salt": "Az/ExzIclQX30tbSxBxtn3BbBr6NuXg2EOwNiCh25YQ=", "id": "minishell"} -------------------------------------------------------------------------------- /challenges/minishell.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Nós descobrimos que o vírus H1N337 também tem uma variante virtual, e uma amostra está encapsulada nessa sandbox. Você consegue escrever um código para bypassá-la sem ser infectado? 3 | 4 | **Server:** nc 200.136.252.34 4545 5 | 6 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/minishell_bd74b36c70709a5e2738780a29aa2c5fb9489f9545661fe0d5d2e2a3ad490292.tar.gz) 7 | 8 | [Mirror](https://static.pwn2win.party/minishell_bd74b36c70709a5e2738780a29aa2c5fb9489f9545661fe0d5d2e2a3ad490292.tar.gz) 9 | 10 | -------------------------------------------------------------------------------- /challenges/noizy_maze.en.md: -------------------------------------------------------------------------------- 1 | 2 | Tired of torturing their prisoners, the bavs decided to outsource their work and began a recruitment program for "digital torturers". One of the applicants made a noisy maze into where the prisoner is transported digitally. Should they manage to find the way out before the timer ends, they'll be rewarded with their own sanity. However, if they don't make it in time, their minds shall be held forever prisoner of the bavs, in a very noisy maze... 3 | 4 | For latency reasons, connect to this challenge through the wildcard box (when you get access). 5 | 6 | **Server:** nc 10.133.70.6 31337 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /challenges/noizy_maze.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Noizy Maze", "tags": ["PPC"], "pk": "pLw1YelqM4NNPCeVdf6JqskI6OQTz4XfEc+NcufEcbg=", "salt": "PiBMmt1e2m68nCnVQfx+veILS6W5RGDowYBgUU1RqNQ=", "id": "noizy_maze", "optional": "isolated"} 2 | -------------------------------------------------------------------------------- /challenges/noizy_maze.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Cansados de torturar seus prisioneiros, os bavs decidiram tercerizar o trabalho e estão fazendo um processo seletivo para a vaga de "torturador digital". Um dos candidatos fez um labirinto barulhento. O prisioneiro transportado digitalmente para dentro do labirinto. Se conseguir sair em tempo hábil é premiado com a recuperação da própria consciência. Passado o tempo limite, sua consciência se tornará uma eterna prisioneira dos bavs, em um labirinto extremamente barulhento... 3 | 4 | Por motivos de latência, resolva esse challenge usando a box Wildcard, de dentro da VPN (quando liberá-la): 5 | 6 | **Server:** nc 10.133.70.6 31337 7 | -------------------------------------------------------------------------------- /challenges/recovery_the_master_key.en.md: -------------------------------------------------------------------------------- 1 | 2 | We know that The Bavarian is creating a cryptosystem which contains a backdoor. Their plan is to force the users to use this system in their "end-to-end secure instant message applications". 3 | 4 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/recoverythemasterkey_dcd5a8ab45570b1ed503b6eb272dbb388e0be937444eaa388812548138d5c4f2.tar.gz) 5 | 6 | [Mirror](https://static.pwn2win.party/recoverythemasterkey_dcd5a8ab45570b1ed503b6eb272dbb388e0be937444eaa388812548138d5c4f2.tar.gz) 7 | -------------------------------------------------------------------------------- /challenges/recovery_the_master_key.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Recovery of the Master Key", "tags": ["Cryptography"], "pk": "z3PqB2ZG36RBoRnS/1b2KkLxEOXjOrPsW/XIbyyQaO8=", "salt": "BpvDX/4EMIVj3NMznLXCuVs2dXmOY7+n7wLLjv+Qa04=", "id": "recovery_the_master_key"} 2 | -------------------------------------------------------------------------------- /challenges/recovery_the_master_key.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Nós sabemos que os bavs estão criando um sistema criptográfico que contém um backdoor. Seu plano é forçar os usuários a usá-lo em seu aplicativo de mensagens com criptografia "end-to-end". 3 | 4 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/recoverythemasterkey_dcd5a8ab45570b1ed503b6eb272dbb388e0be937444eaa388812548138d5c4f2.tar.gz) 5 | 6 | [Mirror](https://static.pwn2win.party/recoverythemasterkey_dcd5a8ab45570b1ed503b6eb272dbb388e0be937444eaa388812548138d5c4f2.tar.gz) 7 | -------------------------------------------------------------------------------- /challenges/russian_polygons.en.md: -------------------------------------------------------------------------------- 1 | 2 | Tired of watching Russian roulette games between prisoners, the bavs decided to create a new game in which the prisoners' survival depends on luck. And it was even more sadistic: there was a chance that all prisioners would die... 3 | 4 | Anyway... Let's go to the rules. The prisoner is fixed at center (0,0) of a circular radius R room with concrete walls. He cannot move. Around the room (outside of it) there are n shooters fixed at indicated positions. After everybody is positioned, N big extremely thin but heavy regular polygon plates of a super dense material, able to flatten even the walls, fall in the room region. Once a shooter has sight of the prisioner, there is a not null chance (indicated in each test) to miss their only shot. If two shooters are aligned, the one closer to the prisoner will shoot and leave. 5 | 6 | Yes... the bavs think all this expense is worth it. 7 | 8 | In the first line of the input there are 3 integers: the radius R, the number of shooters n and the number of polygons N. In each of the following n lines there are 3 real numbers indicating the cartesian coordinates of each shooter and the probability of, once the prisoner is seen, missing their shot. In each of the following N lines there are 4 real numbers and an integer, indicating the coordinates of a regular polygon's center, the coordinates of one of the polygon's vertexes and the number of edges. 9 | 10 | The output must have a single real number with 5 decimal places indicating the probability of survival of the prisoner. 11 | 12 | **Example:** 13 | 14 | input: 15 | 16 | 10 2 2 17 | 18 | 11 0 0.1 19 | 20 | 0 11 0.1 21 | 22 | 10 0 11 0 3 23 | 24 | 0 10 0 10.5 3 25 | 26 | output: 27 | 28 | 0.10000 29 | 30 | **Server:** openssl s_client -connect programming.pwn2.win:9001 31 | 32 | **Connection template:** 33 | 34 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/russian-polygons-template_3319bcb3c22f00875e8c17426dfa80ab579cfa14ff7fad546860718ce04965b4.tar.gz.tar.gz) 35 | 36 | [Mirror](https://static.pwn2win.party/russian-polygons-template_3319bcb3c22f00875e8c17426dfa80ab579cfa14ff7fad546860718ce04965b4.tar.gz.tar.gz) 37 | 38 | -------------------------------------------------------------------------------- /challenges/russian_polygons.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Russian Polygons", "tags": ["PPC-M"], "pk": "xxfA7vpR6EZlmHmEoNBzsPtc6aFapuwJ2bh8+qoiZ7k=", "salt": "ip6D+6DqH7rFVRC3RX1vXd35flcsEg5bzBQmEpm9K6w=", "id": "russian_polygons", "optional": "platform"} 2 | -------------------------------------------------------------------------------- /challenges/russian_polygons.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Cansados de assistir a jogos de roleta russa entre os prisioneiros, os bavs decidiram inventar um novo jogo em que a sobrevivência dos prisioneiros dependiam de sorte. E era ainda mais sádico: tinha uma chance diferente de zero de todos morrerem... 3 | 4 | Enfim... Vamos às regras. No centro (0,0) de uma sala circular de raio R com paredes de concreto é fixado o prisioneiro, sem possibilidade de movimentação. Ao redor da sala (do lado de fora) são pisicionados n atiradores nas posições indicadas em cada teste. Depois de todos posicionados, N grandes placas em forma de polígonos regulares extremamente finas mas extremamente pesadas de um material super denso, capazes de achatar até as paredes, caem na região da sala. Uma vez que um atirador tenha visão direta para o prisioneiro, existe uma chance de erro da única bala presente na arma, que será indicada em cada teste. Se dois atiradores estiverem alinhados, os mais próximos do prisioneiro atiram antes e saem da frente. 5 | 6 | Sim... eles acham que todo esse gasto financeiro vale o "show". 7 | 8 | A primeira linha tem três inteiros indicando, respectivamente, o raio R da sala, o número n de atiradores e o número N de polígonos. As n linhas seguintes tem cada uma três números reais indicando as duas coordenadas cartesianas da posição do atirador e a probabilidade de uma vez avistado o prisioneiro, ele errar o tiro. As N linhas seguintes tem cada uma quatro números reais e um inteiro, indicando, respectivamente, as coordenadas do centro do polígono regular, as coordenadas de um dos vértices e o número de lados. 9 | 10 | A saída deve conter apenas um número real, com 5 casas decimais indicando a probabilidade do prisioneiro sobreviver. 11 | 12 | **Exemplo:** 13 | 14 | input: 15 | 16 | 10 2 2 17 | 18 | 11 0 0.1 19 | 20 | 0 11 0.1 21 | 22 | 10 0 11 0 3 23 | 24 | 0 10 0 10.5 3 25 | 26 | output: 27 | 28 | 0.10000 29 | 30 | **Server:** openssl s_client -connect programming.pwn2.win:9001 31 | 32 | **Connection template:** 33 | 34 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/russian-polygons-template_3319bcb3c22f00875e8c17426dfa80ab579cfa14ff7fad546860718ce04965b4.tar.gz.tar.gz) 35 | 36 | [Mirror](https://static.pwn2win.party/russian-polygons-template_3319bcb3c22f00875e8c17426dfa80ab579cfa14ff7fad546860718ce04965b4.tar.gz.tar.gz) 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /challenges/sgle.en.md: -------------------------------------------------------------------------------- 1 | 2 | Do you know the Super Gravitron by VVVVVV ([https://www.youtube.com/watch?v=CQ-3-K1Ro2g](https://www.youtube.com/watch?v=CQ-3-K1Ro2g))? Yes, Terry Cavanagh is a genius! And the bavs tasked with recruiting new programmers made a version of this game that runs on the console, and use it in their recruitment program! We're trying to infiltrate their IT sector, so we need your programming skills to beat the game, by creating a bot that can last 137 seconds in it! 3 | 4 | **Instructions:** 5 | 6 | **1** - You can only move horizontally, as in the original game, using the directional keys (<- and ->); 7 | 8 | **2** - The objective is "just" to avoid the obstacles; 9 | 10 | **3** - When you hit an obstacle, it will be marked with an "X". Press or to exit; 11 | 12 | **4** - Press or to close the game at any time; 13 | 14 | **5** - The elapsed time and the position of the Jumper(I) are shown in the game screen; 15 | 16 | **6** - Run it in full screen mode to avoid problems; 17 | 18 | For latency reasons, solve this challenge using the Wildcard box, inside the VPN (when you get access): 19 | 20 | ssh sgle@10.133.70.3 21 | 22 | Ahkae3beePhu9ooThei7 23 | 24 | -------------------------------------------------------------------------------- /challenges/sgle.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Super Gravitron Leet Edition (SGLE)", "tags": ["PPC"], "pk": "iXyNSx7K9FZjT2Xxx6akAmzceue5xVtuZk1ZTctKLVM=", "salt": "Sw3cUiIwvw/i1aUxClOe7PHYP+AgDacEFycgQ+JHtGg=", "id": "sgle", "optional": "isolated"} 2 | -------------------------------------------------------------------------------- /challenges/sgle.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Você conhece o Super Gravitron do VVVVVV ([https://www.youtube.com/watch?v=CQ-3-K1Ro2g](https://www.youtube.com/watch?v=CQ-3-K1Ro2g))? Sim, Terry Cavanagh é um grande gênio! Descobrimos que os bavs responsáveis pelo recrutamento de programadores fizeram uma versão desse game que roda no terminal, e usam como processo seletivo! Como predendemos nos infiltrar na área de TI deles, precisamos de suas skills de programming para bater o game, criando um bot que aguente 137 segundos! 3 | 4 | **Instruções**: 5 | 6 | **1** - Você só consegue se mover para os lados, como no jogo original, usando as setas (<- e ->); 7 | 8 | **2** - O objetivo é "apenas" desviar dos obstáculos; 9 | 10 | **3** - Quando você bater em um obstáculo, ele será marcado com um "X", pressione ou para sair; 11 | 12 | **4** - Use ou para fechar o jogo a qualquer momento; 13 | 14 | **5** - O tempo percorrido e a posição do Jumper (I) são mostrados na tela do game; 15 | 16 | **6** - Rode em tela cheia, para evitar problemas; 17 | 18 | Por motivos de latência, resolva esse challenge usando a box Wildcard, de dentro da VPN (quando liberá-la): 19 | 20 | ssh sgle@10.133.70.3 21 | 22 | Ahkae3beePhu9ooThei7 23 | -------------------------------------------------------------------------------- /challenges/sum.en.md: -------------------------------------------------------------------------------- 1 | This problem is an example on how to connect to our server to read inputs and send the outputs for the programming challenges. Given a set of numbers greater than zero, followed by zero and a line break, compute the sum of these numbers. All you need to do in order to obtain the flag is to execute the implementation you see more fit! 2 | 3 | The solution for this problem is available below: 4 | 5 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/sum-solvers-example-platform_6190c72f9ec33a27adac2529193f486aa89a79e3c5861cda8066d7cd0a4914dd.tar.gz) 6 | 7 | [Mirror](https://static.pwn2win.party/sum-solvers-example-platform_6190c72f9ec33a27adac2529193f486aa89a79e3c5861cda8066d7cd0a4914dd.tar.gz) 8 | 9 | **Server**: openssl s_client -connect programming.pwn2.win:9000 10 | -------------------------------------------------------------------------------- /challenges/sum.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Sum [Hello World Platform]", "tags": ["PPC-M"], "pk": "j/9doxZMuFk3eXP55F8pPWqdYPiIaV2xHoJkWXi1UPk=", "salt": "UPmvj8gkGMelC7Z9VqI5Sc3xVcEJ5lDA+dkR7AMjZ9E=", "id": "sum", "optional": "platform"} 2 | -------------------------------------------------------------------------------- /challenges/sum.pt.md: -------------------------------------------------------------------------------- 1 | Este problema é um exemplo de como conectar-se ao nosso servidor para ler as entradas e enviar a saídas dos desafios de programação. Dado um conjunto de números maiores que zero, finalizado pelo número zero e por um pulo de linha, forneça como saída a soma desses números. Basta executar uma das versões para obter a flag! 2 | 3 | A resolução deste problema está disponível em: 4 | 5 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/sum-solvers-example-platform_6190c72f9ec33a27adac2529193f486aa89a79e3c5861cda8066d7cd0a4914dd.tar.gz) 6 | 7 | [Mirror](https://static.pwn2win.party/sum-solvers-example-platform_6190c72f9ec33a27adac2529193f486aa89a79e3c5861cda8066d7cd0a4914dd.tar.gz) 8 | 9 | **Server**: openssl s_client -connect programming.pwn2.win:9000 10 | -------------------------------------------------------------------------------- /challenges/super_storage.en.md: -------------------------------------------------------------------------------- 1 | 2 | One of our informants discovered this bav server, can you exploit it to find out its secrets? 3 | 4 | **Server:** nc 200.136.252.46 2323 5 | 6 | [File](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/super-storage_e40a9e5ac9a0f95973f4f9c906a274c48f6030cfce66ae826c53f6889276cf9e.tar.gz) 7 | 8 | [Mirror](https://static.pwn2win.party/super-storage_e40a9e5ac9a0f95973f4f9c906a274c48f6030cfce66ae826c53f6889276cf9e.tar.gz) 9 | -------------------------------------------------------------------------------- /challenges/super_storage.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Super Storage", "tags": ["Exploitation"], "pk": "3WYqEZ/O6yd2R4C8pM3Tyx41KJ3d2FPcJKdfvuMsjyo=", "salt": "gAOAMmnebv40N06DLWhItoRDRnQ07CnAx32kDTFFeBw=", "id": "super_storage"} -------------------------------------------------------------------------------- /challenges/super_storage.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Um dos nossos informantes descobriu esse servidor dos bavs, você consegue exploitá-lo para descobrirmos seus segredos? 3 | 4 | **Server:** nc 200.136.252.46 2323 5 | 6 | [File](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/super-storage_e40a9e5ac9a0f95973f4f9c906a274c48f6030cfce66ae826c53f6889276cf9e.tar.gz) 7 | 8 | [Mirror](https://static.pwn2win.party/super-storage_e40a9e5ac9a0f95973f4f9c906a274c48f6030cfce66ae826c53f6889276cf9e.tar.gz) 9 | -------------------------------------------------------------------------------- /challenges/too_slow.en.md: -------------------------------------------------------------------------------- 1 | 2 | This binary prints the flag to stdout. However, it is too slow. Can you make it fast enough to obtain the flag before the competition ends? 3 | 4 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/too-slow_89cee0b606423c570d047681ad5d36ed9d13577c995fa9d55c5ecb7fcb6942e4.tar.gz) 5 | 6 | [Mirror](https://static.pwn2win.party/too-slow_89cee0b606423c570d047681ad5d36ed9d13577c995fa9d55c5ecb7fcb6942e4.tar.gz) 7 | 8 | [RISC-V Toolchain](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/riscv-tools.tgz) 9 | -------------------------------------------------------------------------------- /challenges/too_slow.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Too Slow", "tags": ["Reversing"], "pk": "bYRRxOn/5RwJZCw28y9J5odCGkeVcNOkhlzxhALcIlQ=", "salt": "0zJDiYQdGA+2EuN5EnIH5lc61YI7rB1yLysGt8LBFS8=", "id": "too_slow"} -------------------------------------------------------------------------------- /challenges/too_slow.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Esse binário printa a flag na stdout. Porém, é muito lento. Você pode deixá-lo rápido o suficiente para que termine antes do fim da competição? 3 | 4 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/too-slow_89cee0b606423c570d047681ad5d36ed9d13577c995fa9d55c5ecb7fcb6942e4.tar.gz) 5 | 6 | [Mirror](https://static.pwn2win.party/too-slow_89cee0b606423c570d047681ad5d36ed9d13577c995fa9d55c5ecb7fcb6942e4.tar.gz) 7 | 8 | [RISC-V Toolchain](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/riscv-tools.tgz) 9 | -------------------------------------------------------------------------------- /challenges/tpm20.en.md: -------------------------------------------------------------------------------- 1 | 2 | Wilson created this application in a trusted platform. The thing is though, he encrypted the flag but lost the private key :( 3 | 4 | **Server:** nc 10.133.70.5 4500 5 | 6 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/tpm20_828976b44e5a91dd81f1d5da6c46279f4c890dead9b537cff9af07f3930ebf24.tar.gz) 7 | 8 | [Mirror](https://static.pwn2win.party/tpm20_828976b44e5a91dd81f1d5da6c46279f4c890dead9b537cff9af07f3930ebf24.tar.gz) 9 | -------------------------------------------------------------------------------- /challenges/tpm20.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "TPM 2.0", "tags": ["Exploitation", "Cryptography"], "pk": "T7x9HGYLL+i1w1yT7+uKyZHWHMK3j8J8c4lZWqd4UEU=", "salt": "BPBYLc2Nkpezb8RkZBAtX0lvqV7YWbBfTC+zgc0P9yY=", "id": "tpm20", "optional": "isolated"} 2 | -------------------------------------------------------------------------------- /challenges/tpm20.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Wilson criou essa aplicação em uma *trusted platform*, tecnologia muito usada pelos bavs. Ele criptografou a flag, porém perdeu a chave privada. :( 3 | 4 | **Server:** nc 10.133.70.5 4500 5 | 6 | [Link](https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/static.pwn2win.party/tpm20_828976b44e5a91dd81f1d5da6c46279f4c890dead9b537cff9af07f3930ebf24.tar.gz) 7 | 8 | [Mirror](https://static.pwn2win.party/tpm20_828976b44e5a91dd81f1d5da6c46279f4c890dead9b537cff9af07f3930ebf24.tar.gz) 9 | 10 | -------------------------------------------------------------------------------- /challenges/trick_or_treat.en.md: -------------------------------------------------------------------------------- 1 | 2 | Our central of intelligence discovered an exposed server that seems to be a front website used by The Bavarians. They also ascertained that the administrator's account is called "admin". We need you to hack into his account to find out which secrets they are hiding. Be careful, the server is using an aggressive anti-bruteforce system which blocks you for five minutes after only five wrong login attempts. 3 | 4 | **Server**: [http://200.136.252.26](http://200.136.252.26) 5 | 6 | **Automated tools are NOT required and NOT allowed, it's a technical challenge!** 7 | -------------------------------------------------------------------------------- /challenges/trick_or_treat.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Trick or Treat", "tags": ["Web"], "pk": "H0dn0g44b+gJVb+NOB5/a9p66DVCdH7VUv45vZx0kNo=", "salt": "2DVD0x3VvT8HinI2ByxbxyP+Q9M5M0s3DANy10mhtg4=", "id": "trick_or_treat"} -------------------------------------------------------------------------------- /challenges/trick_or_treat.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Nossa central de inteligência descobriu um servidor exposto que parece ser um site usado pelo The Bavarians. Eles também descobriram que a conta de administração é "admin". Precisamos descobrir quais segredos eles estão escondendo. Tenha cuidado, eles estão usando um sistema de anti-bruteforce agressivo que bloqueia você por 5 minutos a cada 5 tentativas erradas. 3 | 4 | **Server**: [http://200.136.252.26](http://200.136.252.26) 5 | 6 | **Tools automatizadas NÃO são necessárias e NÃO são permitidas, esse é um challenge técnico!** 7 | -------------------------------------------------------------------------------- /challenges/ultramegapolyglot.en.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | The Bavarians group is using a new authentication method based on polyglots of esoteric languages, help us to create one! 4 | 5 | [http://200.136.252.44](http://200.136.252.44) 6 | -------------------------------------------------------------------------------- /challenges/ultramegapolyglot.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "U L T R A M E G A P O L Y G L O T", "tags": ["Miscellaneous"], "pk": "GBE33vDB8BJw9Lw2fLjkl8XYHukiI1UTkG3RagYVB4k=", "salt": "P1GmjqM0MhOc5P3aYq3J/Tco2YieBJIhXph7Diz1NmU=", "id": "ultramegapolyglot"} -------------------------------------------------------------------------------- /challenges/ultramegapolyglot.pt.md: -------------------------------------------------------------------------------- 1 | 2 | O grupo Bavarians está utilizando um novo método de autenticação baseado em poliglotas de linguagens esotéricas, nos ajude a criar um! 3 | 4 | [http://200.136.252.44](http://200.136.252.44) 5 | -------------------------------------------------------------------------------- /challenges/wildcard.en.md: -------------------------------------------------------------------------------- 1 | 2 | This box can be used to solve other isolated challenges. Avoid problems with latency, for example. 3 | 4 | ssh wildcard@10.133.70.4 5 | 6 | passwd: eik7avou3yoo9Ohtai4a 7 | -------------------------------------------------------------------------------- /challenges/wildcard.json: -------------------------------------------------------------------------------- 1 | {"description": "", "title": "Wildcard Box [Just read]", "tags": ["Not a chall"], "pk": "DTxhwXrHz9ir/303DNrrS5RkCWpx8UhrGEV66IoSt64=", "salt": "oq/7IWg7nvlJZJ36o9GIbEwMqtxyAgQVlzFiFBw9c5M=", "id": "wildcard", "optional": "isolated"} 2 | -------------------------------------------------------------------------------- /challenges/wildcard.pt.md: -------------------------------------------------------------------------------- 1 | 2 | Essa máquina pode ser utilizada para resolver outros challenges isolados. Evita problemas com latência, por exemplo. 3 | 4 | ssh wildcard@10.133.70.4 5 | 6 | passwd: eik7avou3yoo9Ohtai4a 7 | 8 | -------------------------------------------------------------------------------- /container-docker.en.md: -------------------------------------------------------------------------------- 1 | ## Instructions to install and use the Platform via Docker: 2 | 3 | **1** - Download the Dockerfile [here](https://static.pwn2win.party/Dockerfile) and put it alone in a folder. 4 | 5 | **2** - Install docker: 6 | ```bash 7 | $ sudo apt-get install docker.io 8 | ``` 9 | 10 | **3** - Enter the Dockerfile directory, and build the *base* image: 11 | ```bash 12 | $ sudo docker build -t pwn2win . 13 | ``` 14 | 15 | **4** - Create a container from image (we are assuming here that your user's ssh key is the key that is in GitHub): 16 | ```bash 17 | $ sudo docker run --name pwn2win -it -v $HOME/.ssh/id_rsa:/root/.ssh/id_rsa pwn2win 18 | ``` 19 | 20 | **5** - Replace your GitHub infos in the variables, and install the platform dependencies inside the container: 21 | ```bash 22 | root@c62ed90932e6:/ctf/2018# git config --global user.name "YOUR_USER_HERE" && git config --global user.email "YOUR_EMAIL_HERE" && git clone git@github.com:pwn2winctf/2018.git && cd $HOME_DIR/2018 && curl https://bootstrap.pypa.io/get-pip.py | sudo -H python && sudo -H python -m pip install -r pip-requirements.txt 23 | ``` 24 | - **5.1** - Now, if you type "exit" or reboot your Host PC, you need to go back to the container: 25 | ```bash 26 | $ sudo docker restart pwn2win 27 | $ sudo docker exec -it pwn2win /bin/bash 28 | ``` 29 | 30 | **6** - Back to [README](README.en.md) and follow from the fourth step onwards. 31 | 32 | 33 | -------------------------------------------------------------------------------- /container-docker.pt.md: -------------------------------------------------------------------------------- 1 | ## Instruções para instalar e usar a plataforma com Docker: 2 | 3 | **1** - Baixe o Dockerfile [aqui](https://static.pwn2win.party/Dockerfile) e coloque em uma pasta onde fique apenas ele. 4 | 5 | **2** - Instale o docker: 6 | ```bash 7 | $ sudo apt-get install docker.io 8 | ``` 9 | 10 | **3** - Entre no diretório onde está o Dockerfile, e crie a imagem base: 11 | ```bash 12 | $ sudo docker build -t pwn2win . 13 | ``` 14 | 15 | **4** - Crie o container a partir da imagem (nós estamos assumindo aqui que a key do seu usuário é a que está adicionada no GitHub): 16 | ```bash 17 | $ sudo docker run --name pwn2win -it -v $HOME/.ssh/id_rsa:/root/.ssh/id_rsa pwn2win 18 | ``` 19 | 20 | **5** - Substitua suas informações do GitHub nas variáveis, e instale as dependências da plataforma dentro do container: 21 | ```bash 22 | root@c62ed90932e6:/ctf/2018# git config --global user.name "SEU_NOME_AQUI" && git config --global user.email "SEU_EMAIL_AQUI" && git clone git@github.com:pwn2winctf/2018.git && cd $HOME_DIR/2018 && curl https://bootstrap.pypa.io/get-pip.py | sudo -H python && sudo -H python -m pip install -r pip-requirements.txt 23 | ``` 24 | 25 | - **5.1** - Agora, se você digitar "exit" ou reiniciar sua máquina física, terá que retornar para o container: 26 | ```bash 27 | $ sudo docker restart pwn2win 28 | $ sudo docker exec -it pwn2win /bin/bash 29 | ``` 30 | 31 | **6** - Volte para o [README](README.pt.md) e continue a partir do passo 4. 32 | 33 | 34 | -------------------------------------------------------------------------------- /container-lxc.en.md: -------------------------------------------------------------------------------- 1 | ## Instructions to install and use the container 2 | 3 | **1**. Unless you already use LXD, install and setup it. [This document](https://linuxcontainers.org/lxd/getting-started-cli/#getting-the-packages) contains instructions for various distros. If you are using Ubuntu, just run: 4 | 5 | ```bash 6 | $ sudo apt update 7 | $ sudo apt install lxd 8 | $ sudo lxd init # Accept the default options 9 | ``` 10 | 11 | **2**. [Download our image](https://static.pwn2win.party/pwn2win2018.tar.gz), import it to LXD, and create an instance of the container: 12 | 13 | ```bash 14 | $ lxc image import pwn2win2018.tar.gz --alias=pwn2win2018 15 | $ lxc launch pwn2win2018 pwn2win 16 | ``` 17 | 18 | **3**. The container already has this repository cloned for you. Just make sure your copy is up to date: 19 | 20 | ```bash 21 | $ lxc exec pwn2win -- git pull 22 | ``` 23 | 24 | **4**. Either generate a new SSH key pair inside the container and [add it](https://github.com/settings/ssh/new) to the GitHub account you will be using: 25 | 26 | ```bash 27 | $ lxc exec pwn2win -- ssh-keygen -t ed25519 28 | $ lxc exec pwn2win -- cat .ssh/id_ed25519.pub 29 | ``` 30 | 31 | **or** copy your existing key pair to the container: 32 | 33 | ```bash 34 | $ lxc file push ~/.ssh/id_* pwn2win/root/.ssh/ 35 | ``` 36 | 37 | **5**. Just follow the instructions in the [README](README.en.md) (from the third step onwards) prefixing every command with `lxc exec pwn2win --`. For example, to login as the leader of your team, type `lxc exec pwn2win -- ./ctf init`. You can always spawn a shell by typing `lxc exec pwn2win sh`. 38 | -------------------------------------------------------------------------------- /container-lxc.pt.md: -------------------------------------------------------------------------------- 1 | ## Instruções para instalação e uso do container 2 | 3 | **1**. Primeiro, instale e configure o LXD. [Este documento](https://linuxcontainers.org/lxd/getting-started-cli/#getting-the-packages) contém pacotes para várias distros. Se você está usando ubuntu, siga os passos abaixo: 4 | 5 | ```bash 6 | $ sudo add-apt-repository ppa:ubuntu-lxc/lxc-stable 7 | $ sudo apt update 8 | $ sudo apt install lxd 9 | $ sudo lxd init # Use as opções padrões 10 | ``` 11 | 12 | **2**. Baixe a [imagem](https://static.pwn2win.party/pwn2win2018.tar.gz) do container LXC e importe no LXD: 13 | 14 | ```bash 15 | $ lxc image import pwn2win2018.tar.gz --alias=pwn2win2018 16 | ``` 17 | 18 | **3**. Crie uma instância a partir da imagem: 19 | 20 | ```bash 21 | $ lxc launch pwn2win2018 pwn2win 22 | ``` 23 | **4**. Certifique-se que está com a última versão do repositório: 24 | 25 | ```bash 26 | $ lxc exec pwn2win -- git pull 27 | ``` 28 | 29 | **5**. Agora, **OU** você pode gerar novas chaves e [adicioná-las](https://github.com/settings/keys) no GitHub: 30 | ```bash 31 | $ lxc exec pwn2win -- ssh-keygen -t ed25519 32 | $ lxc exec pwn2win -- cat .ssh/id_ed25519.pub 33 | ``` 34 | 35 | OU ainda pode usar suas próprias chaves, da máquina host: 36 | ```bash 37 | $ lxc file push ~/.ssh/id_* pwn2win/root/.ssh/ 38 | ``` 39 | 40 | 41 | **6**. Siga as instruções do [README](https://github.com/pwn2winctf/2018/blob/master/README.pt.md) prefixando os comandos com *lxc exec pwn2win --*. Por exemplo, se você for o capitão do time, registre a equipe digitando *lxc exec pwn2win -- ./ctf init*. Se quiser spawnar uma shell, use: *lxc exec pwn2win sh*. 42 | -------------------------------------------------------------------------------- /ctf: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals, division, print_function,\ 5 | absolute_import 6 | import os 7 | import sys 8 | import codecs 9 | import argparse 10 | import getpass 11 | import base64 12 | 13 | from nizkctf.cli import scoreboard, challenges, news, team, localserver 14 | from nizkctf.repohost import RepoHost 15 | from nizkctf.subrepo import SubRepo 16 | from nizkctf.challenge import Challenge, derive_keypair, random_salt 17 | from nizkctf.six import input, to_unicode, PY2 18 | from nizkctf.settings import Settings 19 | 20 | 21 | def ensure_unicode_locale(): 22 | if sys.stdout.encoding != 'UTF-8': 23 | sys.stderr.write('\033[1mWARNING\033[00m: This CTF accepts ' 24 | 'international characters in team names and\nproblem ' 25 | 'descriptions. You are currently not using a Unicode ' 26 | 'locale,\ntherefore you may experience random ' 27 | 'UnicodeEncodeError exceptions.\n\nPlease fix by ' 28 | 'changing to a Unicode locale, e.g.\n\n' 29 | ' export LC_ALL=en_US.UTF-8\n\n\n') 30 | 31 | 32 | def read_opt(msg, opts): 33 | while True: 34 | inp = input(msg) 35 | if inp.strip() in opts: 36 | return inp.strip() 37 | 38 | 39 | def read_auth(opt): 40 | while True: 41 | if opt == '1': 42 | print('Enter your username and password:') 43 | username = input('Username: ') 44 | password = getpass.getpass('Password: ') 45 | OTP = input('OTP (only if you use two-factor authentication): ') 46 | if OTP != "": 47 | RepoHost.login(username=username, password=password, OTP=OTP) 48 | else: 49 | RepoHost.login(username=username, password=password) 50 | return True 51 | elif opt == '2': 52 | print('Enter your auth token (with public_repo scope):') 53 | token = input('Token: ') 54 | RepoHost.login(token=token) 55 | return True 56 | 57 | 58 | def cmd_scoreboard(args): 59 | if args.pull: 60 | SubRepo.pull() 61 | 62 | ranking, submissions = scoreboard.rank() 63 | scoreboard.pprint(ranking, top=args.top, show_names=args.names) 64 | 65 | if args.chart: 66 | scoreboard.plot(ranking, submissions) 67 | 68 | 69 | def parse_countries(countries): 70 | if countries == '': 71 | return [] 72 | return [c.lower().strip() for c in countries.split(',')] 73 | 74 | 75 | def cmd_register(args): 76 | team.register(args.name, parse_countries(args.countries)) 77 | 78 | 79 | def cmd_login(args): 80 | if args.token: 81 | RepoHost.login(token=args.token) 82 | elif args.username and args.password: 83 | RepoHost.login(username=args.username, password=args.password) 84 | else: 85 | read_auth('1') 86 | print('Credentials stored') 87 | print('Cloning submissions repository') 88 | SubRepo.clone() 89 | 90 | 91 | def cmd_init(args): 92 | print('NIZKCTF initializing your environment.') 93 | print('First of all, we need your github/gitlab credentials:') 94 | print('[1] auth via username / password') 95 | print('[2] auth with personal access token (with public_repo scope)') 96 | opt = read_opt('>> ', {'1', '2'}) 97 | print('') 98 | 99 | read_auth(opt) 100 | print('') 101 | 102 | print('Cloning submissions repository') 103 | SubRepo.clone() 104 | 105 | print('Do you want to register a new team? [y/n]') 106 | opt = read_opt('>> ', {'y', 'n'}) 107 | if opt == 'y': 108 | print('Enter your team name:') 109 | team_name = input('>> ') 110 | print("Enter a comma-separated list of your member's countries.") 111 | print("Use 2-letter ISO country codes: http://flag-icon-css.lip.is") 112 | print("Example: For a team composed by brazilians and russians: BR,RU") 113 | print("(leave empty if you want to omit nationality)") 114 | team_countries = parse_countries(input('>> ')) 115 | team.register(team_name, team_countries) 116 | print('') 117 | 118 | print('We are all set!') 119 | 120 | 121 | def cmd_submit(args): 122 | print('Checking flag: %s' % args.flag) 123 | ret, msg = challenges.submit_flag(args.flag, args.chall) 124 | print(msg) 125 | if not ret: 126 | sys.exit(1) 127 | 128 | 129 | def cmd_challenges(args): 130 | challenges.pprint() 131 | 132 | 133 | def cmd_serve(args): 134 | localserver.main(port=args.port) 135 | 136 | 137 | def cmd_add_challenge(args): 138 | id = input('Challenge id (digits, letters, underscore): ').strip() 139 | title = input('Title: ').strip() 140 | description = input('Description: ').strip() 141 | if not Settings.dynamic_scoring: 142 | points = int(input('Points: ').strip()) 143 | tags = input('Tags (separate tags with space): ').strip().split() 144 | salt = input('Salt (empty string for random salt): ').strip() 145 | if salt == '': 146 | salt = random_salt() 147 | flag = input('Flag: ').strip() 148 | 149 | pk, sk = derive_keypair(salt, flag) 150 | chall = Challenge(id=id) 151 | chall['id'] = id 152 | chall['title'] = title 153 | chall['description'] = description 154 | if not Settings.dynamic_scoring: 155 | chall['points'] = points 156 | chall['tags'] = tags 157 | chall['salt'] = salt 158 | chall['pk'] = pk 159 | chall.save() 160 | 161 | 162 | def cmd_news(args): 163 | if args.pull: 164 | SubRepo.pull() 165 | news.pprint(news.News(), team_only=args.team) 166 | 167 | 168 | def cmd_add_news(args): 169 | news.submit(args.msg, args.to) 170 | 171 | 172 | def main(): 173 | ensure_unicode_locale() 174 | if PY2: 175 | sys.argv = map(to_unicode, sys.argv) 176 | 177 | commands = { 178 | 'init': cmd_init, 179 | 'login': cmd_login, 180 | 'score': cmd_scoreboard, 181 | 'register': cmd_register, 182 | 'submit': cmd_submit, 183 | 'challs': cmd_challenges, 184 | 'serve': cmd_serve, 185 | 186 | 'add': cmd_add_challenge, 187 | 188 | 'news': cmd_news, 189 | 'add_news': cmd_add_news, 190 | } 191 | 192 | parser = argparse.ArgumentParser(description='nizk CTF cli') 193 | subparsers = parser.add_subparsers(help='command help', 194 | metavar='{init,login,score,register,' 195 | 'submit,challs,serve,news}') 196 | 197 | parser_init = subparsers.add_parser('init', help='init ctf environment') 198 | parser_init.set_defaults(command='init') 199 | 200 | parser_login = subparsers.add_parser('login', 201 | help='authenticate in gitlab/github') 202 | parser_login.set_defaults(command='login') 203 | parser_login.add_argument('--username', type=str, default=None, 204 | metavar='USERNAME', 205 | help='username for logging in') 206 | parser_login.add_argument('--password', type=str, default=None, 207 | metavar='PASSWORD', 208 | help='password for logging in') 209 | parser_login.add_argument('--token', type=str, default=None, 210 | metavar='TOKEN', 211 | help='use personal access token (with public_repo scope) instead of ' 212 | 'username/password') 213 | 214 | parser_score = subparsers.add_parser('score', help='scoreboard help') 215 | parser_score.set_defaults(command='score') 216 | parser_score.add_argument('--top', type=int, 217 | default=0, metavar='N', 218 | help='size of ranking to display') 219 | parser_score.add_argument('--pull', action='store_true', 220 | help='pull submissions before displaying scores') 221 | parser_score.add_argument('--names', action='store_true', 222 | help='display team names') 223 | parser_score.add_argument('--chart', action='store_true', 224 | help='display chart') 225 | 226 | parser_register = subparsers.add_parser('register', 227 | help='register a new team') 228 | parser_register.set_defaults(command='register') 229 | parser_register.add_argument('name', metavar='NAME', help='team name') 230 | parser_register.add_argument('countries', metavar='COUNTRIES', 231 | help='Comma-separated list of 2-letter ISO ' 232 | 'country codes (pass an empty string to ' 233 | 'omit nationality)') 234 | 235 | parser_submit = subparsers.add_parser('submit', help='submit a flag') 236 | parser_submit.set_defaults(command='submit') 237 | parser_submit.add_argument('flag', metavar='FLAG', help='flag') 238 | parser_submit.add_argument('--chall', type=str, default=None, 239 | metavar='CHALL_ID', help='challenge id') 240 | 241 | parser_challenges = subparsers.add_parser('challs', help='list challenges') 242 | parser_challenges.set_defaults(command='challs') 243 | 244 | parser_serve = subparsers.add_parser('serve', help='start a local server') 245 | parser_serve.set_defaults(command='serve') 246 | parser_serve.add_argument('--port', type=int, default=8000, 247 | metavar='PORT', help='port') 248 | 249 | parser_add_challenge = subparsers.add_parser('add') 250 | parser_add_challenge.set_defaults(command='add') 251 | 252 | parser_news = subparsers.add_parser('news', help='pull and show news') 253 | parser_news.set_defaults(command='news') 254 | parser_news.add_argument('--pull', action='store_true', 255 | help='pull news before displaying news') 256 | parser_news.add_argument('--team', action='store_true', 257 | help='show only team news') 258 | 259 | parser_add_news = subparsers.add_parser('add_news') 260 | parser_add_news.set_defaults(command='add_news') 261 | parser_add_news.add_argument('--msg', required=True, 262 | help='msg to be added') 263 | parser_add_news.add_argument('--to', help='team name') 264 | 265 | if len(sys.argv) == 1: 266 | parser.print_help() 267 | sys.exit(1) 268 | args = parser.parse_args() 269 | commands[args.command](args) 270 | 271 | 272 | if __name__ == '__main__': 273 | main() 274 | -------------------------------------------------------------------------------- /frontend/css/styles.css: -------------------------------------------------------------------------------- 1 | .news { 2 | height: 216px; 3 | word-wrap: break-word; 4 | overflow: auto; 5 | } 6 | 7 | .news-priv { 8 | color: red; 9 | font-weight: bold; 10 | } 11 | 12 | .card-content { 13 | height: 145px; 14 | } 15 | 16 | .card .card-content { 17 | padding: 10px; 18 | } 19 | 20 | .card .card-action { 21 | padding: 10px 10px 1px 10px; 22 | } 23 | 24 | .card .row { 25 | margin: 0 0 10px 0; 26 | } 27 | 28 | .clickable { 29 | cursor: pointer; 30 | } 31 | 32 | .btn-small { 33 | width: 30px; 34 | height: 30px; 35 | } 36 | 37 | .icon-header { 38 | font-size: 20px !important; 39 | line-height: 30px !important; 40 | } 41 | 42 | .input-header input { 43 | font-weight: bold; 44 | font-size: 15px !important; 45 | padding-left: 0px !important; 46 | width: 150px; 47 | } 48 | 49 | .team-selected { 50 | background-color: #ccc !important; 51 | } 52 | 53 | .blue-grey.darken-1.is-solved { 54 | background-color: green !important; 55 | } 56 | 57 | .country-flag { 58 | width: 25px; 59 | padding: 2px; 60 | } 61 | 62 | .categories { 63 | text-align: center; 64 | } 65 | 66 | .categories span.badge { 67 | float: initial; 68 | padding: 3px 6px; 69 | cursor: pointer; 70 | } -------------------------------------------------------------------------------- /frontend/js/app.js: -------------------------------------------------------------------------------- 1 | Vue.use(VueI18n); 2 | if (!Cookies.get('lang')) { 3 | Cookies.set('lang', 'En'); 4 | } 5 | const routes = [ 6 | { 7 | path: '/', 8 | component: Home 9 | }, 10 | { 11 | path: '/challenges', 12 | component: Challenges 13 | }, 14 | { 15 | path: '/rank', 16 | component: Rank 17 | }, 18 | { 19 | path: '/team/:name', 20 | component: Team 21 | }, 22 | { 23 | path: '/settings', 24 | component: Settings 25 | } 26 | ]; 27 | 28 | const router = new VueRouter({ 29 | routes 30 | }); 31 | 32 | const Title = Vue.component('app-title', { 33 | template: ` 34 |
35 |
36 |

{{$t(title)}}

37 |
38 |
39 | `, 40 | props: ['title'] 41 | }) 42 | 43 | const app = new Vue({ 44 | router, 45 | data: () => ({ 46 | loaded: false 47 | }), 48 | async mounted() { 49 | settings = await getSettings(); 50 | this.loaded = true; 51 | }, 52 | i18n: new VueI18n({ 53 | locale: Cookies.get('lang'), 54 | fallbackLocale: 'En', 55 | messages: { 56 | En: enLocale, 57 | Pt: ptLocale 58 | } 59 | }) 60 | }).$mount('#app'); 61 | -------------------------------------------------------------------------------- /frontend/js/challenges.js: -------------------------------------------------------------------------------- 1 | const ChallengeModal = Vue.component('challenge-modal', { 2 | template: ` 3 | 16 | `, 17 | props: ['challenge'], 18 | data: () => ({ 19 | loaded: false, 20 | descriptionMap: {} 21 | }), 22 | methods: { 23 | loadDescription: async function(challenge) { 24 | const lang = Cookies.get('lang').toLowerCase(); 25 | 26 | if (!this.descriptionMap[lang]) { 27 | this.descriptionMap[lang] = {}; 28 | } 29 | 30 | if (this.descriptionMap[lang][challenge.id]) { 31 | this.challenge.description = this.descriptionMap[lang][challenge.id]; 32 | return; 33 | } 34 | 35 | const challengeMd = await getChallengeDescription(this.challenge.id, lang); 36 | this.descriptionMap[lang][challenge.id] = converter.makeHtml(challengeMd); 37 | this.challenge.description = this.descriptionMap[lang][challenge.id]; 38 | }, 39 | }, 40 | mounted: async function() { 41 | $('.modal').modal(); 42 | this.loadDescription(this.challenge); 43 | }, 44 | watch: { 45 | challenge: function(challenge) { 46 | thisloaded = false 47 | this.loadDescription(challenge); 48 | } 49 | }, 50 | }) 51 | 52 | const ChallengeComponent = Vue.component('challenge-card', { 53 | template: ` 54 |
55 |
56 |
57 | {{challenge.title}} 58 |
{{$t('total-solves')}}: {{challenge.solves}}
59 |
{{$t('score')}}: {{challenge.points}}
60 |
61 |
62 |
63 | {{$t(challenge.optional)}} 64 | {{tag}} 65 |
66 |
67 |
68 |
69 | `, 70 | props: ['challenge', 'selectChallengeFunction'], 71 | methods: { 72 | selectChallenge: function() { 73 | this.selectChallengeFunction(this.challenge); 74 | } 75 | } 76 | }); 77 | 78 | const Challenges = Vue.component('challenges', { 79 | template: ` 80 |
81 | 82 |
83 | {{category}} 84 |
85 |
86 | 87 |
88 |
89 | 90 |
91 |
92 | `, 93 | data: () => ({ 94 | loaded: false, 95 | challenges: [], 96 | selectedChallenge: null, 97 | categories: [], 98 | filteredChallenges: [], 99 | selectedCategory: null 100 | }), 101 | props: ['hideTitle', 'submissions'], 102 | watch: { 103 | submissions: function(submissions) { 104 | this.loadSubmissions(submissions); 105 | } 106 | }, 107 | methods: { 108 | selectCategory: function(category) { 109 | if (this.selectedCategory === category) { 110 | this.selectedCategory = null; 111 | this.filteredChallenges = this.challenges; 112 | return; 113 | } 114 | 115 | this.selectedCategory = category; 116 | this.filteredChallenges = this.challenges.filter(challenge => challenge.tags.indexOf(category) >= 0); 117 | }, 118 | loadChallenges: async function(challengeList) { 119 | const mountChallPromise = (challId, index) => getChallenge(challId) 120 | .then(chall => this.challenges.splice(index, 0, chall)); 121 | 122 | const challPromiseMap = challList => challList 123 | .filter(chall => this.challenges.map(c => c.id).indexOf(chall) < 0) 124 | .map(mountChallPromise); 125 | 126 | await Promise.all(challPromiseMap(challengeList)); 127 | const categories = new Set(); 128 | this.challenges.forEach(challenge => { 129 | challenge.tags.forEach(category => { 130 | categories.add(category); 131 | }); 132 | }) 133 | this.categories = [...categories]; 134 | this.challenges = this.challenges.sort((challA, challB) => challA.title.localeCompare(challB.title)) 135 | this.filteredChallenges = this.challenges; 136 | 137 | if (!this.submissions && !this.submissionsPolling.isStarted) { 138 | this.submissionsPolling.start(); 139 | } 140 | this.loaded = true 141 | }, 142 | loadSubmissions: function(acceptedSubmissions) { 143 | const userTeam = Cookies.get('team'); 144 | let teamSolves = new Set(); 145 | solves = acceptedSubmissions.standings.reduce((reducer, { taskStats, team }) => { 146 | Object.keys(taskStats).forEach(chall => { 147 | reducer[chall]++ || (reducer[chall] = 1) 148 | }); 149 | 150 | if (userTeam && userTeam === team) { 151 | teamSolves = new Set(Object.keys(taskStats)); 152 | } 153 | return reducer; 154 | }, {}); 155 | 156 | this.challenges.forEach((challenge, index) => { 157 | this.challenges.splice(index, 1, Object.assign({}, challenge, { 158 | solves: solves[challenge.id] || 0, 159 | points: this.calculatePoints(solves[challenge.id]), 160 | solved: teamSolves.has(challenge.id) 161 | })); 162 | }); 163 | }, 164 | calculatePoints: function(solves) { 165 | const { K, V, minpts, maxpts } = settings['dynamic_scoring']; 166 | return parseInt(Math.max(minpts, Math.floor(maxpts - K * Math.log2(((solves + 1 || 1) + V)/(1 + V))))) 167 | }, 168 | openModal: function(challenge) { 169 | this.selectedChallenge = challenge; 170 | Vue.nextTick(() => { 171 | $('.modal').modal('open'); 172 | }) 173 | } 174 | }, 175 | mounted: async function() { 176 | this.loadChallenges(await getChallenges()); 177 | this.submissionsPolling = createPooling( 178 | getSolvedChallenges, 179 | this.loadSubmissions 180 | ); 181 | title = 'Challenges'; 182 | }, 183 | beforeDestroy: function() { 184 | this.challengesPolling.stop(); 185 | this.submissionsPolling.stop(); 186 | } 187 | }); 188 | -------------------------------------------------------------------------------- /frontend/js/functions.js: -------------------------------------------------------------------------------- 1 | const createPooling = (promise, cb, intervalTime) => { 2 | let interval; 3 | return { 4 | isStarted: false, 5 | async start() { 6 | if (this.isStarted) { 7 | return; 8 | } 9 | 10 | this.isStarted = true; 11 | 12 | cb(await promise()); 13 | interval = setInterval(async () => { 14 | cb(await promise()); 15 | }, intervalTime || 60000); 16 | }, 17 | stop () { 18 | this.isStarted = false 19 | clearInterval(interval); 20 | } 21 | } 22 | }; 23 | 24 | const converter = new showdown.Converter(); 25 | const getSubmisionsPath = () => settings.submissions_project.split('/')[1]; 26 | const getTeamPath = teamName => sha256(teamName).splice(1, 0, '/').splice(5, 0, '/'); 27 | const mountUrl = (path, time = (1000 * 60 * 10)) => `${path}?_${Math.floor(+(new Date)/time)}` 28 | 29 | const getSettings = () => $.getJSON('settings.json'); 30 | const getNews = () => $.getJSON(mountUrl(`/${getSubmisionsPath()}/news.json`)); 31 | const getChallenges = () => $.getJSON(mountUrl('challenges/index.json')); 32 | const getChallenge = id => $.getJSON(mountUrl(`challenges/${id}.json`)); 33 | const getChallengeDescription = (id, lang) => $.get(mountUrl(`challenges/${id}.${lang.toLowerCase()}.md`)); 34 | const getSolvedChallenges = () => $.getJSON(mountUrl(`/${getSubmisionsPath()}/accepted-submissions.json`, 1000 * 60)); 35 | const getChart = () => mountUrl("https://cloud.ufscar.br:8080/v1/AUTH_c93b694078064b4f81afd2266a502511/charts/top.svg", 1000 * 60); 36 | const getTeam = teamName => $.getJSON(mountUrl(`/${getSubmisionsPath()}/${getTeamPath(teamName)}/team.json`)); 37 | const getTeamMembers = teamName => $.getJSON(mountUrl(`/${getSubmisionsPath()}/${getTeamPath(teamName)}/members.json`)); 38 | const getLocaleMessages = lang => $.getJSON(mountUrl(`frontend/locales/${lang}.json`)); 39 | 40 | String.prototype.splice = function(idx, rem, str) { 41 | return this.slice(0, idx) + str + this.slice(idx + Math.abs(rem)); 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/js/home.js: -------------------------------------------------------------------------------- 1 | const News = Vue.component('news', { 2 | template: ` 3 |
4 | 18 |
19 |
20 | [ {{formatDate(singleNews.time)}} ] admin: {{formatNews(singleNews)}} 21 | [ {{formatDate(singleNews.time)}} ] admin: {{$t("private-message")}} 22 |
23 |
24 |
25 |
[ {{formatDate(solve.time)}} ] {{solve.team}} {{$t("solved")}} {{solve.chall}}
26 |
27 |
28 | `, 29 | data: () => ({ 30 | title: "Home", 31 | news: [], 32 | solves: [] 33 | }), 34 | methods: { 35 | formatDate: date => moment(date, "X").format('DD-MM-YYYY HH:mm:ss'), 36 | formatNews: function(msg) { 37 | if (msg.to) { 38 | return 39 | } 40 | 41 | return msg.msg 42 | 43 | }, 44 | loadNews: function(news) { 45 | this.news = news.filter(msg => { 46 | console.log(!msg.to || (msg.to && msg.to === Cookies.get('team'))); 47 | return !msg.to || (msg.to && msg.to === Cookies.get('team')) 48 | }) 49 | .sort((msgA, msgB) => msgB.time - msgA.time); 50 | }, 51 | setChallengesSolves: function(acceptedSubmissions) { 52 | this.solves = acceptedSubmissions.standings.reduce((reducer, { taskStats, team }) => { 53 | Object.keys(taskStats).forEach(chall => { 54 | reducer.push({ 55 | team, 56 | chall, 57 | time: taskStats[chall].time 58 | }); 59 | }); 60 | return reducer; 61 | }, []) 62 | .sort((solveA, solveB) => solveB.time - solveA.time); 63 | } 64 | }, 65 | mounted: function() { 66 | $('ul.tabs').tabs(); 67 | this.newsPolling = createPooling( 68 | getNews, 69 | this.loadNews 70 | ); 71 | this.newsPolling.start(); 72 | 73 | this.submissionsPolling = createPooling( 74 | getSolvedChallenges, 75 | this.setChallengesSolves 76 | ); 77 | this.submissionsPolling.start(); 78 | }, 79 | beforeDestroy: function() { 80 | this.newsPolling.stop(); 81 | this.submissionsPolling.stop(); 82 | } 83 | }); 84 | 85 | const Home = { 86 | template: ` 87 |
88 | 89 |
90 |
91 |
{{$t('news')}}
92 | 93 |
94 |
95 |
{{$t('rank')}}
96 | 97 |
98 |
99 |
100 |
101 |
{{$t('challenges')}}
102 | 103 |
104 |
105 |
106 | `, 107 | mounted: () => { 108 | title = "Home"; 109 | }, 110 | 111 | } 112 | -------------------------------------------------------------------------------- /frontend/js/rank.js: -------------------------------------------------------------------------------- 1 | const Rank = Vue.component('rank', { 2 | template: ` 3 |
4 | 5 |
6 | Chart 7 |
8 |
    9 |
  • 10 |
    {{team.pos}}. {{team.team}} 11 |
    {{team.score}}
    12 |
    13 | 14 |
    15 |
    16 |
  • 17 |
18 |
19 | `, 20 | data: () => ({ 21 | rank: [], 22 | teams: {}, 23 | userTeam: Cookies.get('team') 24 | }), 25 | methods: { 26 | getTeamFlagUrl: country => `https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/${settings.flag_icon_css_ver || '2.8.0'}/flags/4x3/${country}.svg`, 27 | loadChart: function(data) { 28 | $('#chart').attr('src', data); 29 | }, 30 | loadTeam: async function(teamName) { 31 | if (!this.teams[teamName]) { 32 | this.teams[teamName] = await getTeam(teamName); 33 | } 34 | 35 | return this.teams[teamName]; 36 | }, 37 | loadRank: function(acceptedSubmissions) { 38 | this.rank = acceptedSubmissions.standings.filter((team, i) => i < (this.limit || acceptedSubmissions.standings.length)); 39 | this.rank.forEach(async (rank, index) => { 40 | this.rank.splice(index, 1, Object.assign({}, rank, { 41 | countries: (await this.loadTeam(rank.team)).countries 42 | })); 43 | }) 44 | }, 45 | teamClick: function(teamName) { 46 | this.$router.push({ path: `/team/${teamName}` }); 47 | } 48 | }, 49 | props: ['limit', 'hideTitle'], 50 | mounted: function() { 51 | this.rankPolling = createPooling( 52 | getSolvedChallenges, 53 | this.loadRank 54 | ); 55 | this.rankPolling.start(); 56 | this.chartPolling = createPooling( 57 | getChart, 58 | this.loadChart 59 | ); 60 | this.chartPolling.start(); 61 | }, 62 | beforeDestroy: function() { 63 | this.rankPolling.stop(); 64 | this.chartPolling.stop(); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /frontend/js/services.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwn2winctf/2018/5329e25e97bf834b5eadd0fb7858e654538b4f88/frontend/js/services.js -------------------------------------------------------------------------------- /frontend/js/settings.js: -------------------------------------------------------------------------------- 1 | const Settings = Vue.component('settings', { 2 | template: ` 3 |
4 | 5 |

{{$t('team')}}:

6 |

7 | {{$t('language')}}: 8 | 12 |

13 | Save 14 |
15 | `, 16 | data: () => ({ 17 | team: Cookies.get('team'), 18 | language: Cookies.get('lang') || 'En' 19 | }), 20 | methods: { 21 | save() { 22 | const lang = $('select').val(); 23 | Cookies.set('team', this.team); 24 | Cookies.set('lang', lang); 25 | app.$i18n.locale = lang; 26 | } 27 | }, 28 | mounted() { 29 | $(document).ready(function() { 30 | $('select').material_select(); 31 | }); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /frontend/js/story.js: -------------------------------------------------------------------------------- 1 | const ChallengeComponent = Vue.component('challenge-card', { 2 | template: ` 3 |
4 |
5 |
6 | {{challenge.title}} 7 |

{{challenge.description.substr(0,100)}}...

8 |

9 | {{tag}} 10 |

11 |
12 |
13 | More 14 |
15 |
16 |
17 | `, 18 | props: ['challenge'] 19 | }); 20 | 21 | const Challenges = { 22 | template: ` 23 |
24 | 25 |
26 | `, 27 | data: () => ({ 28 | challenges 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /frontend/js/team.js: -------------------------------------------------------------------------------- 1 | const Team = Vue.component('team', { 2 | template: ` 3 |
4 |
5 |

{{team.name}}

6 |

{{$t('position')}}: {{teamRank.pos}}

7 |

{{$t('score')}}: {{teamRank.score}}

8 |

Crypt PK: {{team.crypt_pk}}

9 |

Sign PK: {{team.sign_pk}}

10 |
{{$t('members')}}
11 |
    12 |
  • 13 | {{index + 1}}. {{member.username}} 14 |
  • 15 |
16 |
{{$t('solved-challenges')}}
17 |
    18 |
  • 19 | {{index + 1}}. {{key}} 20 |
  • 21 |
22 |
23 |
24 |

404 - Team "{{$route.params.name}}" not found

25 |
26 |
27 | `, 28 | data: () => ({ 29 | loaded: false 30 | }), 31 | methods: { 32 | loadTeam: async function(teamName) { 33 | return getTeam(teamName); 34 | }, 35 | loadMembers: async function(teamName) { 36 | return getTeamMembers(teamName); 37 | }, 38 | loadTeamRank: async function(teamName) { 39 | const solvedChallenges = await getSolvedChallenges() 40 | return solvedChallenges.standings.filter(teamRank => teamRank.team === teamName)[0]; 41 | } 42 | }, 43 | mounted: async function() { 44 | await Promise.all([ 45 | this.loadTeamRank(this.$route.params.name) 46 | .then(teamRank => { this.teamRank = teamRank }), 47 | this.loadTeam(this.$route.params.name) 48 | .then(team => { this.team = team }), 49 | this.loadMembers(this.$route.params.name) 50 | .then(members => { this.members = members }) 51 | ]); 52 | this.loaded = true; 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /frontend/locales/en.js: -------------------------------------------------------------------------------- 1 | const enLocale = { 2 | 'home': 'Home', 3 | 'challenges': 'Challenges', 4 | 'rank': 'Rank', 5 | 'rules': 'Rules', 6 | 'settings': 'Settings', 7 | 'news': 'News', 8 | 'messages': 'Messages', 9 | 'solves': 'Solves', 10 | 'private-message': 'Your team has a new private msg, use console to read', 11 | 'solved': 'solved', 12 | 'total-solves': 'Total solves', 13 | 'score': 'Score', 14 | 'categories': 'Categories', 15 | 'isolated': 'Isolated', 16 | 'platform': 'Platform', 17 | 'position': 'Position', 18 | 'members': 'Members', 19 | 'solved-challenges': 'Solved Challenges', 20 | 'team': 'Team', 21 | 'language': 'Language' 22 | } 23 | -------------------------------------------------------------------------------- /frontend/locales/pt.js: -------------------------------------------------------------------------------- 1 | const ptLocale = { 2 | 'home': 'Home', 3 | 'challenges': 'Desafios', 4 | 'rank': 'Rank', 5 | 'rules': 'Regras', 6 | 'settings': 'Configurações', 7 | 'news': 'Notícias', 8 | 'messages': 'Mensagens', 9 | 'solves': 'Resolvidos', 10 | 'private-message': 'Seu time tem uma nova mensagem, use o console para lê-la.', 11 | 'solved': 'resolveu', 12 | 'total-solves': 'Total de soluções', 13 | 'score': 'Pontos', 14 | 'categories': 'Categorias', 15 | 'isolated': 'Isolado', 16 | 'platform': 'Plataforma', 17 | 'position': 'Posição', 18 | 'members': 'Membros', 19 | 'solved-challenges': 'Desafios Resolvidos', 20 | 'team': 'Time', 21 | 'language': 'Língua' 22 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 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 | -------------------------------------------------------------------------------- /known_hosts: -------------------------------------------------------------------------------- 1 | github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== 2 | gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY= 3 | gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9 4 | gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf 5 | -------------------------------------------------------------------------------- /lambda_function.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | from nizkctf.settings import Settings 6 | from nizkctf.subrepo import SubRepo 7 | from nizkctf.repohost import RepoHost 8 | from nizkctf.proposal import consider_proposal 9 | from nizkctf.six import to_bytes 10 | import os 11 | import json 12 | import base64 13 | import tempfile 14 | import traceback 15 | 16 | 17 | def run(merge_info): 18 | SubRepo.set_clone_into(tempfile.mkdtemp()) 19 | 20 | # Prepare git and ssh for usage inside the container 21 | setup_environment() 22 | 23 | # Merge proposal if changes are valid 24 | consider_proposal(merge_info) 25 | 26 | 27 | def handle_payload(payload, context): 28 | merge_info = RepoHost.webhook.adapt_payload(payload) 29 | 30 | if not merge_info: 31 | # Message not of our interest (e.g. merge request closed) 32 | return 33 | 34 | try: 35 | run(merge_info) 36 | except: 37 | # Do not re-raise, we do not want automatic retries 38 | traceback.print_exc() 39 | # Send tracking number to the user 40 | send_cloudwatch_info(merge_info, context) 41 | 42 | 43 | def send_cloudwatch_info(merge_info, context): 44 | proj = Settings.submissions_project 45 | mr_id = merge_info['mr_id'] 46 | 47 | comment = "Sorry. A failure has occurred when processing your proposal. " \ 48 | "Please contact support and present the following info:\n\n" \ 49 | "**Stream name**: %s\n" \ 50 | "**Request ID**: %s\n" % \ 51 | (context.log_stream_name, context.aws_request_id) 52 | 53 | repohost = RepoHost.instance() 54 | repohost.mr_comment(proj, mr_id, comment) 55 | repohost.mr_close(proj, mr_id) 56 | 57 | 58 | def handle_apigw(event, context): 59 | headers = event['params']['header'] 60 | raw_payload = event['body'] 61 | 62 | # autenticate the message 63 | secret = to_bytes(os.getenv('WEBHOOK_SECRET_TOKEN')) 64 | RepoHost.webhook.auth(secret, headers, to_bytes(raw_payload)) 65 | 66 | payload = json.loads(raw_payload) 67 | return handle_payload(payload, context) 68 | 69 | 70 | def handle_sns(event, context): 71 | raw_payload = event['Records'][0]['Sns']['Message'] 72 | payload = json.loads(raw_payload) 73 | 74 | # no way to authenticate, but also no need to 75 | # (publishing to the SNS topic should already be authenticated) 76 | 77 | return handle_payload(payload, context) 78 | 79 | 80 | def setup_environment(): 81 | root = os.getenv('LAMBDA_TASK_ROOT') 82 | bin_dir = os.path.join(root, 'bin') 83 | os.environ['PATH'] += ':' + bin_dir 84 | os.environ['GIT_EXEC_PATH'] = bin_dir 85 | 86 | ssh_dir = tempfile.mkdtemp() 87 | 88 | ssh_identity = os.path.join(ssh_dir, 'identity') 89 | with os.fdopen(os.open(ssh_identity, os.O_WRONLY | os.O_CREAT, 0o600), 90 | 'w') as f: 91 | f.write(base64.b64decode(os.getenv('SSH_IDENTITY'))) 92 | 93 | ssh_config = os.path.join(ssh_dir, 'config') 94 | with open(ssh_config, 'w') as f: 95 | f.write('CheckHostIP no\n' 96 | 'StrictHostKeyChecking yes\n' 97 | 'IdentityFile %s\n' 98 | 'UserKnownHostsFile %s\n' % 99 | (ssh_identity, os.path.join(root, 'known_hosts'))) 100 | 101 | os.environ['GIT_SSH_COMMAND'] = 'ssh -F %s' % ssh_config 102 | 103 | 104 | def lambda_handler(event, context): 105 | if 'Records' in event: 106 | return handle_sns(event, context) 107 | elif 'body' in event: 108 | return handle_apigw(event, context) 109 | raise ValueError("Did not recognize a valid event originated by SNS nor " 110 | "by API Gateway. Did you configure it correctly?") 111 | -------------------------------------------------------------------------------- /nizkctf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwn2winctf/2018/5329e25e97bf834b5eadd0fb7858e654538b4f88/nizkctf/__init__.py -------------------------------------------------------------------------------- /nizkctf/acceptedsubmissions.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import time 7 | from .six import text_type 8 | from .subrepo import SubRepo 9 | from .serializable import SerializableDict 10 | from .scoring import compute_points 11 | 12 | 13 | ACCEPTED_SUBMISSIONS_FILE = 'accepted-submissions.json' 14 | 15 | 16 | class AcceptedSubmissions(SerializableDict): 17 | pretty_print = True 18 | 19 | def __init__(self): 20 | super(AcceptedSubmissions, self).__init__() 21 | self.setdefault('tasks', []) 22 | self.setdefault('standings', []) 23 | 24 | def path(self): 25 | return SubRepo.get_path(ACCEPTED_SUBMISSIONS_FILE) 26 | 27 | def get_team_standing(self, team_name): 28 | for team_standing in self['standings']: 29 | if team_standing['team'] == team_name: 30 | return team_standing 31 | 32 | team_standing = {'team': team_name, 33 | 'taskStats': {}, 34 | 'score': 0} 35 | self['standings'].append(team_standing) 36 | return team_standing 37 | 38 | def get_solves(self, chall_id): 39 | solves = set() 40 | for team_standing in self['standings']: 41 | if chall_id in team_standing['taskStats']: 42 | solves.add(team_standing['team']) 43 | return solves 44 | 45 | def compute_points(self, chall, additional_solves=0): 46 | num_solves = len(self.get_solves(chall.id)) + additional_solves 47 | return compute_points(chall, num_solves) 48 | 49 | def recompute_score(self, chall): 50 | points = self.compute_points(chall) 51 | chall_id = chall.id 52 | # update affected team's standings 53 | for team_standing in self['standings']: 54 | task_stats = team_standing['taskStats'] 55 | if chall_id in task_stats: 56 | task_stats[chall_id]['points'] = points 57 | team_standing['score'] = sum(task['points'] 58 | for task in task_stats.values()) 59 | 60 | def rank(self): 61 | standings = self['standings'] 62 | standings.sort(key=lambda standing: (standing['score'], 63 | -standing['lastAccept']), 64 | reverse=True) 65 | for i, standing in enumerate(standings): 66 | standing['pos'] = i + 1 67 | 68 | def add(self, chall, team): 69 | chall_id = chall.id 70 | team_name = team['name'] 71 | 72 | if chall_id not in self['tasks']: 73 | self['tasks'].append(chall_id) 74 | 75 | team_standing = self.get_team_standing(team_name) 76 | 77 | if chall_id in team_standing['taskStats']: 78 | # Challenge already submitted by team 79 | return 80 | 81 | accepted_time = int(time.time()) 82 | team_standing['taskStats'][chall_id] = {'points': 0, 83 | 'time': accepted_time} 84 | team_standing['lastAccept'] = accepted_time 85 | 86 | self.recompute_score(chall) 87 | self.rank() 88 | self.save() 89 | -------------------------------------------------------------------------------- /nizkctf/challenge.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | from base64 import b64decode 6 | import os 7 | import re 8 | import json 9 | import codecs 10 | import pysodium 11 | from .serializable import SerializableDict, SerializableList 12 | from .settings import Settings 13 | 14 | 15 | CHALLENGES_DIR = 'challenges' 16 | INDEX_FILE = 'index.json' 17 | 18 | thisdir = os.path.dirname(os.path.realpath(__file__)) 19 | chall_dir = os.path.realpath(os.path.join(thisdir, os.pardir, CHALLENGES_DIR)) 20 | 21 | 22 | class Challenge(SerializableDict): 23 | def __init__(self, id): 24 | self.validate_id(id) 25 | self.id = id 26 | super(Challenge, self).__init__() 27 | 28 | def __eq__(self, other): 29 | return self.id == other.id 30 | 31 | def __hash__(self): 32 | return hash(self.id) 33 | 34 | def path(self): 35 | return os.path.join(chall_dir, self.id + '.json') 36 | 37 | def description(self, lang='en'): 38 | # Get from a localized markdown file 39 | path = os.path.join(chall_dir, '{}.{}.md'.format(self.id, lang)) 40 | if os.path.exists(path): 41 | with codecs.open(path, encoding='utf-8') as f: 42 | return f.read() 43 | # Get from the json 44 | return self['description'] 45 | 46 | @staticmethod 47 | def validate_id(id): 48 | if len(id) > Settings.max_size_chall_id or \ 49 | not re.match(r'^[a-zA-Z0-9-_]+$', id): 50 | raise ValueError('invalid challenge ID') 51 | 52 | @staticmethod 53 | def _binary_field(k): 54 | return k in {'salt', 'pk'} 55 | 56 | @staticmethod 57 | def index(): 58 | return ChallengeIndex() 59 | 60 | 61 | class ChallengeIndex(SerializableList): 62 | def path(self): 63 | return os.path.join(chall_dir, INDEX_FILE) 64 | 65 | 66 | def derive_keypair(salt, flag): 67 | flag = flag.encode('utf-8') 68 | assert isinstance(salt, bytes) 69 | assert isinstance(flag, bytes) 70 | assert len(salt) == pysodium.crypto_pwhash_scryptsalsa208sha256_SALTBYTES 71 | 72 | chall_seed = pysodium.crypto_pwhash_scryptsalsa208sha256( 73 | pysodium.crypto_sign_SEEDBYTES, 74 | flag, 75 | salt, 76 | Settings.scrypt_ops_limit, 77 | Settings.scrypt_mem_limit) 78 | 79 | return pysodium.crypto_sign_seed_keypair(chall_seed) 80 | 81 | 82 | def random_salt(): 83 | return pysodium.randombytes( 84 | pysodium.crypto_pwhash_scryptsalsa208sha256_SALTBYTES) 85 | 86 | 87 | def lookup_flag(flag, chall_id=None): 88 | if chall_id: 89 | # challenge provided, only try it 90 | try_challenges = [Challenge(chall_id)] 91 | if not try_challenges[0].exists(): 92 | raise ValueError("A challenge named '%s' does not exist." % 93 | chall_id) 94 | else: 95 | # try every challenge 96 | try_challenges = [Challenge(id) for id in Challenge.index()] 97 | 98 | try_salts = set(chall['salt'] for chall in try_challenges) 99 | pk_chall = {chall['pk']: chall for chall in try_challenges} 100 | 101 | for salt in try_salts: 102 | pk, sk = derive_keypair(salt, flag) 103 | match = pk_chall.get(pk) 104 | 105 | if match: 106 | return match, sk 107 | 108 | return None, None 109 | -------------------------------------------------------------------------------- /nizkctf/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwn2winctf/2018/5329e25e97bf834b5eadd0fb7858e654538b4f88/nizkctf/cli/__init__.py -------------------------------------------------------------------------------- /nizkctf/cli/challenges.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import codecs 7 | import json 8 | import pysodium 9 | import base64 10 | import textwrap 11 | 12 | from . import log 13 | from .teamsecrets import TeamSecrets 14 | from ..team import my_team 15 | from ..proof import proof_create 16 | from ..challenge import Challenge, lookup_flag 17 | from ..subrepo import SubRepo 18 | from ..acceptedsubmissions import AcceptedSubmissions 19 | 20 | 21 | def submit_flag(flag, chall_id=None): 22 | chall, chall_sk = lookup_flag(flag, chall_id) 23 | 24 | if chall_sk is None: 25 | return False, 'This is not the correct flag.' 26 | 27 | SubRepo.pull() 28 | 29 | submissions = my_team().submissions() 30 | if chall in submissions.challs(): 31 | return False, 'Your team already solved %s.' % chall.id 32 | 33 | proof = proof_create(chall.id, chall_sk) 34 | submissions.submit(proof) 35 | SubRepo.push(commit_message='Proof: found flag for %s' % chall.id) 36 | 37 | return True, 'Congratulations! You found the right flag for %s.' % chall.id 38 | 39 | 40 | LINE_WIDTH = 72 41 | 42 | 43 | def pprint(): 44 | print('') 45 | print('-'*LINE_WIDTH) 46 | print('') 47 | submissions = AcceptedSubmissions() 48 | for chall_id in Challenge.index(): 49 | chall = Challenge(chall_id) 50 | print('ID: %s (%d points) [%s]' % ( 51 | chall.id, 52 | submissions.compute_points(chall, additional_solves=1), 53 | ', '.join(chall['tags']))) 54 | print('') 55 | print(chall['title']) 56 | print('') 57 | print('\n'.join(textwrap.wrap(chall.description(), 58 | LINE_WIDTH, 59 | replace_whitespace=False, 60 | drop_whitespace=False, 61 | break_on_hyphens=False))) 62 | print('') 63 | print('-'*LINE_WIDTH) 64 | print('') 65 | -------------------------------------------------------------------------------- /nizkctf/cli/localserver.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | try: 7 | from BaseHTTPServer import HTTPServer 8 | from SimpleHTTPServer import SimpleHTTPRequestHandler 9 | except ImportError: 10 | from http.server import SimpleHTTPRequestHandler, HTTPServer 11 | from ..settings import Settings 12 | from ..subrepo import SubRepo 13 | from .teamsecrets import TeamSecrets 14 | from ..localsettings import LocalSettings 15 | 16 | 17 | def handler(routes, root_redir=None, forbidden=set()): 18 | forbidden = set(os.path.realpath(path) for path in forbidden) 19 | 20 | class RequestHandler(SimpleHTTPRequestHandler): 21 | protocol_version = 'HTTP/1.0' 22 | 23 | def do_GET(self): 24 | if root_redir and self.path == '/': 25 | self.send_response(301) 26 | self.send_header("Location", root_redir) 27 | self.end_headers() 28 | return 29 | SimpleHTTPRequestHandler.do_GET(self) 30 | 31 | def translate_path(self, path): 32 | path = path.split('?', 1)[0] 33 | path = path.split('#', 1)[0] 34 | 35 | root = None 36 | for url_prefix, cur_root in routes: 37 | if path.startswith(url_prefix): 38 | root = cur_root 39 | path = path[len(url_prefix):] 40 | break 41 | if not root: 42 | return '' 43 | 44 | os.chdir(root) 45 | path = SimpleHTTPRequestHandler.translate_path(self, path) 46 | if path in forbidden: 47 | return '' 48 | return path 49 | 50 | return RequestHandler 51 | 52 | 53 | def main(port=8000): 54 | thisdir = os.path.dirname(os.path.realpath(__file__)) 55 | rootdir = os.path.realpath(os.path.join(thisdir, os.pardir, os.pardir)) 56 | subdir = SubRepo.get_path() 57 | 58 | ctf = os.path.basename(rootdir) 59 | submissions = os.path.basename(Settings.submissions_project) 60 | 61 | routes = [ 62 | ('/%s' % ctf, rootdir), 63 | ('/%s' % submissions, subdir), 64 | ] 65 | 66 | forbidden = { 67 | LocalSettings.path(), 68 | TeamSecrets.path(), 69 | } 70 | 71 | HandlerClass = handler(routes, '/%s' % ctf, forbidden) 72 | 73 | server_address = ('localhost', port) 74 | httpd = HTTPServer(server_address, HandlerClass) 75 | sa = httpd.socket.getsockname() 76 | print("Serving HTTP on", sa[0], "port", sa[1], "...") 77 | httpd.serve_forever() 78 | -------------------------------------------------------------------------------- /nizkctf/cli/log.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | 6 | 7 | def info(s): 8 | print('\033[93m[*]\033[00m %s' % s) 9 | 10 | 11 | def success(s): 12 | print('\033[92m[+]\033[00m %s' % s) 13 | 14 | 15 | def fail(s): 16 | print('\033[91m[!]\033[00m %s' % s) 17 | -------------------------------------------------------------------------------- /nizkctf/cli/news.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import sys 7 | import time 8 | import json 9 | import pysodium 10 | from base64 import b64decode 11 | from ..news import News 12 | from ..team import my_team 13 | from ..text import width 14 | 15 | from .teamsecrets import TeamSecrets 16 | from ..team import Team 17 | from ..proposal import retry_push 18 | from ..six import to_unicode 19 | 20 | 21 | TIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S' 22 | 23 | 24 | def submit(msg_text, to=None): 25 | for _ in retry_push('Added news to %s' % (to or 'all')): 26 | News().add(msg_text, to) 27 | 28 | 29 | def pprint(news, team_only): 30 | ''' 31 | Pretty print news in terminal. 32 | 33 | Args: 34 | news (list): List of news. 35 | team_only (bool): whether to display only team directed news or not. 36 | ''' 37 | 38 | if len(news) == 0: 39 | print('No news yet.') 40 | return 41 | 42 | team = my_team() 43 | 44 | def decrypt_news(news_item): 45 | assert('to' in news_item) 46 | 47 | # Message was sent to a team, so we need to decode and decrypt it 48 | decoded_msg = b64decode(news_item['msg'].encode("utf-8")) 49 | try: 50 | team_pk, team_sk = team['crypt_pk'], TeamSecrets['crypt_sk'] 51 | decrypted_msg = pysodium.crypto_box_seal_open(decoded_msg, 52 | team_pk, 53 | team_sk) 54 | except: 55 | decrypted_msg = b'' 56 | 57 | news_item['msg'] = decrypted_msg.decode("utf-8") 58 | return news_item 59 | 60 | def filter_news(news): 61 | # Filter items based on team_only flag, applying decryptionif needed 62 | to_filter = [team['name']] + ([] if team_only else [None]) 63 | for news_item in news: 64 | to = news_item.get('to') 65 | if to in to_filter: 66 | yield decrypt_news(news_item) if to else news_item 67 | 68 | news = filter_news(news) 69 | 70 | to_len = max(width(team['name']), 10) 71 | 72 | # FIXME test formatting 73 | time_len = msg_len = 20 74 | 75 | def hyph(n): 76 | return '-'*(n + 2) 77 | 78 | sep = hyph(time_len) + '+' + hyph(to_len) + '+' + hyph(msg_len) 79 | 80 | def fmtcol(s, n): 81 | return ' ' + s + ' '*(n - width(s) + 1) 82 | 83 | def fmt(time, to, msg): 84 | return fmtcol(time, time_len) + '|' + \ 85 | fmtcol(to, to_len) + '|' + \ 86 | fmtcol(msg, msg_len) 87 | 88 | def fmtime(timestamp): 89 | return to_unicode(time.strftime(TIME_DISPLAY_FORMAT, 90 | time.localtime(timestamp))) 91 | 92 | print('') 93 | print(sep) 94 | print(fmt('Date', 'To', 'Message')) 95 | print(sep) 96 | 97 | for news_item in news: 98 | print(fmt(fmtime(news_item['time']), 99 | news_item.get('to', to_unicode('all')), news_item['msg'])) 100 | 101 | print(sep) 102 | print('') 103 | -------------------------------------------------------------------------------- /nizkctf/cli/scoreboard.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import sys 6 | import json 7 | import operator 8 | import tempfile 9 | import subprocess 10 | import codecs 11 | from ..team import Team 12 | from ..text import width 13 | from ..six import viewitems 14 | from ..acceptedsubmissions import AcceptedSubmissions 15 | 16 | 17 | def rank(): 18 | ''' 19 | Compute ranking given a submissions file. 20 | 21 | Args: 22 | f (file): File-like object with the submissions json. 23 | 24 | Returns: 25 | List containing the computed ranking for the submission list. 26 | The result is in the following format: 27 | 28 | (team, score) 29 | 30 | And a map containing submissions sorted by team. 31 | ''' 32 | 33 | submissions = {} 34 | scores = [] 35 | for team in AcceptedSubmissions()['standings']: 36 | team_id = Team(name=team['team']).id 37 | submissions[team_id] = [sub for sub in team['taskStats'].values()] 38 | submissions[team_id] = sorted(submissions[team_id], 39 | key=operator.itemgetter('time')) 40 | scores.append((team_id, team['score'])) 41 | 42 | return (scores, submissions) 43 | 44 | 45 | def pprint(ranking, top=0, show_names=False): 46 | ''' 47 | Pretty print scoreboard in terminal. 48 | 49 | Args: 50 | ranking (list): List of tuples containing teams and scores. 51 | top (int): Number of teams to show in scoreboard. 52 | 53 | ''' 54 | 55 | if top == 0: 56 | top = len(ranking) 57 | 58 | if len(ranking) == 0: 59 | print('Nobody scored yet.') 60 | return 61 | 62 | ranking = ranking[:top] 63 | 64 | if show_names: 65 | ranking = [(Team(id=team)['name'], score) for team, score in ranking] 66 | 67 | team_len = max(width(team) for team, score in ranking) 68 | team_len = max(team_len, 10) 69 | 70 | pos_len = score_len = 6 71 | 72 | def hyph(n): 73 | return '-'*(n + 2) 74 | 75 | sep = hyph(pos_len) + '+' + hyph(team_len) + '+' + hyph(score_len) 76 | 77 | def fmtcol(s, n): 78 | return ' ' + s + ' '*(n - width(s) + 1) 79 | 80 | def fmt(pos, team, score): 81 | return fmtcol(pos, pos_len) + '|' + \ 82 | fmtcol(team, team_len) + '|' + \ 83 | fmtcol(score, score_len) 84 | 85 | print('') 86 | print(sep) 87 | print(fmt('Pos', 'Team', 'Score')) 88 | print(sep) 89 | 90 | for idx, (team, score) in enumerate(ranking): 91 | pos = '%d' % (idx + 1) 92 | print(fmt(pos, team, '%d' % score)) 93 | 94 | print(sep) 95 | print('') 96 | 97 | 98 | def plot(ranking, submissions, top=3): 99 | ''' 100 | Plot points for top teams. 101 | 102 | Args: 103 | ranking (list): List containing teams and scores sorted in 104 | descending order. 105 | submissions (dict): Dict [team] -> submission list. 106 | top (int): Number of teams to appear in chart. 107 | ''' 108 | if len(ranking) == 0: 109 | return 110 | 111 | # generate temporary files with data points 112 | fnames = [] 113 | for team, _ in ranking[0:top]: 114 | f = tempfile.NamedTemporaryFile(suffix='.dat', 115 | prefix='nizkctf-', delete=True) 116 | w = codecs.getwriter('utf-8')(f) 117 | partial = 0 118 | for subm in submissions[team]: 119 | partial += subm['points'] 120 | w.write('%s, %d\n' % (subm['time'], partial)) 121 | w.flush() 122 | fnames.append((team, f)) 123 | 124 | # generate gnuplot file 125 | f = tempfile.NamedTemporaryFile(suffix='.gp', 126 | prefix='nizkctf-', delete=True) 127 | w = codecs.getwriter('utf-8')(f) 128 | w.write('set terminal dumb 120 30\n') 129 | w.write('set xdata time\n') 130 | w.write('set datafile sep \',\'\n') 131 | w.write('set timefmt "%s"\n') 132 | w.write('set style data steps\n') 133 | w.write('plot ') 134 | fmt = '\'%s\' using 1:2 title \'%s\'' 135 | w.write(fmt % (fnames[0][1].name, fnames[0][0])) 136 | for team, ft in fnames[1:]: 137 | w.write(', ') 138 | w.write(fmt % (ft.name, team)) 139 | w.flush() 140 | 141 | # plot in terminal 142 | p = subprocess.Popen(['gnuplot', f.name], 143 | stderr=sys.stderr, 144 | stdout=sys.stdout) 145 | p.wait() 146 | 147 | # close/delete files 148 | f.close() 149 | for nm, f in fnames: 150 | f.close() 151 | -------------------------------------------------------------------------------- /nizkctf/cli/team.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import hashlib 6 | import os 7 | import json 8 | import codecs 9 | import pysodium 10 | import base64 11 | from . import log 12 | from .teamsecrets import write_team_secrets 13 | from ..team import Team 14 | from ..subrepo import SubRepo 15 | from ..six import to_unicode 16 | 17 | 18 | def register(team_name, countries): 19 | team_name = to_unicode(team_name) 20 | 21 | log.info('updating subrepo') 22 | SubRepo.pull() 23 | 24 | log.info('registering new team: %s' % team_name) 25 | team = Team(name=team_name) 26 | 27 | if team.exists(): 28 | log.fail('team is already registered') 29 | return False 30 | 31 | log.info('generating encryption keypair') 32 | crypt_pk, crypt_sk = pysodium.crypto_box_keypair() 33 | 34 | log.info('generating signature keypair') 35 | sign_pk, sign_sk = pysodium.crypto_sign_keypair() 36 | 37 | team.update({'countries': countries, 38 | 'crypt_pk': crypt_pk, 39 | 'sign_pk': sign_pk}) 40 | team.validate() 41 | team.save() 42 | 43 | SubRepo.push(commit_message='Register team %s' % team_name) 44 | log.success('team %s added successfully' % team_name) 45 | 46 | write_team_secrets(team.id, crypt_sk, sign_sk) 47 | 48 | return True 49 | -------------------------------------------------------------------------------- /nizkctf/cli/teamsecrets.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import shutil 7 | import time 8 | from . import log 9 | from ..serializable import SerializableDict 10 | 11 | 12 | TEAMSECRETS_FILE = 'team-secrets.json' 13 | 14 | 15 | class DefaultTeamSecrets(SerializableDict): 16 | def path(self): 17 | thisdir = os.path.dirname(os.path.realpath(__file__)) 18 | return os.path.join(thisdir, os.pardir, os.pardir, TEAMSECRETS_FILE) 19 | 20 | @staticmethod 21 | def _binary_field(k): 22 | return k.endswith('_sk') 23 | 24 | 25 | TeamSecrets = DefaultTeamSecrets() 26 | 27 | 28 | def write_team_secrets(team_id, crypt_sk, sign_sk): 29 | if TeamSecrets.exists(): 30 | log.info('overriding %s' % TEAMSECRETS_FILE) 31 | 32 | backup = TeamSecrets.path() + time.strftime('.%Y-%m-%d-%H-%M-%S') 33 | shutil.copy(TeamSecrets.path(), backup) 34 | 35 | TeamSecrets['id'] = team_id 36 | TeamSecrets['crypt_sk'] = crypt_sk 37 | TeamSecrets['sign_sk'] = sign_sk 38 | TeamSecrets.save() 39 | 40 | log.success('ready, share %s with your team!' % TEAMSECRETS_FILE) 41 | -------------------------------------------------------------------------------- /nizkctf/gen_iso3166: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals, division, print_function,\ 5 | absolute_import 6 | 7 | from nizkctf.settings import Settings 8 | import os 9 | import json 10 | import string 11 | import requests 12 | 13 | 14 | def iter_country_codes(): 15 | r = requests.get('https://api.github.com/repos/lipis/flag-icon-css' 16 | '/contents/flags/1x1?ref=%s' % Settings.flag_icon_css_ver) 17 | r.raise_for_status() 18 | for item in r.json(): 19 | if item['type'] == 'file': 20 | basename, ext = os.path.splitext(item['name']) 21 | if ext == '.svg': 22 | yield basename 23 | 24 | 25 | def gen_code(): 26 | allowed_chars = set(string.ascii_lowercase).union(set('-')) 27 | 28 | py = 'valid_countries = set([' 29 | i = 0 30 | 31 | for country_code in iter_country_codes(): 32 | if i % 8 == 0: 33 | py += '\n ' 34 | 35 | assert set(country_code).issubset(allowed_chars) 36 | py += " '%s'," % country_code 37 | 38 | i += 1 39 | 40 | py += '\n])' 41 | return py 42 | 43 | 44 | def main(): 45 | print(gen_code()) 46 | 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /nizkctf/iso3166.py: -------------------------------------------------------------------------------- 1 | valid_countries = set([ 2 | 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'ao', 3 | 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 4 | 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 5 | 'bj', 'bl', 'bm', 'bn', 'bo', 'bq', 'br', 'bs', 6 | 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 7 | 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 8 | 'co', 'cr', 'cu', 'cv', 'cw', 'cx', 'cy', 'cz', 9 | 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 10 | 'eg', 'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 11 | 'fk', 'fm', 'fo', 'fr', 'ga', 'gb-eng', 'gb-nir', 'gb-sct', 12 | 'gb-wls', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 13 | 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 14 | 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 15 | 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 16 | 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 17 | 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 18 | 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 19 | 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 20 | 'me', 'mf', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 21 | 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 22 | 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 23 | 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 24 | 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 25 | 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 26 | 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 27 | 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 28 | 'sm', 'sn', 'so', 'sr', 'ss', 'st', 'sv', 'sx', 29 | 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 30 | 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 31 | 'tw', 'tz', 'ua', 'ug', 'um', 'un', 'us', 'uy', 32 | 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 33 | 'wf', 'ws', 'ye', 'yt', 'za', 'zm', 'zw', 34 | ]) 35 | -------------------------------------------------------------------------------- /nizkctf/localsettings.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import json 7 | import threading 8 | 9 | 10 | LOCALSETTINGS_FILE = 'local-settings.json' 11 | 12 | 13 | class DefaultLocalSettings(object): 14 | __lock__ = threading.Lock() 15 | 16 | def path(self): 17 | thisdir = os.path.dirname(os.path.realpath(__file__)) 18 | return os.path.join(thisdir, os.pardir, LOCALSETTINGS_FILE) 19 | 20 | def __init__(self): 21 | if os.path.exists(self.path()): 22 | with open(self.path()) as f: 23 | self.__dict__.update(json.load(f)) 24 | 25 | def __setattr__(self, k, v): 26 | self.__dict__[k] = v 27 | with self.__lock__: 28 | with open(self.path(), 'w') as f: 29 | json.dump(self.__dict__, f) 30 | 31 | 32 | LocalSettings = DefaultLocalSettings() 33 | -------------------------------------------------------------------------------- /nizkctf/news.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import time 7 | import pysodium 8 | from base64 import b64encode 9 | from .team import Team 10 | from .six import text_type 11 | from .subrepo import SubRepo 12 | from .serializable import SerializableList 13 | 14 | 15 | NEWS_FILE = 'news.json' 16 | 17 | 18 | class News(SerializableList): 19 | pretty_print = True 20 | 21 | def __init__(self): 22 | super(News, self).__init__() 23 | 24 | def path(self): 25 | return SubRepo.get_path(NEWS_FILE) 26 | 27 | def add(self, msg_text, to=None): 28 | current_time = int(time.time()) 29 | if to is None: 30 | message = {"msg": msg_text, "time": current_time} 31 | else: 32 | team_pk = Team(name=to)['crypt_pk'] 33 | encrypted_msg = pysodium.crypto_box_seal(msg_text.encode("utf-8"), 34 | team_pk) 35 | encoded_msg = b64encode(encrypted_msg) 36 | 37 | message = {"msg": encoded_msg.decode("utf-8"), 38 | "to": to, 39 | "time": current_time} 40 | 41 | self.append(message) 42 | self.save() 43 | -------------------------------------------------------------------------------- /nizkctf/proof.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import pysodium 7 | from base64 import b64encode, b64decode 8 | from .six import text_type 9 | from .challenge import Challenge 10 | from .cli.teamsecrets import TeamSecrets 11 | 12 | 13 | def proof_open(team, proof): 14 | assert isinstance(proof, bytes) 15 | proof = b64decode(proof) 16 | 17 | claimed_chall_id = proof[2*64:].decode('utf-8') 18 | claimed_chall = Challenge(claimed_chall_id) 19 | 20 | chall_pk = claimed_chall['pk'] 21 | team_pk = team['sign_pk'] 22 | 23 | membership_proof = pysodium.crypto_sign_open(proof, chall_pk) 24 | chall_id = pysodium.crypto_sign_open(membership_proof, 25 | team_pk).decode('utf-8') 26 | 27 | if claimed_chall_id != chall_id: 28 | raise ValueError('invalid proof') 29 | 30 | return claimed_chall 31 | 32 | 33 | def proof_create(chall_id, chall_sk): 34 | chall_id = chall_id.encode('utf-8') 35 | 36 | assert isinstance(chall_id, bytes) 37 | assert isinstance(chall_sk, bytes) 38 | 39 | team_sk = TeamSecrets['sign_sk'] 40 | 41 | membership_proof = pysodium.crypto_sign(chall_id, team_sk) 42 | proof = pysodium.crypto_sign(membership_proof, chall_sk) 43 | 44 | return b64encode(proof) 45 | -------------------------------------------------------------------------------- /nizkctf/proposal.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import re 7 | import time 8 | import random 9 | import subprocess 10 | from .settings import Settings 11 | from .repohost import RepoHost 12 | from .subrepo import SubRepo 13 | from .team import Team, TEAM_FILE, SUBMISSIONS_FILE 14 | from .acceptedsubmissions import AcceptedSubmissions 15 | 16 | 17 | DIFF_MAX_SIZE = 5000 18 | PUSH_RETRIES = 10 19 | SLEEP_FACTOR = 0.2 20 | 21 | 22 | def consider_proposal(merge_info): 23 | # Clone official repository 24 | SubRepo.clone(fork=False) 25 | 26 | # Set CI user name / email 27 | setup_user_name_and_email() 28 | 29 | # Fetch proposal 30 | add_proposal_remote(merge_info) 31 | 32 | # Get commits between which to compute diffs 33 | commit = merge_info['source_commit'] 34 | merge_base = get_merge_base(commit) 35 | 36 | # Check if there is a single commit in the merge request 37 | check_rev_count(merge_base, commit) 38 | # Check if the diff is not too big 39 | check_diff_size(merge_base, commit) 40 | # Check if only allowed ops were done (add/modify) 41 | check_no_unallowed_ops(merge_base, commit) 42 | # Validate and get files added/modified 43 | added_file = get_added_file(merge_base, commit) 44 | modified_file = get_modified_file(merge_base, commit) 45 | 46 | if added_file and modified_file: 47 | raise ValueError("We only allow commits doing a single operation") 48 | 49 | changed_file = added_file or modified_file 50 | if not changed_file: 51 | raise ValueError("You managed to make a commit which does nothing") 52 | changed_basename = os.path.basename(changed_file) 53 | 54 | if added_file and changed_basename == TEAM_FILE: 55 | team_registration(merge_info, added_file) 56 | elif changed_file and changed_basename == SUBMISSIONS_FILE: 57 | flag_submission(merge_info, changed_file) 58 | else: 59 | raise ValueError("unrecognized operation") 60 | 61 | 62 | def team_registration(merge_info, added_file): 63 | # Checkout first to get the new team file 64 | commit = merge_info['source_commit'] 65 | checkout(commit) 66 | 67 | team = filename_owner(added_file) 68 | team.validate() 69 | 70 | def local_changes(): 71 | # Back to branch, do local modifications 72 | checkout('master') 73 | add_member(team, merge_info) 74 | 75 | local_changes() # Validate local modifications before accepting MR 76 | accept_proposal(merge_info) 77 | 78 | for _ in retry_push('Add member who registered team'): 79 | local_changes() 80 | 81 | 82 | def flag_submission(merge_info, modified_file): 83 | team = filename_owner(modified_file) 84 | challs_before = set(team.submissions().challs()) 85 | 86 | # Checkout to get the newly submitted challenge 87 | commit = merge_info['source_commit'] 88 | checkout(commit) 89 | 90 | challs_after = set(team.submissions().challs()) 91 | 92 | new_challs = challs_after - challs_before 93 | assert len(new_challs) == 1 94 | chall, = new_challs 95 | 96 | def local_changes(): 97 | # Back to branch, do local modifications 98 | checkout('master') 99 | add_member(team, merge_info) 100 | AcceptedSubmissions().add(chall, team) 101 | 102 | local_changes() # Validate local modifications before accepting MR 103 | accept_proposal(merge_info) 104 | 105 | for _ in retry_push('Accept challenge solution'): 106 | local_changes() 107 | 108 | 109 | def add_member(team, merge_info): 110 | team_dir = team.dir() 111 | if not os.path.exists(team_dir): 112 | os.makedirs(team_dir) 113 | 114 | team.members().add(id=merge_info['user_id'], 115 | username=merge_info['username']) 116 | 117 | 118 | def accept_proposal(merge_info, retries=PUSH_RETRIES): 119 | proj = Settings.submissions_project 120 | mr_id = merge_info['mr_id'] 121 | commit = merge_info['source_commit'] 122 | 123 | repohost = RepoHost.instance() 124 | for retry in range(1, retries + 1): 125 | try: 126 | repohost.mr_accept(proj, mr_id, commit) 127 | break 128 | except: 129 | time.sleep(SLEEP_FACTOR * retry * random.random()) 130 | if retry == retries: 131 | raise 132 | 133 | 134 | def retry_push(commit_message, retries=PUSH_RETRIES): 135 | for retry in range(1, retries + 1): 136 | try: 137 | checkout('master') 138 | SubRepo.git(['reset', '--hard', 'upstream/master']) 139 | SubRepo.pull() 140 | yield retry # do local modifications 141 | SubRepo.push(commit_message, merge_request=False) 142 | break 143 | except: 144 | time.sleep(SLEEP_FACTOR * retry * random.random()) 145 | if retry == retries: 146 | raise 147 | 148 | 149 | def filename_owner(filename): 150 | team_id, basename = os.path.split(filename) 151 | return Team(id=team_id) 152 | 153 | 154 | def add_proposal_remote(merge_info): 155 | url = merge_info['source_ssh_url'] 156 | SubRepo.git(['remote', 'add', 'proposal', url]) 157 | SubRepo.git(['fetch', '--all']) 158 | 159 | 160 | def setup_user_name_and_email(): 161 | ci_user_name = os.getenv('CI_USER_NAME') 162 | ci_user_email = os.getenv('CI_USER_EMAIL') 163 | if ci_user_name: 164 | SubRepo.git(['config', 'user.name', ci_user_name]) 165 | if ci_user_email: 166 | SubRepo.git(['config', 'user.email', ci_user_email]) 167 | 168 | 169 | def checkout(commit): 170 | SubRepo.git(['checkout', commit]) 171 | 172 | 173 | def get_added_file(src, dest): 174 | return get_file(src, dest, 'A', {TEAM_FILE, SUBMISSIONS_FILE}) 175 | 176 | 177 | def get_modified_file(src, dest): 178 | return get_file(src, dest, 'M', {SUBMISSIONS_FILE}) 179 | 180 | 181 | def get_file(src, dest, filt, whitelist): 182 | stats = diff_stats(src, dest, ['--diff-filter=' + filt]) 183 | if len(stats) == 0: 184 | return None 185 | if len(stats) != 1: 186 | raise ValueError("We only allow a single file to be added or modified " 187 | "per commit") 188 | 189 | stat, = stats 190 | lines_added, lines_removed, filename = stat 191 | if lines_removed != 0: 192 | raise ValueError("We do not allow lines to be removed from files") 193 | if lines_added != 1: 194 | raise ValueError("Changes can only add a single line to a file") 195 | 196 | check_whitelist(filename, whitelist) 197 | return filename 198 | 199 | 200 | def check_no_unallowed_ops(src, dest): 201 | stats = diff_stats(src, dest, ['--diff-filter=am']) 202 | if len(stats) != 0: 203 | raise ValueError("We only allow files to be added or modified") 204 | 205 | 206 | def check_whitelist(filename, whitelist): 207 | basename = os.path.basename(filename) 208 | if basename not in whitelist: 209 | raise ValueError("Filename '%s' not in the whitelist" % basename) 210 | 211 | 212 | def diff_stats(src, dest, args=[]): 213 | stats = SubRepo.git(['diff', '--numstat'] + args + [src, dest], 214 | stdout=subprocess.PIPE) 215 | lines = [re.split(r'\s+', line.strip(), 2) for line in 216 | stats.split('\n')] 217 | lines = [line for line in lines if line != ['']] 218 | return [(int(lines_added), int(lines_removed), filename) 219 | for lines_added, lines_removed, filename 220 | in lines] 221 | 222 | 223 | def check_rev_count(src, dest): 224 | revs = int(SubRepo.git(['rev-list', '--count', src+'...'+dest], 225 | stdout=subprocess.PIPE).strip()) 226 | 227 | if revs != 1: 228 | raise ValueError("We only accept a single commit per merge request") 229 | 230 | 231 | def check_diff_size(src, dest): 232 | diff = SubRepo.git(['diff', '--no-color', '-U0', src, dest], 233 | stdout=subprocess.PIPE) 234 | 235 | if len(diff) > DIFF_MAX_SIZE: 236 | raise ValueError("Diff size (%d bytes) is above the maximum permitted " 237 | "(%d bytes)" % (len(diff), DIFF_MAX_SIZE)) 238 | 239 | 240 | def get_merge_base(commit): 241 | merge_base = SubRepo.git(['merge-base', 'upstream/master', commit], 242 | stdout=subprocess.PIPE).strip() 243 | return merge_base 244 | -------------------------------------------------------------------------------- /nizkctf/repohost/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | from .github import GitHub 6 | from .gitlab import GitLab 7 | 8 | from ..settings import Settings 9 | RepoHost = globals()[Settings.repository_host] 10 | -------------------------------------------------------------------------------- /nizkctf/repohost/common.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import requests 7 | import urllib 8 | import codecs 9 | from ..localsettings import LocalSettings 10 | 11 | 12 | class BaseRepoHost(object): 13 | @classmethod 14 | def login(cls, username=None, password=None, token=None, OTP=None): 15 | if not token and (username and password): 16 | token = cls.get_token(username, password, OTP) 17 | if not token: 18 | raise ValueError("Pass either a token or an username/password") 19 | LocalSettings.token = token 20 | 21 | @classmethod 22 | def instance(cls): 23 | token = os.getenv('REPOHOST_TOKEN') or \ 24 | LocalSettings.token 25 | return cls(token) 26 | 27 | def __init__(self, token): 28 | self.token = token 29 | self.s = requests.Session() 30 | self._init_session() 31 | 32 | def _init_session(self): 33 | pass 34 | 35 | @classmethod 36 | def get_token(cls, username, password, OTP): 37 | pass 38 | 39 | @staticmethod 40 | def _raise_for_status(r): 41 | try: 42 | r.raise_for_status() 43 | except Exception as e: 44 | raise APIError(r.text, e) 45 | 46 | 47 | class APIError(Exception): 48 | pass 49 | 50 | 51 | class WebhookAuthError(Exception): 52 | pass 53 | 54 | 55 | quote_plus = urllib.quote_plus if hasattr(urllib, 'quote_plus') else \ 56 | urllib.parse.quote_plus 57 | -------------------------------------------------------------------------------- /nizkctf/repohost/github.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import requests 6 | import hashlib 7 | import hmac 8 | from ..settings import Settings 9 | from ..six import to_bytes 10 | from .common import BaseRepoHost, APIError, WebhookAuthError 11 | 12 | 13 | class GitHubWebhook(object): 14 | @staticmethod 15 | def auth(secret, headers, raw_payload): 16 | received_sig = to_bytes(headers['X-Hub-Signature']) 17 | 18 | h = hmac.new(secret, raw_payload, hashlib.sha1).hexdigest() 19 | correct_sig = b'sha1=' + to_bytes(h) 20 | 21 | if not hmac.compare_digest(received_sig, correct_sig): 22 | raise WebhookAuthError() 23 | 24 | @staticmethod 25 | def adapt_payload(payload): 26 | # filtering 27 | if 'pull_request' not in payload: 28 | return None 29 | if payload['action'] not in {'opened', 'reopened'}: 30 | return None 31 | if payload['pull_request']['base']['repo']['full_name'] != \ 32 | Settings.submissions_project: 33 | return None 34 | if payload['pull_request']['base']['ref'] != 'master': 35 | return None 36 | # mappings 37 | return {"mr_id": payload['pull_request']['number'], 38 | "source_ssh_url": payload['pull_request']['head']['repo'] 39 | ['ssh_url'], 40 | "source_commit": payload['pull_request']['head']['sha'], 41 | "user_id": payload['pull_request']['user']['id'], 42 | "username": payload['pull_request']['user']['login']} 43 | 44 | 45 | class GitHub(BaseRepoHost): 46 | webhook = GitHubWebhook 47 | 48 | @classmethod 49 | def get_token(cls, username, password, OTP): 50 | authorization = {'scopes': 'public_repo', 51 | 'note': Settings.ctf_name} 52 | if OTP is None: 53 | headers = None 54 | else: 55 | headers = {'X-GitHub-OTP': OTP} 56 | 57 | r = requests.post(Settings.github_api_endpoint + 58 | 'authorizations', 59 | json=authorization, 60 | auth=(username, password), 61 | headers=headers) 62 | 63 | data = r.json() 64 | 65 | try: 66 | if cls._has_error(data, 'already_exists'): 67 | raise APIError 68 | if cls._has_error(data, 'Bad credentials'): 69 | raise BadCreds("Bad Credentials: Please certify that your login and password are correct") 70 | except APIError: 71 | print("API Error: Please visit https://github.com/settings/tokens " 72 | "and make sure you do not already have a personal " 73 | "access token called '%s'" % Settings.ctf_name) 74 | #except BadCreds: 75 | # print("Bad Credentials: Please certify that your login and password are correct") 76 | 77 | cls._raise_for_status(r) 78 | 79 | return data['token'] 80 | 81 | @staticmethod 82 | def get_ssh_url(proj): 83 | return Settings.github_ssh_url % proj 84 | 85 | def fork(self, source): 86 | r = self.s.post(Settings.github_api_endpoint + 87 | 'repos/' + source + '/forks') 88 | self._raise_for_status(r) 89 | data = r.json() 90 | return data['full_name'], data['ssh_url'] 91 | 92 | def merge_request(self, source, target, 93 | source_branch='master', 94 | target_branch='master', 95 | title='Pull Request'): 96 | source_branch = source.split('/', 2)[0] + ':' + source_branch 97 | 98 | pull_request = {'head': source_branch, 99 | 'base': target_branch, 100 | 'title': title} 101 | 102 | r = self.s.post(Settings.github_api_endpoint + 103 | 'repos/' + target + '/pulls', 104 | json=pull_request) 105 | self._raise_for_status(r) 106 | return r.json() 107 | 108 | def mr_comment(self, proj, mr_id, contents): 109 | r = self.s.post(Settings.github_api_endpoint + 110 | 'repos/' + proj + '/issues/%d' % mr_id + '/comments', 111 | json={'body': contents}) 112 | self._raise_for_status(r) 113 | return r.json() 114 | 115 | def mr_close(self, proj, mr_id): 116 | r = self.s.patch(Settings.github_api_endpoint + 117 | 'repos/' + proj + '/pulls/%d' % mr_id, 118 | json={'state': 'closed'}) 119 | self._raise_for_status(r) 120 | return r.json() 121 | 122 | def mr_accept(self, proj, mr_id, sha): 123 | r = self.s.put(Settings.github_api_endpoint + 124 | 'repos/' + proj + '/pulls/%d' % mr_id + '/merge', 125 | json={'sha': sha}) 126 | self._raise_for_status(r) 127 | return r.json() 128 | 129 | def _init_session(self): 130 | self.s.headers.update({'Authorization': 'token ' + self.token}) 131 | 132 | @staticmethod 133 | def _has_error(data, err_code): 134 | return any(err.get('code') == err_code 135 | for err in data.get('errors', [])) 136 | -------------------------------------------------------------------------------- /nizkctf/repohost/gitlab.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import requests 6 | import hmac 7 | import re 8 | from ..settings import Settings 9 | from ..six import to_bytes 10 | from .common import BaseRepoHost, APIError, WebhookAuthError, quote_plus 11 | 12 | 13 | class GitLabWebhook(object): 14 | @staticmethod 15 | def auth(secret, headers, raw_payload): 16 | received_token = to_bytes(headers['X-Gitlab-Token']) 17 | if not hmac.compare_digest(secret, received_token): 18 | raise WebhookAuthError() 19 | 20 | @staticmethod 21 | def adapt_payload(payload): 22 | # filtering 23 | if payload['object_kind'] != 'merge_request': 24 | return None 25 | if payload['object_attributes']['action'] not in {'open', 'reopen'}: 26 | return None 27 | if payload['object_attributes']['target']['path_with_namespace'] != \ 28 | Settings.submissions_project: 29 | return None 30 | if payload['object_attributes']['target_branch'] != 'master': 31 | return None 32 | # mappings 33 | return {"mr_id": payload['object_attributes']['id'], 34 | "source_ssh_url": payload['object_attributes']['source'] 35 | ['git_ssh_url'], 36 | "source_commit": payload['object_attributes']['last_commit'] 37 | ['id'], 38 | "user_id": payload['object_attributes']['author_id'], 39 | "username": payload['user']['username']} 40 | 41 | 42 | class GitLab(BaseRepoHost): 43 | webhook = GitLabWebhook 44 | 45 | @classmethod 46 | def get_token(cls, username, password): 47 | auth = {'login': username, 48 | 'password': password} 49 | 50 | r = requests.post(Settings.gitlab_api_endpoint + 51 | 'session', 52 | json=auth) 53 | cls._raise_for_status(r) 54 | 55 | data = r.json() 56 | return data['private_token'] 57 | 58 | @staticmethod 59 | def get_ssh_url(proj): 60 | return Settings.gitlab_ssh_url % proj 61 | 62 | def fork(self, source): 63 | r = self.s.post(Settings.gitlab_api_endpoint + 64 | 'projects/fork/' + quote_plus(source)) 65 | 66 | data = r.json() 67 | if self._has_error(data, 'name', 'has already been taken'): 68 | # Simulate GitHub API behaviour (return already existing fork) 69 | username = self._get_user_namespace() 70 | sink = username + '/' + source.split('/', 2)[1] 71 | sink_proj = self._get_project(sink) 72 | forked_from = sink_proj.get('forked_from_project', {})\ 73 | .get('path_with_namespace') 74 | if forked_from != source: 75 | raise APIError("Project '%s' already exists and is not a fork " 76 | "from '%s'. Please remove or rename it to " 77 | "allow a fork to be made." % (sink, source)) 78 | data = sink_proj 79 | else: 80 | self._raise_for_status(r) 81 | 82 | return data['path_with_namespace'], data['ssh_url_to_repo'] 83 | 84 | def merge_request(self, source, target, 85 | source_branch='master', 86 | target_branch='master', 87 | title='Merge Request'): 88 | target_id = self._get_project(target)['id'] 89 | 90 | merge_request = {'source_branch': source_branch, 91 | 'target_branch': target_branch, 92 | 'title': title, 93 | 'target_project_id': target_id} 94 | 95 | r = self.s.post(Settings.gitlab_api_endpoint + 96 | 'projects/' + quote_plus(source) + '/merge_requests', 97 | json=merge_request) 98 | self._raise_for_status(r) 99 | return r.json() 100 | 101 | def mr_comment(self, proj, mr_id, contents): 102 | r = self.s.post(Settings.gitlab_api_endpoint + 103 | 'projects/' + quote_plus(proj) + 104 | '/merge_requests/%d' % mr_id + '/notes', 105 | json={'body': contents}) 106 | self._raise_for_status(r) 107 | return r.json() 108 | 109 | def mr_close(self, proj, mr_id): 110 | r = self.s.put(Settings.gitlab_api_endpoint + 111 | 'projects/' + quote_plus(proj) + 112 | '/merge_requests/%d' % mr_id, 113 | json={'state_event': 'close'}) 114 | self._raise_for_status(r) 115 | return r.json() 116 | 117 | def mr_accept(self, proj, mr_id, sha): 118 | r = self.s.put(Settings.gitlab_api_endpoint + 119 | 'projects/' + quote_plus(proj) + 120 | '/merge_requests/%d' % mr_id + '/merge', 121 | json={'sha': sha}) 122 | self._raise_for_status(r) 123 | return r.json() 124 | 125 | def _get_user_namespace(self): 126 | return (n['path'] for n in self._get_namespaces() 127 | if n['kind'] == 'user').next() 128 | 129 | def _get_namespaces(self): 130 | r = self.s.get(Settings.gitlab_api_endpoint + 131 | 'namespaces') 132 | self._raise_for_status(r) 133 | return r.json() 134 | 135 | def _get_project(self, proj): 136 | r = self.s.get(Settings.gitlab_api_endpoint + 137 | 'projects/' + quote_plus(proj)) 138 | self._raise_for_status(r) 139 | return r.json() 140 | 141 | def _init_session(self): 142 | self.s.headers.update({'PRIVATE-TOKEN': self.token}) 143 | 144 | @staticmethod 145 | def _has_error(data, key, msg): 146 | return msg in data.get('message', {}).get(key, []) 147 | -------------------------------------------------------------------------------- /nizkctf/scoring.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from .settings import Settings 4 | from math import floor, log 5 | 6 | 7 | def compute_points(chall, num_solves): 8 | num_solves = max(1, num_solves) 9 | 10 | params = Settings.dynamic_scoring 11 | if not params: 12 | return chall['points'] 13 | 14 | # Google CTF 2017's formula 15 | K, V, minpts, maxpts = params['K'], params['V'], \ 16 | params['minpts'], params['maxpts'] 17 | return int(max(minpts, floor(maxpts - K*log((num_solves + V)/(1 + V), 2)))) 18 | -------------------------------------------------------------------------------- /nizkctf/serializable.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import json 7 | from base64 import b64encode, b64decode 8 | from .six import viewitems, text_type 9 | 10 | 11 | class Serializable(object): 12 | pretty_print = False 13 | 14 | def __init__(self): 15 | self.load() 16 | 17 | def load(self): 18 | if self.exists(): 19 | self.clear() 20 | with open(self.path()) as f: 21 | self.update(json.load(f)) 22 | self._unserialize_inplace() 23 | 24 | def save(self): 25 | with open(self.path(), 'w') as f: 26 | kw = {'sort_keys': True, 'indent': 1} if self.pretty_print else {} 27 | json.dump(self._serialize(), f, **kw) 28 | 29 | def exists(self): 30 | return os.path.exists(self.path()) 31 | 32 | def _unserialize_inplace(self): 33 | pass 34 | 35 | def _serialize(self): 36 | return self 37 | 38 | 39 | class SerializableDict(Serializable, dict): 40 | @staticmethod 41 | def _binary_field(k): 42 | return False 43 | 44 | def _unserialize_inplace(self): 45 | for k, v in viewitems(self): 46 | if self._binary_field(k): 47 | assert isinstance(v, text_type) 48 | self[k] = b64decode(v) 49 | 50 | def _serialize(self): 51 | return {k: b64encode(v).decode('utf-8') if self._binary_field(k) else v 52 | for k, v in viewitems(self)} 53 | 54 | 55 | class SerializableList(Serializable, list): 56 | def clear(self): 57 | del self[:] 58 | 59 | def update(self, l): 60 | self += l 61 | -------------------------------------------------------------------------------- /nizkctf/settings.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import json 7 | from .six import viewitems 8 | 9 | 10 | class Settings(object): 11 | pass 12 | 13 | 14 | def load(): 15 | """ Load settings from json file """ 16 | thisdir = os.path.dirname(os.path.realpath(__file__)) 17 | with open(os.path.join(thisdir, os.pardir, 'settings.json')) as f: 18 | settings = json.load(f) 19 | assert isinstance(settings, dict) 20 | for k, v in viewitems(settings): 21 | setattr(Settings, k, v) 22 | 23 | 24 | load() 25 | -------------------------------------------------------------------------------- /nizkctf/six.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # a subset of the six module 3 | 4 | from __future__ import unicode_literals, division, print_function,\ 5 | absolute_import 6 | import operator 7 | import sys 8 | 9 | 10 | PY2 = sys.version_info[0] == 2 11 | 12 | text_type = type('') 13 | 14 | 15 | if PY2: 16 | viewitems = operator.methodcaller("viewitems") 17 | input = raw_input 18 | else: 19 | viewitems = operator.methodcaller("items") 20 | input = input 21 | 22 | 23 | def to_bytes(s): 24 | if isinstance(s, text_type): 25 | return bytes(s.encode('utf-8')) 26 | return s 27 | 28 | 29 | def to_unicode(s): 30 | if isinstance(s, text_type): 31 | return s 32 | encoding = sys.getfilesystemencoding() 33 | s = s.decode(encoding) 34 | return s 35 | -------------------------------------------------------------------------------- /nizkctf/subrepo.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import subprocess 7 | import base64 8 | import pysodium 9 | from .settings import Settings 10 | from .localsettings import LocalSettings 11 | from .repohost import RepoHost 12 | 13 | 14 | SUBREPO_NAME = 'submissions' 15 | 16 | 17 | class SubRepo(object): 18 | @classmethod 19 | def set_clone_into(cls, clone_into): 20 | cls.clone_into = clone_into 21 | cls.path = os.path.join(clone_into, SUBREPO_NAME) 22 | 23 | @classmethod 24 | def get_path(cls, subpath=''): 25 | if os.path.exists(cls.path): 26 | return os.path.join(cls.path, subpath) 27 | raise EnvironmentError("The subrepository path ('%s') was not created " 28 | "yet. Please call 'ctf login' to get it cloned " 29 | "before performing any further actions." % 30 | cls.path) 31 | 32 | @classmethod 33 | def clone(cls, fork=True): 34 | repohost = RepoHost.instance() 35 | upstream_url = repohost.get_ssh_url(Settings.submissions_project) 36 | 37 | if fork: 38 | forked_project, origin_url = \ 39 | repohost.fork(Settings.submissions_project) 40 | LocalSettings.forked_project = forked_project 41 | else: 42 | origin_url = upstream_url 43 | 44 | cls.git(['clone', origin_url, SUBREPO_NAME], cwd=cls.clone_into) 45 | cls.git(['remote', 'add', 'upstream', upstream_url]) 46 | 47 | if fork: 48 | cls.git(['remote', 'set-url', 'origin', 49 | repohost.get_ssh_url(forked_project)]) 50 | 51 | @classmethod 52 | def pull(cls): 53 | cls.git(['checkout', 'master']) 54 | cls.git(['pull', '--rebase', 'upstream', 'master']) 55 | 56 | @classmethod 57 | def push(cls, commit_message='commit', merge_request=True): 58 | branch = 'master' 59 | if merge_request: 60 | branch = cls.random_branch() 61 | cls.git(['checkout', '-b', branch, 'master']) 62 | 63 | cls.git(['add', '-A']) 64 | cls.git(['commit', '--no-gpg-sign', '-m', commit_message], 65 | returncodes={0, 1}) # do not fail on 'nothing to commit' 66 | cls.git(['push', '-u', 'origin', branch]) 67 | 68 | if merge_request: 69 | repohost = RepoHost.instance() 70 | repohost.merge_request(LocalSettings.forked_project, 71 | Settings.submissions_project, 72 | source_branch=branch, 73 | title=commit_message) 74 | 75 | @staticmethod 76 | def random_branch(): 77 | return base64.b32encode(pysodium.randombytes(10))\ 78 | .decode('utf-8').lower() 79 | 80 | @classmethod 81 | def git(cls, args, **kwargs): 82 | returncodes = kwargs.pop('returncodes', {0}) 83 | if 'cwd' not in kwargs: 84 | kwargs['cwd'] = cls.get_path() 85 | 86 | p = subprocess.Popen(['git'] + args, **kwargs) 87 | 88 | r = None 89 | if 'stdout' in kwargs: 90 | r = p.stdout.read() 91 | 92 | returncode = p.wait() 93 | if returncode not in returncodes: 94 | raise GitError(returncode) 95 | 96 | return r 97 | 98 | 99 | class GitError(Exception): 100 | def __init__(self, returncode, *args): 101 | self.returncode = returncode 102 | super(GitError, self).__init__(*args) 103 | 104 | 105 | thisdir = os.path.dirname(os.path.realpath(__file__)) 106 | SubRepo.set_clone_into(os.path.realpath(os.path.join(thisdir, os.pardir))) 107 | -------------------------------------------------------------------------------- /nizkctf/team.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import os 6 | import re 7 | import hashlib 8 | import pysodium 9 | from .six import text_type 10 | from .iso3166 import valid_countries 11 | from .settings import Settings 12 | from .subrepo import SubRepo 13 | from .serializable import SerializableDict, SerializableList 14 | from .proof import proof_open 15 | from .cli.teamsecrets import TeamSecrets 16 | 17 | 18 | TEAM_FILE = 'team.json' 19 | MEMBERS_FILE = 'members.json' 20 | SUBMISSIONS_FILE = 'submissions.csv' 21 | 22 | 23 | class Team(SerializableDict): 24 | def __init__(self, name=None, id=None): 25 | if name: 26 | id = self.name_to_id(name) 27 | self.update({'name': name}) 28 | if id: 29 | self.validate_id(id) 30 | self.id = id 31 | else: 32 | raise ValueError('Either name or id are required') 33 | 34 | super(Team, self).__init__() 35 | 36 | if self.exists(): 37 | self.validate() 38 | 39 | def dir(self): 40 | return SubRepo.get_path(self.id) 41 | 42 | def path(self): 43 | return os.path.join(self.dir(), TEAM_FILE) 44 | 45 | def save(self): 46 | if not self.exists(): 47 | os.makedirs(self.dir()) 48 | super(Team, self).save() 49 | 50 | def members(self): 51 | return TeamMembers(self) 52 | 53 | def submissions(self): 54 | return TeamSubmissions(self) 55 | 56 | @staticmethod 57 | def name_to_id(name): 58 | assert isinstance(name, text_type) 59 | sha = hashlib.sha256(name.encode('utf-8')).hexdigest() 60 | return sha[0:1] + '/' + sha[1:4] + '/' + sha[4:] 61 | 62 | @staticmethod 63 | def validate_id(id): 64 | assert isinstance(id, text_type) 65 | if not re.match(r'^[0-9a-f]/[0-9a-f]{3}/[0-9a-f]{60}$', id): 66 | raise ValueError('Invalid Team ID') 67 | 68 | @staticmethod 69 | def _binary_field(k): 70 | return k.endswith('_pk') 71 | 72 | def validate(self): 73 | expected_keys = {'name', 'countries', 'crypt_pk', 'sign_pk'} 74 | if set(self.keys()) != expected_keys: 75 | raise ValueError("Team should contain, and only contain: %s" % 76 | ', '.join(expected_keys)) 77 | 78 | assert isinstance(self['name'], text_type) 79 | if len(self['name']) > Settings.max_size_team_name: 80 | raise ValueError("Team name must have at most %d chars." % 81 | Settings.max_size_team_name) 82 | if self.name_to_id(self['name']) != self.id: 83 | raise ValueError("Team name does not match its ID") 84 | 85 | assert isinstance(self['countries'], list) 86 | if len(self['countries']) > Settings.max_size_team_countries: 87 | raise ValueError("Team must have at most %d countries." % 88 | Settings.max_size_team_countries) 89 | for country in self['countries']: 90 | if country not in valid_countries: 91 | raise ValueError("Must use 2-letter ISO country codes.") 92 | 93 | assert isinstance(self['crypt_pk'], bytes) 94 | if len(self['crypt_pk']) != pysodium.crypto_box_PUBLICKEYBYTES: 95 | raise ValueError("Team's crypt_pk has incorrect size") 96 | 97 | assert isinstance(self['sign_pk'], bytes) 98 | if len(self['sign_pk']) != pysodium.crypto_sign_PUBLICKEYBYTES: 99 | raise ValueError("Team's sign_pk has incorrect size") 100 | 101 | 102 | class TeamMembers(SerializableList): 103 | pretty_print = True 104 | 105 | def __init__(self, team): 106 | self.team = team 107 | self.team_dir = team.dir() 108 | super(TeamMembers, self).__init__() 109 | 110 | def path(self): 111 | return os.path.join(self.team_dir, MEMBERS_FILE) 112 | 113 | def projection(self, attr): 114 | return [member[attr] for member in self] 115 | 116 | def add(self, id=None, username=None): 117 | assert isinstance(id, int) or isinstance(id, long) 118 | assert isinstance(username, text_type) 119 | 120 | another_team = lookup_member(id=id) 121 | if another_team: 122 | if another_team != self.team: 123 | raise ValueError("User '%s' is already member of team '%s'" % 124 | (username, another_team['name'])) 125 | else: 126 | # do nothing, but do not fail if it is the same team 127 | return 128 | 129 | self.append({'id': id, 'username': username}) 130 | self.save() 131 | 132 | 133 | class TeamSubmissions(object): 134 | def __init__(self, team): 135 | self.team = team 136 | self.path = os.path.join(team.dir(), SUBMISSIONS_FILE) 137 | 138 | def submit(self, proof): 139 | assert isinstance(proof, bytes) 140 | with open(self.path, 'ab') as f: 141 | f.write(proof + b'\n') 142 | 143 | def challs(self): 144 | r = [] 145 | if os.path.exists(self.path): 146 | with open(self.path, 'rb') as f: 147 | for proof in f: 148 | r.append(proof_open(self.team, proof.strip())) 149 | if len(set(r)) != len(r): 150 | raise ValueError('Team submissions contain repeated challenges') 151 | return r 152 | 153 | 154 | def my_team(): 155 | return Team(id=TeamSecrets['id']) 156 | 157 | 158 | def all_teams(): 159 | root = SubRepo.get_path() 160 | for path, dirs, files in os.walk(root): 161 | if TEAM_FILE in files: 162 | assert path.startswith(root) 163 | id = path[len(root):].strip('/') 164 | yield Team(id=id) 165 | 166 | 167 | def lookup_member(id=None, username=None): 168 | if id: 169 | attr = 'id' 170 | value = id 171 | elif username: 172 | attr = 'username' 173 | value = username 174 | else: 175 | raise ValueError('Provide either an id or an username') 176 | 177 | for team in all_teams(): 178 | if value in team.members().projection(attr): 179 | return team 180 | 181 | return None 182 | -------------------------------------------------------------------------------- /nizkctf/text.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, division, print_function,\ 4 | absolute_import 5 | import unicodedata 6 | 7 | 8 | def width(s): 9 | asian = sum(unicodedata.east_asian_width(c) in {'W', 'F'} for c in s) 10 | return len(s) + asian 11 | -------------------------------------------------------------------------------- /pip-requirements.txt: -------------------------------------------------------------------------------- 1 | requests[security] >= 2.13.0 2 | pysodium >= 0.6.9.1 3 | -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ctf_name": "Pwn2Win CTF 2018", 3 | "repository_host": "GitHub", 4 | "submissions_project": "pwn2winctf/2018submissions", 5 | 6 | "scrypt_ops_limit": 33554432, 7 | "scrypt_mem_limit": 402653184, 8 | 9 | "dynamic_scoring": {"K": 80.0, "V": 3.0, "minpts": 50, "maxpts": 500}, 10 | 11 | "max_size_team_name": 50, 12 | "max_size_chall_id": 30, 13 | "max_size_team_countries": 5, 14 | "flag_icon_css_ver": "2.8.0", 15 | 16 | "github_api_endpoint": "https://api.github.com/", 17 | "github_ssh_url": "git@github.com:%s.git", 18 | 19 | "gitlab_api_endpoint": "https://gitlab.com/api/v3/", 20 | "gitlab_ssh_url": "git@gitlab.com:%s.git" 21 | } 22 | -------------------------------------------------------------------------------- /setup-vpn: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | if [[ $# != 3 ]]; then 3 | echo "usage: $0 vpn_addr team_id password" 4 | exit 1 5 | fi 6 | vpn_addr="$1" 7 | team_id="$2" 8 | password="$3" 9 | 10 | echo "team-${team_id}" > auth.txt 11 | echo "$password" >> auth.txt 12 | 13 | vpn_port=$((10000 + $team_id)) 14 | 15 | cat > ctf.ovpn < 39 | -----BEGIN CERTIFICATE----- 40 | MIIDtTCCAp2gAwIBAgIJAKz96Ok7WRJ4MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV 41 | BAYTAkJSMRAwDgYDVQQKEwdQd24yV2luMRMwEQYDVQQDEwpQd24yV2luIENBMQ8w 42 | DQYDVQQpEwZzZXJ2ZXIwHhcNMTcxMDExMDIxMTAxWhcNMjcxMDA5MDIxMTAxWjBF 43 | MQswCQYDVQQGEwJCUjEQMA4GA1UEChMHUHduMldpbjETMBEGA1UEAxMKUHduMldp 44 | biBDQTEPMA0GA1UEKRMGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 45 | CgKCAQEAv0ONla2e6+JrhRElPobYgXyZs9ZlGplo6NYH4n2iOUPODkFfydRMkhqs 46 | T48q7s3sWpHOezr5Qj9SepGUvcYK/9tc7uAn2psUW8FOOK3qGjvw4o6G2x9sI/tS 47 | J6OWKbu84Xy05l6BrRxI+qWVLcYgjogIflXgkwLcFLUA19uGaQYzaPO4csGtGVPC 48 | oS0mrn/GgyH6RSXN502LUO4b+3LihI5fxf2nQjTb3pdImVMtznbP8XNaq/je5h5q 49 | hQT67DWjXVdZd41awMJlbvbmywdROLYUVMO73q78C1vg6lrr44tNi4D3cYXNwA18 50 | S+99+dDSCiTrtlr0dtGR8AHdOwM8GwIDAQABo4GnMIGkMB0GA1UdDgQWBBQMMm2V 51 | mbmXdQrfFdEZ+A3Vj8lUbjB1BgNVHSMEbjBsgBQMMm2VmbmXdQrfFdEZ+A3Vj8lU 52 | bqFJpEcwRTELMAkGA1UEBhMCQlIxEDAOBgNVBAoTB1B3bjJXaW4xEzARBgNVBAMT 53 | ClB3bjJXaW4gQ0ExDzANBgNVBCkTBnNlcnZlcoIJAKz96Ok7WRJ4MAwGA1UdEwQF 54 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBALJIROdxRx7M+R+OUUK0soIZlIiJEuXA 55 | nPNXvvC3hhYeo54GaiPBmfrDEtp+dgTpTzVuW+nur7M/oSnCAwBvasaUXQU+Am/A 56 | Z1r8zBSIsDDRM3OCfKbqUymjpzGNz7S6GawYIcroak5NW/C8VcuZzo7FTXPSI32u 57 | thfeDTzWTIcXOaKi1efsKgR49JVQ6YVhv5dzHxYtfZa3AGiRQRD4lKfbeQcd+Eh+ 58 | mzr8C4EuOK+YQiXHSyO9DxilNaR3t5LeNyiRH/xC2gFcBJtR1Ep/ZYNdA9TT41Gd 59 | ERKi59X9sSQJ7h+ZM8F56E99/7oW02PUpbxgf4CciLFcQKXk07uZJX8= 60 | -----END CERTIFICATE----- 61 | 62 | EOF 63 | 64 | cat > verify-cn <