DaVinciCTF 2021 writeups

Writeup mar 18, 2021

Last weekend we enjoyed competing in DaVinciCTF 2021 and we reached 10th place. It has been an awesome CTF especially for our new team members. Hope these writeups will be useful to someone.  


  • Category: Web
  • Points: 10
  • Solves: 266
  • Solved by: Lu191


Can you find a way to authenticate as admin?



The name of the challenge tells us that we need a way to authenticate as admin, so as we don't know the password, we have to bypass the in some way authentication.


The most common attack that we can try on this login page to bypass authentication is a SQL Injection.
We try to login with the username admin and the password ' or 1 -- - and indeed it worked, we successfully bypassed authentication with a SQL Injection.
Now let’s find the flag, let's inspect the code of the page where we have been redirected and indeed, and indeed there we find the flag.



Bootless RSA

  • Category: Crypto
  • Points: 25
  • Solves: 127
  • Solved by: Crypt3d4ta, 4cu1


We have JSON file in which we found:

  • N = 148818474926605063920889194160313225216327492347368329952620222220173505969004341728021623813340175402441807560635794342531823708335067243413446678485411066531733814714571491348985375389581214154895499404668547123130986872208497176485731000235899479072455273651103419116166704826517589143262273754343465721499
  • e = 3
  • ct = 4207289555943423943347752283361812551010483368240079114775648492647342981294466041851391508960558500182259304840957212211627194015260673748342757900843998300352612100260598133752360374373

We immediately notice that the ciphertext is small compared to N.
We also have a low public exponent.

So we can get the plaintext message by simply making the third root of the ciphertext.

For simplicity, we put the following expression on wolframalpha.com:

"4207289555943423943347752283361812551010483368240079114775648492647342981294466041851391508960558500182259304840957212211627194015260673748342757900843998300352612100260598133752360374373 ^ (1/3)"

and we obtain: "161436153337866397698230350131849911745245662937350542382627197"

Converting to base 16 results: "64764354467B5253345F6D3064756C305F696E66316E6974797D"

So now, converting to ascii we get "dvCTF{RS4_m0dul0_inf1nity}"

Da Vinci vs Pacioli

  • Category: Stega
  • Points: 74
  • Solves: 18
  • Solved by: 4cu1, hdesk, M4tex00


What an amazing game. Can you find my passwords?

Flag format: dvCTF{password}


In this challenge we are given a zip file called 'important_files'.
Once the content has been extracted, we are faced with another zip and a file with the .pgn (Portable Game Notation) extension, a format used to record chess games.
The 'chest.zip' file is password protected. First we look in the 'amazing_game.pgn' file and try to get some information.

[Event "Playing chess between drawings"]
[Date "2021.02.28"]
[White "Leonardo Da Vinci"]
[Black "Luca Pacioli"]
[Result "0-1"]
[UTCDate "1489.02.28"]
[UTCTime "11:35:18"]
[Variant "Standard"]
[TimeControl "-"]
[ECO "A00"]
[Opening "Anderssen Opening"]
[Termination "Normal"]

