After having solved this challenge, I learned that the syntax used in this C file is actually a real programming language.
Once you read the readme, it will all make sense why the syntax is so weird.
Flag:
DUCTF{I_D0nT_Th1nk_th15_1s_R34L_C} - yes I think so
SimpleFTPServer
Enumeration
Connecting using the netcat command, I was greeted with the text 220 vsFTPd (v2.3.4) ready..., which turn out to be red-herring.
Entering gibberish returns error, however the error indicates that the server is running Python.
Although I am unable to login using the USER and PASS command, I soon realized they are all red-herring since I can list the directory with LIST and change directory with CWD.
Using RETR to retrieve a file on flag.txt, I received a fake flag:
and accessing pwn give us access to the source code of the app:
#!/usr/bin/env python3
# Adapted from: https://gist.github.com/ZoeS17/467387af22de19c028f0430dcfc5ada8#file-ftpserver-py-L83
# FTP spec comments borrowed from Wikipedia
import os,time,operator,sys
allow_delete = False
local_ip = '0.0.0.0'
local_port = 1337
currdir = os.path.abspath('.')
ENCODING = "utf-8"
f = open("/chal/flag.txt", "r")
FLAG = f.read()
f.close()
f = open("/chal/pwn", "r")
SOURCE_CODE = f.read()
f.close()
class FTPServerThread():
def __init__(self):
self.basewd=currdir
self.cwd=self.basewd
self.rest=False
self.pasv_mode=False
self.mode='A'
def run(self):
sys.stdout.buffer.write('220 vsFTPd (v2.3.4) ready...\r\n'.encode(ENCODING)) # Red Herring
while True:
sys.stdout.flush()
recv=sys.stdin.buffer.readline()
if not recv: break
else:
lst = recv.decode(ENCODING).strip().split(" ", 1)
if len(lst) < 2:
cmd, args = lst[0], ""
else:
cmd, args = lst
try:
func=operator.attrgetter(cmd)(self)
msg = func(args)
sys.stdout.buffer.write(f'{msg}'.encode(ENCODING))
except Exception as e:
sys.stdout.buffer.write(f'500 Sorry. {e}\r\n'.encode(ENCODING))
def SYST(self,args):
'''
Return system type.
'''
return '215 UNIX Type: L8\r\n'
def OPTS(self,args):
'''
RFC 2389 Select options for a feature (for example OPTS UTF8 ON).
'''
if args.upper()=='UTF8 ON':
return '200 OK.\r\n'
else:
return '451 Sorry.\r\n'
def USER(self,args):
'''
Authentication username.
'''
return '331 OK.\r\n'
def PASS(self,args):
'''
Authentication password.
'''
# return '230 OK.\r\n'
return '530 Incorrect.\r\n' # Red Herring
def QUIT(self,args):
'''
Disconnect.
'''
sys.stdout.buffer.write('221 Goodbye.\r\n'.encode(ENCODING))
exit()
def NOOP(self,args):
'''
No operation (dummy packet; used mostly on keepalives).
'''
return '200 OK.\r\n'
def TYPE(self,args):
'''
Sets the transfer mode
'''
if args == "A":
self.mode=args
return '200 ASCII mode.\r\n'
if args == "I":
self.mode=args
return '200 Binary mode.\r\n'
return '504 Command not implemented for that parameter.\r\n'
def CDUP(self,args):
'''
Change to Parent Directory.
'''
if not os.path.samefile(self.cwd,self.basewd):
#learn from stackoverflow
self.cwd=os.path.abspath(os.path.join(self.cwd,'..'))
return '200 OK.\r\n'
def PWD(self,args):
'''
Print working directory. Returns the current directory of the host.
'''
cwd=os.path.relpath(self.cwd,self.basewd)
if cwd=='.':
cwd='/'
else:
cwd='/'+cwd
return '257 \"%s\"\r\n' % cwd
def CWD(self,args):
'''
RFC 697 Change working directory.
'''
chwd=args
if chwd=='/':
self.cwd=self.basewd
elif chwd[0]=='/':
self.cwd=os.path.join(self.basewd,chwd[1:])
else:
self.cwd=os.path.join(self.cwd,chwd)
return '250 OK.\r\n'
def PORT(self,args):
'''
Specifies an address and port to which the server should connect.
'''
return '501 Syntax error in parameters or argument.\r\n'
def PASV(self,args):
'''
Enter passive mode.
'''
return '227 Entering Passive Mode (%s,%u,%u).\r\n' % ("ip_here", "port_here", "port_here") # Hint that this isn't a real server
def LIST(self,args): # Red Herring, you see the flag.txt but get it
sys.stdout.buffer.write(b'150 Here comes the directory listing.\r\n')
for t in os.listdir(self.cwd):
k=self.toListItem(os.path.join(self.cwd,t))
sys.stdout.buffer.write((k+'\r\n').encode(ENCODING))
return '226 Directory send OK.\r\n'
def toListItem(self,fn):
st=os.stat(fn)
fullmode='rwxrwxrwx'
mode=''
for i in range(9):
mode+=((st.st_mode>>(8-i))&1) and fullmode[i] or '-'
d=(os.path.isdir(fn)) and 'd' or '-'
ftime=time.strftime(' %b %d %H:%M ', time.gmtime(st.st_mtime))
return d+mode+' 1 user group '+str(st.st_size)+ftime+os.path.basename(fn)
def MKD(self,args):
dn=os.path.join(self.cwd,args)
os.mkdir(dn)
return '257 Directory created.\r\n'
def RMD(self,args):
dn=os.path.join(self.cwd,args)
if allow_delete:
# os.rmdir(dn) # No mutations for you
return '250 Directory deleted.\r\n'
else:
return '450 Not allowed.\r\n'
def DELE(self,args):
fn=os.path.join(self.cwd,args)
if allow_delete:
# os.remove(fn) # No mutations for you
return '250 File deleted.\r\n'
else:
return '450 Not allowed.\r\n'
def RNFR(self,args):
self.rnfn=os.path.join(self.cwd,args)
return '350 Ready.\r\n'
def RNTO(self,args):
fn=os.path.join(self.cwd,args)
# os.rename(self.rnfn,fn)
return '250 File renamed.\r\n'
def REST(self,args):
self.pos=int(args)
self.rest=True
return '250 File position reseted.\r\n'
def RETR(self,args):
fn=os.path.join(self.cwd,args)
if self.mode=='I':
fi=open(fn,'rb')
else:
fi=open(fn,'r')
sys.stdout.buffer.write('150 Opening data connection.\r\n'.encode(ENCODING))
if self.rest:
fi.seek(self.pos)
self.rest=False
# Deny the flag, but make it clear they are on the correct track.
if "flag.txt" in args:
sys.stdout.write('DUCTF{- Actually no, I don\'t feel like giving that up yet. ;)\r\n')
# "Leak" source code by allowing download of it.
if "pwn" in args:
sys.stdout.write(f'{SOURCE_CODE}\r\n')
# data= fi.read(1024)
# self.start_datasock()
# while data:
# self.datasock.send(data)
# data=fi.read(1024)
# fi.close()
# self.stop_datasock()
return '226 Transfer complete.\r\n'
def STOR(self,args):
fn=os.path.join(self.cwd,args)
if self.mode=='I':
fo=open(fn,'wb')
else:
fo=open(fn,'w')
sys.stdout.buffer.write('150 Opening data connection.\r\n'.encode(ENCODING))
# self.start_datasock()
# while True:
# data=self.datasock.recv(1024)
# if not data: break
# fo.write(data)
# fo.close()
# self.stop_datasock()
return '226 Transfer complete.\r\n'
FTPServerThread().run()
Looking at this script, it appears that all FTP commands are useless (they only return text, unable to return file, os module not interacting with the system). However, the way our input get processed is interesting:
while True:
sys.stdout.flush()
recv=sys.stdin.buffer.readline()
if not recv: break
else:
lst = recv.decode(ENCODING).strip().split(" ", 1)
if len(lst) < 2:
cmd, args = lst[0], ""
else:
cmd, args = lst
try:
func=operator.attrgetter(cmd)(self)
msg = func(args)
sys.stdout.buffer.write(f'{msg}'.encode(ENCODING))
except Exception as e:
sys.stdout.buffer.write(f'500 Sorry. {e}\r\n'.encode(ENCODING))
First, our inputs are written into cmd and args using list unpacking.
After that, our input is processed using operator.attrgetter(cmd)(self), so if we entered LIST, it will attempt to call FTPServerThread().LIST(self).
If there are additional arguments being passed in, it will be called by the function in the variable func.
And if the command you run has error, it will return.
--> At this point we can confirm this challenge is indeed PyJail and we need to retrieve the FLAG constant.
Payload
My first attempt is to use basic PyJail payload in order to reach global function then call __import__ to import system modules. However, as I mentioned before, every function call will be called with self. So my payload will now become:
So my thought now is to find a function that accept self but still can reach global variables. Luckily, we have a function defined inside this class already - toListItem.
Running this function directly returns a different error, indicating we are going in the right direction.
This is what happens behind the scene:
And since FLAG is a global variable, we can use get function of a dictionary to read the value of FLAG.
baby ruby
#!/usr/bin/env ruby
while input = STDIN.gets.chomp do eval input if input.size < 5 end
This script allows us to input code which is less than 5 characters, then execute it. Therefore, our ultimate goal is to call a shell to execute longer command on the server.
Googling a bit, we already know that backticks (``) can be used to execute system command.
You can watch this video to understand how backticks works:
So far, we know that the output of the command is not being redirected to STDOUT, but to STDERR instead. So we can't see anything. Therefore, we just need to redirect our output it to STDERR using whoami 1>&2, for example.
Rougeful Helper
Unpacking the zip file reveals a file directory structure of a Windows machine.
Checking all of them to find something network-scannning-related, these are some suspicious directory:
\Files\Program Files\VSA X\Probe
\Files\Windows\System32\Npcap
\Files\Program Files\Npcap
Only the VSA X folder has interesting data. In VSA X\Probe\tmp\scan-run, there are many logs file refering to a network scanning that occurs after the 15:32:20 timestamp. Our answer is in the ndp file, which stores all the payload and options used.