CTF

[hkcert] Custom-Web-Server(1) Write Up

wermut 2025. 2. 27. 01:14

 

custom-server-1.zip
2.41MB

 

문제 분석

 

 

 

맨 처음 문제에 접근하면 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