1. d4 { A40 Queen's Pawn Game } h5 2. g3 Rh7 3. b3 f6 4. Bh6 Kf7 5. Nc3 a5 6. Bh3 Qe8 7. a4 Kg6 8. f4 Kf7 9. e4 Na6 10. Nce2 Nb4 
2. 11. Rb1 Qd8 12. Kf1 Na2 13. c4 Nc3 14. Qc2 d6 15. Bxg7 Rxg7 16. Bf5 Ra7 17. h3 Rg4 18. Bg6+ Rxg6 19. Rb2 d5 20. Kg2 Qd7 
3. 21. Qc1 Ke6 22. b4 Nb1 23. Qd2 Nxd2 24. Rb3 Nh6 25. Kh2 dxc4 26. Re3 h4 27. gxh4 Nb1 28. Nf3 { Black resigns. } 1-0

We know that is possible to encode and decode data in a chess game via chess steganography.

Using the following site: https://incoherency.co.uk/chess-steg we can decode the moves of the game and get the string 3nfW@XuAT4LS4B5HmWBD&qMM5@RqVMgs which turns out to be the zip archive password.

At this point we can extract the contents of the chest.zip file. We get two files: keys.kdbx and nothing_to_see_here.

To access the encrypted database we need a master password.

Even though the file is called "nothing to see here" we still tried to take a look. Also because the file has a large size (1,3 GB).
The output of the command strings nothing_to_see_here appears to return a repeating pattern.


and so on.

To better analyze the content we redirect the output to a file with strings nothing_to_see_here > out.txt

Let's try to find out if there is something different from the usual pattern.

By running sort -u out.txt > out2.txt followed by cat out2.txt we obtain:


As we suspected!

By converting the hexadecimal string we get the database master password: sup4_dup4_p4ssw0rd_4m4z1ng_n01_w1ll_susp3ct

Now we can access the database. Inside we found various accounts with their passwords.

From the description of the challenge we know that a password must be entered between the flag format.

After few attemps we find the right one that corresponds to MQNFZ0VPGDsfeQCvudeX.

We put in the flag format, sent it and BOOM, we got the points.



Flipping tables

  • Category: Crypto
  • Points: 73
  • Solves: 23
  • Solved by: SM_SC2, 0xThorn, Th3Revenge


┳━┳ ヽ(ಠل͜ಠ)ノ

nc challs.dvc.tf 3333


The challenge accepts a hex string and prints a custom hash. The most important thing we have to note is that it prints two different answers: E(input || flag) and !E(input || flag). We decided to ignore !E(input || flag) answer and to understand what E(input || flag) means.
So we started inserting a lot of strings of different lengths and taking note of each result.

Input E(input || flag) Length
7caafa90873c58d4640dd8b9894c929ac908eab8b015d40fc92d62674716fc09 64
'0' * 2 a680be8ae7921f855290508120d8f2f06bc1563491713fa0c2f76f0fbbb799d9 64
'0' * 4 e6de7a0fbed3f07fa8069390484814b8322b94a315b8f8ad9329a3df83ee4099 64
'0' * 8 f9952c4d157bac9a94dcb4bd01ad8893d7a39d11517c8ffc12adeb2d60e1b12c 64
'0' * 16 d26b8b947a906dee1875d7c47f3fd830fb08b408dcc9910df47f90788438186d 64
'0' * 32 001f11041f67ed07b61b191f997498167caafa90873c58d4640dd8b9894c929ac908eab8b015d40fc92d62674716fc09 96
'0' * 18 b9d0828cdfd56526f1c5db5800cee3ea18f77807d91f685f7c9ec14b59a479d9155075db7f6ceeafd3d4db2a44eae980 96
'0' * 64 001f11041f67ed07b61b191f99749816001f11041f67ed07b61b191f997498167caafa90873c58d4640dd8b9894c929ac908eab8b015d40fc92d62674716fc09 128
'0' * 48 001f11041f67ed07b61b191f99749816d26b8b947a906dee1875d7c47f3fd830fb08b408dcc9910df47f90788438186d 96
'0' * 50 001f11041f67ed07b61b191f99749816b9d0828cdfd56526f1c5db5800cee3ea18f77807d91f685f7c9ec14b59a479d9155075db7f6ceeafd3d4db2a44eae980 128
'0' * 80 001f11041f67ed07b61b191f99749816001f11041f67ed07b61b191f99749816d26b8b947a906dee1875d7c47f3fd830fb08b408dcc9910df47f90788438186d 128
'0' * 82 001f11041f67ed07b61b191f99749816001f11041f67ed07b61b191f99749816b9d0828cdfd56526f1c5db5800cee3ea18f77807d91f685f7c9ec14b59a479d9155075db7f6ceeafd3d4db2a44eae980 160

We can see that the hash length is a multiple of 32 characters.

Input length Hash Length
0 64
16 64
18 96
48 96
80 128
82 160

Comparing '0' * 32, '0' * 50 and '0' * 64 hashes, we can see that the first 32 characters are the same.




Comparing empty input, '0' * 32, '0' * 64 and 'f'*64 hashes, we can also see that the last 32 characters are the same.





So we understood that E(input || flag) means

  1. Concatenate input with flag
  2. Add padding if len(input) < 32
  3. Encrypt

It seems an Oracle padding.



The idea is to brute force each character of the flag:

  1. Send '0' * (BLOCK_SIZE - 2) and take note of the hash ( encrypted_flag )
  2. For each printable character c, send hex(c) padded with BLOCK_SIZE - 2 zeros and store the hash ( encrypted_char )
  3. If encrypted_flag == encrypted_char we found the first character
  4. Reduce padding of 2 characters and append the encrypted_flag
  5. Repeat until you complete the first block

We have the first part of the flag dvCTF{3CB_4ngry_.
Now we have to change BLOCK_SIZE to 64 characters and repeat all using the first part of the hashed flag.

To be sure that the idea was good, we first compared '0' * 30 hash with '0' * 30 + 'd' hash since we knew that the flag should have started with d.



Format me

  • Category: Pwn
  • Points: 90
  • Solves: 47
  • Solved by: drw0if


nc challs.dvc.tf 8888


When we connected to the service we reached a command line service which reverse the input we provide. Since the challenge name reminds us about format string we attempted to leak informations:

%x %d %x

No particular output is provided... Let's try to puts it in reverse order:

x% d% x%

Boom, format string exploit confirmed! We sprayed some %{i}$s to leak all the string pointed by stack values and we got the flag.


  • Category: Pwn
  • Points: 59
  • Solves: 92
  • Solved by: drw0if - hdesk


nc challs.dvc.tf 4444 or nc challs.dvc.tf 7777


Common buffer overflow with jump to a never called function, since there is no PIE enabled we can spam the function address and hope to reach the target


  • Category: Web
  • Points: 66
  • Solves: 61
  • Solved by: raff01


Can you get more information about the members?



In order to solve this challenge it's necessary to have solved the previous one called "Authentication" that permits to access the following web service:


Let's have a look to the web site. We have a page where there is a table with some information about members on the right and a form that allows to search members on the left. By analysing the source code of the page we can see that the form uses the GET method to send search-parameters, so all the text we write will be encoded into the url. Once the server has recived our data, it will return information about members. So it seems there's a MySQL Database back the application. Let's inject some malicious code into the text field:

Leonard" OR 1=1; --

and the application will return all the records of the table, so the application is vulnerable to SQL Injection!


Probably the flag isn't in the same table where are archived all the members. So the best thing to do is to show all the table names and see if there is something interesting. To do this we can use the UNION command that permits to add a malicious sub-query to the original one and get other data from the database. In particular the table information_schema.tables contains the information about all the tables located in the db. Let's inject the following code:


and the application will return all the table names that are stored in the database. If we scroll down we can see two interesting tables: members that is the table that contains all the members information and another table called supa_secret_table, let's analyse it. The table called information_schema.columns contains all the information about the columns of all the tables stored. So we can get the field names of supa_secret_table by injecting this code:


the application will return two records with the name of the fields: id and flag. So now let's get the content of flag:

Leonard" UNION (SELECT flag,2,3 FROM supa_secret_table); --

and the application will print the flag!




  • Category: Scripting
  • Points: 98
  • Solvers: 25
  • Solved by: Crypt3d4ta, 4cu1


nc challs.dvc.tf 3096


When we try to connect to the service, the server show us this text:
Let's play a game. If you can tell me what number I am thinking of, I will give you the flag. What number am I thinking of?

When we try to guess the number, the server throws out:
Nice try! I was thinking of <random_number>

We assumed that the number are generated with python random module based on Mersenne Twister, a pseudo random generator.
To solved this challenge we wrote this script that at the end it worked perfectly.

from pwn import *
from mt19937predictor import MT19937Predictor

conn = remote('challs.dvc.tf',3096)

numbers = []

for i in range(624):
    conn.sendline('3') # or any other value
    answer = conn.recvline().decode('ascii')
    split_answer = answer.split(' ')
    number = int(split_answer[12])


predictor = MT19937Predictor()
for i in numbers:

guess = predictor.getrandbits(32)

flag: dvCTF{tw1st3d_numb3rs}


  • Category: Pwn
  • Points: 499
  • Solves: 15
  • Solved by: drw0if - hdesk


I created this amazing service to store all my famous quotes. Can you get the flag?

nc challs.dvc.tf 2222


Since we are given the source code we started analyzing the general beaviour of the service: we can manage a list of quotes which are composed of:

  • content
  • content size
  • title
  • title size
  • function pointer to quote_write function
  • function pointer to quote_read function

For each quote we add the service asks malloc a chunk of memory for the main struct (48 bytes), a chunk for the title and a chunk for the content, we can choose che size for both the title and the content, and it increments a global counter book_ctr. Whenever we delete a quote it decrements book_ctr and frees the specified struct number.

At a first look the service appears to be vulnerable to double free and use-after-free:

  • if we have 10 quotes and we ask to delete the first one it gets freed but the counter decrements to 9, so we can now delete all the structs from 1 to 9, so we can free again the first quote;
  • if we free the first struct and book_ctr is more than one we can modify the first struct again.

We started creating 10 quotes and deleting 1 and 2 so we could fill up the fastbin list. We created a new quote and listing them we discovered that the second and the last one were identical: the fastbin attack can be abused!

Next we looked for a way to gather arbitrary read/write: we thought about creating a quote whose content pointer points to a user specified address, to achieve this we chained:

  • allocate 10 quotes
  • free 1 and 2 in order
  • create a new quote whose content size is the same as struct size

in this way we got a new struct which is the same as the second one but the content pointer points to the base of the first struct since we asked for same size chunks. With this memory layout writing onto the second quote content we can overwrite the first quote content pointer which is used for quote editing.

Next step is to leak some libc address like fgets so we can calculate the offset at which libc has been loaded and then the real address of system, in order to achieve this we need to overwrite the first struct content pointer to the fgets GOT entry, then we can read the content and leak the address to do our calculations.

We are almost done: we need a libc function called with our directly input:

  • no printf or puts is called with our input
  • fgets is used to read the menu option so if we overwrite it we can't control the program anymore
  • sscanf seems to be the only useful function cause it is used with our direct input to parse it in the menu function

We overwrite the sscanf GOT entry with system and launched /bin/sh.

In the end the exploit:

  • allocate then quotes
  • delete quote 1 to populate fastbin
  • delete quote 2 to populate fastbin
  • create a new quote with content_size set to 48
  • edit the quote 9 (or 2) to overwrite quote 1 content pointer to sscanf@got address
  • display quote 1 content to leak sscanf real address
  • calculate the offset and the real system address
  • edit the quote 1 content to set system address instead of sscanf
  • send /bin/sh command
  • profit


  • Category: Stega
  • Points: 10
  • Solves: 131
  • Solved by: raff01


Just read!!



In this challenge we'are given an image called "flag.png". The description says "Just read!!" so probably the flag is hidden in the image somehow. Let's analyse the image with a Steganographic tool like Stegsolve. If we slide the various planes we won't find anything but if we pay attention to the center of the image we can notice something strange, probably it's the flag!


Let's analyse the image with the stereogram solver of Stegsolve. This tool permits us to set the offset. If we increment it we can see that something is appearing in the center of the image. If we set the offset to 160 we will obtain the flag!




Rocca Pia

  • Category: Reverse
  • Points: 50
  • Solves: 124
  • Solved by: Lu191


Help me! I can't find the password for this binary!


We start analyzing the file that we need to reverse, it's not stripped so it will be easier to reverse because we have all symbols.
Let's start to analyze it with ghidra, looking at the main function.

undefined8 main(int param_1,undefined8 *param_2)

  int iVar1;
  undefined8 uVar2;
  if (param_1 == 2) {
    iVar1 = transform(param_2[1]);
    if (iVar1 == 0) {
      puts("Nice flag");
    else {
      puts("Nice try");
    uVar2 = 0;
  else {
    printf("Usage: %s <password>\n",*param_2);
    uVar2 = 1;
  return uVar2;

We see that the only function that we'll need to look at is transform that is used to check if the password that in this case is also the flag is correct or not, this will be passed as an argument when we launch the binary.
Now we need to analyze the transform function which takes as input the first argument with which the binary is called.

void transform(long param_1)

  size_t sVar1;
  uint local_24;
  char *local_20;
  local_24 = 0;
  while( true ) {
    sVar1 = strlen("wAPcULZh\x7f\x06x\x04LDd\x06~Z\"YtJNice flag");
    if (sVar1 <= (ulong)(long)(int)local_24) break;
    if ((local_24 & 1) == 0) {
      local_20[(int)local_24] = *(byte *)(param_1 + (int)local_24) ^ 0x13;
    else {
      local_20[(int)local_24] = *(byte *)(param_1 + (int)local_24) ^ 0x37;
    local_24 = local_24 + 1;
  strncmp("wAPcULZh\x7f\x06x\x04LDd\x06~Z\"YtJNice flag",local_20,0x16);

This function takes the string that we passed as an argument when we called the binary and perform an xor operation with each character of the string, it use a counter variable local_24 that is set initially to 0 and gets incremented each iteration of the while loop, it's used as an index of the string that is transformed, if the current value of this counter is even the character at the specific index of the string (input[local_24]) gets xored with 0x13 otherwise it gets xored with 0x37, the loop ends when the counter is equal to the length of a string that is later compared with the transformed string that is modified in the while loop.


At this point we now that we can reverse the XOR operation (z = x XOR y then x = z XOR y), but first we need to get the hex values of the string that is used for the last comparison.

                             PASSWD   XREF[3]:     Entry Point(*),                                                             transform:00101221(*), 
        00102010 77              ??         77h    w
        00102011 41              ??         41h    A
        00102012 50              ??         50h    P
        00102013 63              ??         63h    c
        00102014 55              ??         55h    U
        00102015 4c              ??         4Ch    L
        00102016 5a              ??         5Ah    Z
        00102017 68              ??         68h    h
        00102018 7f              ??         7Fh    
        00102019 06              ??         06h
        0010201a 78              ??         78h    x
        0010201b 04              ??         04h
        0010201c 4c              ??         4Ch    L
        0010201d 44              ??         44h    D
        0010201e 64              ??         64h    d
        0010201f 06              ??         06h
        00102020 7e 5a 22 59     ddw        59225A7Eh
        00102024 74              ??         74h    t
        00102025 4a              ??         4Ah    J
                             DAT_00102026                                    XREF[1]:     main:00101286(*)  
        00102026 4e              ??         4Eh    N
        00102027 69              ??         69h    i
        00102028 63              ??         63h    c
        00102029 65              ??         65h    e
        0010202a 20              ??         20h     
        0010202b 66              ??         66h    f
        0010202c 6c              ??         6Ch    l
        0010202d 61              ??         61h    a
        0010202e 67              ??         67h    g
        0010202f 00              ??         00h

Then we wrote the script that performs the xor operations we found in the transform function on this string, and we got the flag.

flag_xored = [0x77, 0x41, 0x50, 0x63, 0x55, 0x4c, 0x5a, 0x68, 0x7f, 0x06, 0x78, 0x04, 0x4c, 0x44, 0x64, 0x06, 0x7e, 0x5a, 0x22, 0x59, 0x74, 0x4a, 0x4e, 0x69, 0x63, 0x65, 0x20, 0x66, 0x6c, 0x61, 0x67]
flag = []

for i in range(len(flag_xored)):
    if(i % 2  == 0):
        flag.append(chr(flag_xored[i] ^ 0x13))
        flag.append(chr(flag_xored[i] ^ 0x37))